use anyhow::{Context, Result};
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;

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

/// AMP (Application Management Panel) adapter.
///
/// Communicates with AMP instances via their REST API to manage
/// Rust game servers. AMP uses session-based auth: POST to
/// /API/Core/Login returns a sessionID that must be sent (UPPERCASED)
/// in all subsequent request bodies. All endpoints are POST at
/// /API/{Module}/{Method}.
pub struct AmpAdapter {
    http: Client,
    pub api_endpoint: String,
    username: String,
    password: String,
    session_id: Arc<RwLock<Option<String>>>,
}

// -- AMP API request/response types --

#[derive(Debug, Serialize)]
struct AmpLoginRequest {
    username: String,
    password: String,
    token: String,
    #[serde(rename = "rememberMe")]
    remember_me: bool,
}

#[derive(Debug, Deserialize)]
struct AmpLoginResponse {
    success: bool,
    #[serde(default, rename = "sessionID")]
    session_id: Option<String>,
    #[serde(default)]
    result: Option<serde_json::Value>,
}

#[derive(Debug, Serialize)]
struct AmpSessionRequest {
    #[serde(rename = "SESSIONID")]
    session_id: String,
}

#[derive(Debug, Serialize)]
struct AmpConsoleRequest {
    #[serde(rename = "SESSIONID")]
    session_id: String,
    message: String,
}

#[derive(Debug, Serialize)]
struct AmpFileRequest {
    #[serde(rename = "SESSIONID")]
    session_id: String,
    #[serde(rename = "Directory", skip_serializing_if = "Option::is_none")]
    directory: Option<String>,
    #[serde(rename = "Filename", skip_serializing_if = "Option::is_none")]
    filename: Option<String>,
    #[serde(rename = "Data", skip_serializing_if = "Option::is_none")]
    data: Option<String>,
}

#[derive(Debug, Deserialize)]
struct AmpGenericResponse {
    #[serde(default)]
    #[allow(dead_code)]
    success: bool,
    #[serde(default, rename = "Status")]
    status: Option<AmpStatusData>,
    #[serde(default, rename = "result")]
    result: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize)]
struct AmpStatusData {
    #[serde(default, rename = "State")]
    state: i32,
    #[serde(default, rename = "Uptime")]
    uptime: Option<String>,
    #[serde(default, rename = "Metrics")]
    metrics: Option<AmpMetrics>,
}

#[derive(Debug, Deserialize)]
struct AmpMetrics {
    #[serde(default, rename = "CPU Usage")]
    cpu_usage: Option<AmpMetricValue>,
    #[serde(default, rename = "Memory Usage")]
    memory_usage: Option<AmpMetricValue>,
}

#[derive(Debug, Deserialize)]
struct AmpMetricValue {
    #[serde(default, rename = "RawValue")]
    raw_value: f64,
}

#[derive(Debug, Deserialize)]
struct AmpInstance {
    #[serde(rename = "InstanceID")]
    instance_id: String,
    #[serde(rename = "InstanceName")]
    instance_name: String,
    #[serde(default, rename = "IP")]
    ip: Option<String>,
    #[serde(default, rename = "Port")]
    port: Option<i32>,
    #[serde(default, rename = "Running")]
    running: bool,
}

#[derive(Debug, Deserialize)]
struct AmpFileEntry {
    #[serde(rename = "Filename")]
    filename: String,
    #[serde(rename = "IsDirectory")]
    is_directory: bool,
    #[serde(default, rename = "SizeBytes")]
    size_bytes: Option<i64>,
    #[serde(default, rename = "Modified")]
    modified: Option<String>,
}

impl AmpAdapter {
    pub fn new(api_endpoint: String, username: String, password: String) -> Self {
        Self {
            http: Client::new(),
            api_endpoint: api_endpoint.trim_end_matches('/').to_string(),
            username,
            password,
            session_id: Arc::new(RwLock::new(None)),
        }
    }

    /// Authenticate with AMP and store the session ID.
    async fn login(&self) -> Result<String> {
        let url = format!("{}/API/Core/Login", self.api_endpoint);

        let body = AmpLoginRequest {
            username: self.username.clone(),
            password: self.password.clone(),
            token: String::new(),
            remember_me: false,
        };

        let response = self
            .http
            .post(&url)
            .json(&body)
            .send()
            .await
            .context("Failed to connect to AMP API")?;

        let login: AmpLoginResponse = response
            .json()
            .await
            .context("Failed to parse AMP login response")?;

        if !login.success {
            anyhow::bail!("AMP login failed — check credentials");
        }

        let sid = login
            .session_id
            .context("AMP login succeeded but no sessionID returned")?
            .to_uppercase();

        let mut lock = self.session_id.write().await;
        *lock = Some(sid.clone());

        Ok(sid)
    }

    /// Get a valid session ID, logging in if necessary.
    async fn get_session_id(&self) -> Result<String> {
        let existing = self.session_id.read().await.clone();
        match existing {
            Some(sid) => Ok(sid),
            None => self.login().await,
        }
    }

    /// Make an authenticated POST request to AMP. Retries once on auth failure.
    async fn amp_post<T: Serialize>(
        &self,
        module: &str,
        method: &str,
        body: &T,
    ) -> Result<serde_json::Value> {
        let url = format!("{}/API/{}/{}", self.api_endpoint, module, method);

        let response = self
            .http
            .post(&url)
            .json(body)
            .send()
            .await
            .with_context(|| format!("AMP API call failed: {module}/{method}"))?;

        let status = response.status();
        if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
            // Session expired — re-login and retry
            let sid = self.login().await?;
            tracing::debug!("AMP session expired, re-authenticated: {}", &sid[..8]);

            // We need to re-serialize with new session ID — caller must retry.
            anyhow::bail!("AMP session expired, please retry");
        }

        let value: serde_json::Value = response
            .json()
            .await
            .with_context(|| format!("Failed to parse AMP response for {module}/{method}"))?;

        Ok(value)
    }

    /// Convenience: make a session-only request (no extra params).
    async fn amp_session_call(
        &self,
        module: &str,
        method: &str,
    ) -> Result<serde_json::Value> {
        let sid = self.get_session_id().await?;
        let body = AmpSessionRequest { session_id: sid };
        self.amp_post(module, method, &body).await
    }
}

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

    async fn discover_servers(&self) -> Result<Vec<DiscoveredServer>> {
        let value = self.amp_session_call("ADSModule", "GetInstances").await?;

        let instances: Vec<AmpInstance> =
            serde_json::from_value(value.get("result").cloned().unwrap_or_default())
                .unwrap_or_default();

        Ok(instances
            .into_iter()
            .map(|i| DiscoveredServer {
                panel_server_id: i.instance_id,
                name: i.instance_name,
                ip: i.ip,
                port: i.port,
                game_port: None,
                status: if i.running {
                    "running".to_string()
                } else {
                    "stopped".to_string()
                },
            })
            .collect())
    }

    async fn get_server_status(&self, _server_id: &str) -> Result<ServerStatus> {
        let value = self.amp_session_call("Core", "GetStatus").await?;

        let resp: AmpGenericResponse =
            serde_json::from_value(value).context("Failed to parse AMP status")?;

        let status = resp.status.unwrap_or(AmpStatusData {
            state: 0,
            uptime: None,
            metrics: None,
        });

        // AMP state codes: 0=Stopped, 5=PreStart, 10=Configuring, 20=Starting, 30=Ready, 40=Restarting
        let is_running = status.state >= 30;

        let (cpu, mem) = status.metrics.map_or((None, None), |m| {
            (
                m.cpu_usage.map(|v| v.raw_value),
                m.memory_usage.map(|v| v.raw_value as i64),
            )
        });

        // Parse uptime string "HH:MM:SS" to seconds
        let uptime_secs = status.uptime.and_then(|u| {
            let parts: Vec<&str> = u.split(':').collect();
            if parts.len() == 3 {
                let h: i64 = parts[0].parse().ok()?;
                let m: i64 = parts[1].parse().ok()?;
                let s: i64 = parts[2].parse().ok()?;
                Some(h * 3600 + m * 60 + s)
            } else {
                None
            }
        });

        Ok(ServerStatus {
            is_running,
            cpu_usage: cpu,
            memory_usage_mb: mem,
            uptime_seconds: uptime_secs,
        })
    }

    async fn start_server(&self, _server_id: &str) -> Result<()> {
        self.amp_session_call("Core", "Start").await?;
        Ok(())
    }

    async fn stop_server(&self, _server_id: &str) -> Result<()> {
        self.amp_session_call("Core", "Stop").await?;
        Ok(())
    }

    async fn restart_server(&self, _server_id: &str) -> Result<()> {
        self.amp_session_call("Core", "Restart").await?;
        Ok(())
    }

    async fn send_command(&self, _server_id: &str, command: &str) -> Result<String> {
        let sid = self.get_session_id().await?;
        let body = AmpConsoleRequest {
            session_id: sid,
            message: command.to_string(),
        };

        let value = self.amp_post("Core", "SendConsoleMessage", &body).await?;
        Ok(value
            .get("result")
            .and_then(|v| v.as_str())
            .unwrap_or("OK")
            .to_string())
    }

    async fn get_file(&self, _server_id: &str, path: &str) -> Result<Vec<u8>> {
        let sid = self.get_session_id().await?;
        let body = AmpFileRequest {
            session_id: sid,
            directory: None,
            filename: Some(path.to_string()),
            data: None,
        };

        let value = self
            .amp_post("FileManagerPlugin", "GetFileContents", &body)
            .await?;

        let contents = value
            .get("result")
            .and_then(|v| v.as_str())
            .unwrap_or_default();

        Ok(contents.as_bytes().to_vec())
    }

    async fn put_file(&self, _server_id: &str, path: &str, data: &[u8]) -> Result<()> {
        let sid = self.get_session_id().await?;
        let content = String::from_utf8_lossy(data).to_string();

        let body = AmpFileRequest {
            session_id: sid,
            directory: None,
            filename: Some(path.to_string()),
            data: Some(content),
        };

        self.amp_post("FileManagerPlugin", "WriteFileChunk", &body)
            .await?;
        Ok(())
    }

    async fn delete_file(&self, _server_id: &str, path: &str) -> Result<()> {
        let sid = self.get_session_id().await?;
        let body = AmpFileRequest {
            session_id: sid,
            directory: None,
            filename: Some(path.to_string()),
            data: None,
        };

        self.amp_post("FileManagerPlugin", "TrashFile", &body)
            .await?;
        Ok(())
    }

    async fn list_files(&self, _server_id: &str, path: &str) -> Result<Vec<FileEntry>> {
        let sid = self.get_session_id().await?;
        let body = AmpFileRequest {
            session_id: sid,
            directory: Some(path.to_string()),
            filename: None,
            data: None,
        };

        let value = self
            .amp_post("FileManagerPlugin", "GetDirectoryListing", &body)
            .await?;

        let entries: Vec<AmpFileEntry> =
            serde_json::from_value(value.get("result").cloned().unwrap_or_default())
                .unwrap_or_default();

        Ok(entries
            .into_iter()
            .map(|e| FileEntry {
                name: e.filename.clone(),
                path: format!("{}/{}", path.trim_end_matches('/'), e.filename),
                is_directory: e.is_directory,
                size_bytes: e.size_bytes,
                modified_at: e.modified,
            })
            .collect())
    }

    async fn trigger_steam_update(&self, _server_id: &str) -> Result<()> {
        self.amp_session_call("Core", "Update").await?;
        Ok(())
    }
}
