use anyhow::{Context, Result};
use async_trait::async_trait;
use reqwest::{header, Client};
use serde::{Deserialize, Serialize};

use super::panel_adapter::{DiscoveredServer, FileEntry, PanelAdapter, ServerStatus};

/// Pterodactyl panel adapter.
///
/// Communicates with Pterodactyl (or Pelican) panels via the Client API.
/// Uses Bearer token auth with `ptlc_` prefix keys. All JSON responses
/// use the `Application/vnd.pterodactyl.v1+json` accept header.
pub struct PterodactylAdapter {
    http: Client,
    pub api_endpoint: String,
    api_key: String,
}

// -- Pterodactyl API response types --

#[derive(Debug, Deserialize)]
struct PteroListResponse<T> {
    data: Vec<PteroDataObject<T>>,
}

#[derive(Debug, Deserialize)]
struct PteroDataObject<T> {
    attributes: T,
}

#[derive(Debug, Deserialize)]
struct PteroServer {
    identifier: String,
    name: String,
    #[serde(default)]
    description: String,
    node: Option<String>,
    #[serde(default)]
    is_suspended: bool,
    #[serde(default)]
    is_installing: bool,
}

#[derive(Debug, Deserialize)]
struct PteroResources {
    current_state: String,
    resources: PteroResourceData,
}

#[derive(Debug, Deserialize)]
struct PteroResourceData {
    #[serde(default)]
    cpu_absolute: f64,
    #[serde(default)]
    memory_bytes: i64,
    #[serde(default)]
    uptime: i64,
}

#[derive(Debug, Deserialize)]
struct PteroFileAttributes {
    name: String,
    #[serde(default)]
    is_file: bool,
    #[serde(default)]
    size: i64,
    #[serde(default)]
    modified_at: Option<String>,
    #[serde(default)]
    mimetype: Option<String>,
}

#[derive(Debug, Deserialize)]
struct PteroSignedUrl {
    attributes: PteroUrlAttributes,
}

#[derive(Debug, Deserialize)]
struct PteroUrlAttributes {
    url: String,
}

#[derive(Debug, Serialize)]
struct PowerSignal {
    signal: String,
}

#[derive(Debug, Serialize)]
struct ConsoleCommand {
    command: String,
}

#[derive(Debug, Serialize)]
struct DeleteFiles {
    root: String,
    files: Vec<String>,
}

impl PterodactylAdapter {
    pub fn new(api_endpoint: String, api_key: String) -> Self {
        Self {
            http: Client::new(),
            api_endpoint: api_endpoint.trim_end_matches('/').to_string(),
            api_key,
        }
    }

    /// Build default headers for Pterodactyl API requests.
    fn headers(&self) -> header::HeaderMap {
        let mut headers = header::HeaderMap::new();
        headers.insert(
            header::AUTHORIZATION,
            header::HeaderValue::from_str(&format!("Bearer {}", self.api_key))
                .expect("Invalid API key for header"),
        );
        headers.insert(
            header::ACCEPT,
            header::HeaderValue::from_static("Application/vnd.pterodactyl.v1+json"),
        );
        headers.insert(
            header::CONTENT_TYPE,
            header::HeaderValue::from_static("application/json"),
        );
        headers
    }

    /// GET request to Pterodactyl API.
    async fn ptero_get(&self, path: &str) -> Result<reqwest::Response> {
        let url = format!("{}{}", self.api_endpoint, path);
        let response = self
            .http
            .get(&url)
            .headers(self.headers())
            .send()
            .await
            .with_context(|| format!("Pterodactyl GET failed: {path}"))?;

        let status = response.status();
        if !status.is_success() {
            let body = response.text().await.unwrap_or_default();
            anyhow::bail!("Pterodactyl API error {status}: {body}");
        }

        Ok(response)
    }

    /// POST request to Pterodactyl API.
    async fn ptero_post<T: Serialize>(&self, path: &str, body: &T) -> Result<reqwest::Response> {
        let url = format!("{}{}", self.api_endpoint, path);
        let response = self
            .http
            .post(&url)
            .headers(self.headers())
            .json(body)
            .send()
            .await
            .with_context(|| format!("Pterodactyl POST failed: {path}"))?;

        let status = response.status();
        // 204 No Content is success for power/command operations
        if !status.is_success() {
            let body = response.text().await.unwrap_or_default();
            anyhow::bail!("Pterodactyl API error {status}: {body}");
        }

        Ok(response)
    }
}

#[async_trait]
impl PanelAdapter for PterodactylAdapter {
    async fn test_connection(&self) -> Result<bool> {
        match self.ptero_get("/api/client").await {
            Ok(_) => Ok(true),
            Err(e) => {
                tracing::warn!("Pterodactyl connection test failed: {e}");
                Ok(false)
            }
        }
    }

    async fn discover_servers(&self) -> Result<Vec<DiscoveredServer>> {
        let response = self.ptero_get("/api/client").await?;

        // Deserialize manually to handle the nested Pterodactyl response format
        let body: serde_json::Value = response
            .json()
            .await
            .context("Failed to parse Pterodactyl server list")?;

        let data = body
            .get("data")
            .and_then(|d| d.as_array())
            .cloned()
            .unwrap_or_default();

        let mut servers = Vec::new();
        for entry in data {
            let attrs = match entry.get("attributes") {
                Some(a) => a,
                None => continue,
            };

            let server: PteroServer = match serde_json::from_value(attrs.clone()) {
                Ok(s) => s,
                Err(_) => continue,
            };

            let status = if server.is_suspended {
                "suspended"
            } else if server.is_installing {
                "installing"
            } else {
                "unknown"
            };

            servers.push(DiscoveredServer {
                panel_server_id: server.identifier.clone(),
                name: server.name,
                ip: server.node,
                port: None,
                game_port: None,
                status: status.to_string(),
            });
        }

        Ok(servers)
    }

    async fn get_server_status(&self, server_id: &str) -> Result<ServerStatus> {
        let path = format!("/api/client/servers/{}/resources", server_id);
        let response = self.ptero_get(&path).await?;

        let body: serde_json::Value = response
            .json()
            .await
            .context("Failed to parse Pterodactyl resources")?;

        let attrs = body
            .get("attributes")
            .cloned()
            .unwrap_or_default();

        let resources: PteroResources =
            serde_json::from_value(attrs).context("Failed to parse resource attributes")?;

        let is_running = resources.current_state == "running";

        Ok(ServerStatus {
            is_running,
            cpu_usage: Some(resources.resources.cpu_absolute),
            memory_usage_mb: Some(resources.resources.memory_bytes / 1_048_576),
            uptime_seconds: Some(resources.resources.uptime / 1000), // Ptero reports ms
        })
    }

    async fn start_server(&self, server_id: &str) -> Result<()> {
        let path = format!("/api/client/servers/{}/power", server_id);
        self.ptero_post(&path, &PowerSignal {
            signal: "start".to_string(),
        })
        .await?;
        Ok(())
    }

    async fn stop_server(&self, server_id: &str) -> Result<()> {
        let path = format!("/api/client/servers/{}/power", server_id);
        self.ptero_post(&path, &PowerSignal {
            signal: "stop".to_string(),
        })
        .await?;
        Ok(())
    }

    async fn restart_server(&self, server_id: &str) -> Result<()> {
        let path = format!("/api/client/servers/{}/power", server_id);
        self.ptero_post(&path, &PowerSignal {
            signal: "restart".to_string(),
        })
        .await?;
        Ok(())
    }

    async fn send_command(&self, server_id: &str, command: &str) -> Result<String> {
        let path = format!("/api/client/servers/{}/command", server_id);
        self.ptero_post(&path, &ConsoleCommand {
            command: command.to_string(),
        })
        .await?;
        // Pterodactyl returns 204 for commands — no response body
        Ok("Command sent".to_string())
    }

    async fn get_file(&self, server_id: &str, path: &str) -> Result<Vec<u8>> {
        let encoded_path = urlencoding::encode(path);
        let api_path = format!(
            "/api/client/servers/{}/files/contents?file={}",
            server_id, encoded_path
        );

        let url = format!("{}{}", self.api_endpoint, api_path);
        let response = self
            .http
            .get(&url)
            .headers(self.headers())
            .send()
            .await
            .context("Pterodactyl file read failed")?;

        let status = response.status();
        if !status.is_success() {
            let body = response.text().await.unwrap_or_default();
            anyhow::bail!("Pterodactyl file read error {status}: {body}");
        }

        let bytes = response.bytes().await.context("Failed to read file bytes")?;
        Ok(bytes.to_vec())
    }

    async fn put_file(&self, server_id: &str, path: &str, data: &[u8]) -> Result<()> {
        let encoded_path = urlencoding::encode(path);
        let api_path = format!(
            "/api/client/servers/{}/files/write?file={}",
            server_id, encoded_path
        );

        let url = format!("{}{}", self.api_endpoint, api_path);
        let response = self
            .http
            .post(&url)
            .headers(self.headers())
            .body(data.to_vec())
            .send()
            .await
            .context("Pterodactyl file write failed")?;

        let status = response.status();
        if !status.is_success() {
            let body = response.text().await.unwrap_or_default();
            anyhow::bail!("Pterodactyl file write error {status}: {body}");
        }

        Ok(())
    }

    async fn delete_file(&self, server_id: &str, path: &str) -> Result<()> {
        // Extract directory and filename from path
        let (root, filename) = match path.rfind('/') {
            Some(pos) => (&path[..pos], &path[pos + 1..]),
            None => ("/", path),
        };

        let api_path = format!("/api/client/servers/{}/files/delete", server_id);
        self.ptero_post(
            &api_path,
            &DeleteFiles {
                root: root.to_string(),
                files: vec![filename.to_string()],
            },
        )
        .await?;

        Ok(())
    }

    async fn list_files(&self, server_id: &str, path: &str) -> Result<Vec<FileEntry>> {
        let encoded_path = urlencoding::encode(path);
        let api_path = format!(
            "/api/client/servers/{}/files/list?directory={}",
            server_id, encoded_path
        );

        let response = self.ptero_get(&api_path).await?;

        let body: serde_json::Value = response
            .json()
            .await
            .context("Failed to parse Pterodactyl file list")?;

        let data = body
            .get("data")
            .and_then(|d| d.as_array())
            .cloned()
            .unwrap_or_default();

        let mut entries = Vec::new();
        for item in data {
            let attrs = match item.get("attributes") {
                Some(a) => a,
                None => continue,
            };

            let file: PteroFileAttributes = match serde_json::from_value(attrs.clone()) {
                Ok(f) => f,
                Err(_) => continue,
            };

            entries.push(FileEntry {
                name: file.name.clone(),
                path: format!("{}/{}", path.trim_end_matches('/'), file.name),
                is_directory: !file.is_file,
                size_bytes: Some(file.size),
                modified_at: file.modified_at,
            });
        }

        Ok(entries)
    }

    async fn trigger_steam_update(&self, server_id: &str) -> Result<()> {
        // Pterodactyl doesn't have a native SteamCMD trigger.
        // The best approach is to stop the server and trigger a reinstall,
        // or send a console command if the server supports it.
        // For Rust servers, stopping + reinstall via the startup API is cleanest.
        let path = format!("/api/client/servers/{}/settings/reinstall", server_id);

        let url = format!("{}{}", self.api_endpoint, path);
        let response = self
            .http
            .post(&url)
            .headers(self.headers())
            .send()
            .await
            .context("Pterodactyl reinstall trigger failed")?;

        let status = response.status();
        if !status.is_success() {
            let body = response.text().await.unwrap_or_default();
            anyhow::bail!("Pterodactyl reinstall error {status}: {body}");
        }

        Ok(())
    }
}
