use anyhow::{Context, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use uuid::Uuid;

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

/// Default timeout for NATS request/reply operations with the companion agent.
const AGENT_TIMEOUT: Duration = Duration::from_secs(30);

/// Extended timeout for operations that take longer (file transfers, Steam updates).
const AGENT_LONG_TIMEOUT: Duration = Duration::from_secs(120);

// -- Request/Response payloads for companion agent communication --

#[derive(Debug, Serialize)]
struct AgentCommand {
    action: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    command: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    data_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    validate: Option<bool>,
}

#[derive(Debug, Deserialize)]
struct AgentResponse {
    success: bool,
    #[serde(default)]
    message: String,
    #[serde(default)]
    data: serde_json::Value,
}

#[derive(Debug, Deserialize)]
struct AgentServerInfo {
    name: String,
    #[serde(default)]
    ip: Option<String>,
    #[serde(default)]
    port: Option<i32>,
    #[serde(default)]
    game_port: Option<i32>,
    status: String,
}

#[derive(Debug, Deserialize)]
struct AgentStatus {
    is_running: bool,
    #[serde(default)]
    cpu_usage: Option<f64>,
    #[serde(default)]
    memory_usage_mb: Option<i64>,
    #[serde(default)]
    uptime_seconds: Option<i64>,
}

#[derive(Debug, Deserialize)]
struct AgentFileEntry {
    name: String,
    path: String,
    is_directory: bool,
    #[serde(default)]
    size_bytes: Option<i64>,
    #[serde(default)]
    modified_at: Option<String>,
}

/// Companion Agent adapter.
///
/// Unlike AMP and Pterodactyl which use REST APIs, the Companion Agent
/// runs alongside the game server and communicates via NATS JetStream.
/// Commands are published to the agent's subject namespace and responses
/// are received on reply subjects. This enables direct server management
/// without a panel intermediary.
pub struct CompanionAdapter {
    nats: NatsBridge,
    license_id: Uuid,
}

impl CompanionAdapter {
    pub fn new(nats: NatsBridge, license_id: Uuid) -> Self {
        Self { nats, license_id }
    }

    /// Build a NATS subject scoped to this license's agent.
    fn subject(&self, action: &str) -> String {
        format!("corrosion.{}.agent.{}", self.license_id, action)
    }

    /// Send a command to the companion agent and parse the response.
    async fn send_command_to_agent(
        &self,
        action: &str,
        cmd: AgentCommand,
        timeout: Duration,
    ) -> Result<AgentResponse> {
        let subject = self.subject(action);
        self.nats
            .request_json::<AgentCommand, AgentResponse>(&subject, &cmd, timeout)
            .await
            .with_context(|| format!("Companion agent command '{action}' failed"))
    }
}

#[async_trait]
impl PanelAdapter for CompanionAdapter {
    async fn test_connection(&self) -> Result<bool> {
        let cmd = AgentCommand {
            action: "ping".to_string(),
            path: None,
            command: None,
            data_url: None,
            validate: None,
        };

        match self.send_command_to_agent("ping", cmd, Duration::from_secs(5)).await {
            Ok(resp) => Ok(resp.success),
            Err(_) => Ok(false),
        }
    }

    async fn discover_servers(&self) -> Result<Vec<DiscoveredServer>> {
        let cmd = AgentCommand {
            action: "info".to_string(),
            path: None,
            command: None,
            data_url: None,
            validate: None,
        };

        let resp = self.send_command_to_agent("info", cmd, AGENT_TIMEOUT).await?;

        if !resp.success {
            anyhow::bail!("Agent info request failed: {}", resp.message);
        }

        let info: AgentServerInfo =
            serde_json::from_value(resp.data).context("Failed to parse agent server info")?;

        Ok(vec![DiscoveredServer {
            panel_server_id: self.license_id.to_string(),
            name: info.name,
            ip: info.ip,
            port: info.port,
            game_port: info.game_port,
            status: info.status,
        }])
    }

    async fn get_server_status(&self, _server_id: &str) -> Result<ServerStatus> {
        let cmd = AgentCommand {
            action: "status".to_string(),
            path: None,
            command: None,
            data_url: None,
            validate: None,
        };

        let resp = self.send_command_to_agent("status", cmd, AGENT_TIMEOUT).await?;

        if !resp.success {
            anyhow::bail!("Agent status request failed: {}", resp.message);
        }

        let status: AgentStatus =
            serde_json::from_value(resp.data).context("Failed to parse agent status")?;

        Ok(ServerStatus {
            is_running: status.is_running,
            cpu_usage: status.cpu_usage,
            memory_usage_mb: status.memory_usage_mb,
            uptime_seconds: status.uptime_seconds,
        })
    }

    async fn start_server(&self, _server_id: &str) -> Result<()> {
        let cmd = AgentCommand {
            action: "start".to_string(),
            path: None,
            command: None,
            data_url: None,
            validate: None,
        };

        let resp = self.send_command_to_agent("start", cmd, AGENT_LONG_TIMEOUT).await?;

        if !resp.success {
            anyhow::bail!("Failed to start server: {}", resp.message);
        }
        Ok(())
    }

    async fn stop_server(&self, _server_id: &str) -> Result<()> {
        let cmd = AgentCommand {
            action: "stop".to_string(),
            path: None,
            command: None,
            data_url: None,
            validate: None,
        };

        let resp = self.send_command_to_agent("stop", cmd, AGENT_LONG_TIMEOUT).await?;

        if !resp.success {
            anyhow::bail!("Failed to stop server: {}", resp.message);
        }
        Ok(())
    }

    async fn restart_server(&self, _server_id: &str) -> Result<()> {
        let cmd = AgentCommand {
            action: "restart".to_string(),
            path: None,
            command: None,
            data_url: None,
            validate: None,
        };

        let resp = self.send_command_to_agent("restart", cmd, AGENT_LONG_TIMEOUT).await?;

        if !resp.success {
            anyhow::bail!("Failed to restart server: {}", resp.message);
        }
        Ok(())
    }

    async fn send_command(&self, _server_id: &str, command: &str) -> Result<String> {
        let cmd = AgentCommand {
            action: "command".to_string(),
            path: None,
            command: Some(command.to_string()),
            data_url: None,
            validate: None,
        };

        let resp = self.send_command_to_agent("command", cmd, AGENT_TIMEOUT).await?;

        if !resp.success {
            anyhow::bail!("Command execution failed: {}", resp.message);
        }

        // Extract command output from response data
        Ok(resp.data.as_str().unwrap_or(&resp.message).to_string())
    }

    async fn get_file(&self, _server_id: &str, path: &str) -> Result<Vec<u8>> {
        let cmd = AgentCommand {
            action: "file.get".to_string(),
            path: Some(path.to_string()),
            command: None,
            data_url: None,
            validate: None,
        };

        let resp = self
            .send_command_to_agent("file.get", cmd, AGENT_LONG_TIMEOUT)
            .await?;

        if !resp.success {
            anyhow::bail!("File read failed: {}", resp.message);
        }

        // File contents returned as base64-encoded string in data
        let b64 = resp.data.as_str().context("File data not a string")?;
        base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64)
            .context("Failed to decode file data from base64")
    }

    async fn put_file(&self, _server_id: &str, path: &str, data: &[u8]) -> Result<()> {
        // For large files, we upload to Corrosion storage and send a download URL.
        // For small files (<1MB), we inline the data as base64.
        let cmd = if data.len() < 1_048_576 {
            let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, data);
            AgentCommand {
                action: "file.put".to_string(),
                path: Some(path.to_string()),
                command: Some(encoded), // reuse command field for inline data
                data_url: None,
                validate: None,
            }
        } else {
            // Large file: agent should download from a signed URL
            // This requires the map manager to generate a URL first
            anyhow::bail!(
                "Large file upload via companion agent requires signed URL ({}B > 1MB)",
                data.len()
            );
        };

        let resp = self
            .send_command_to_agent("file.put", cmd, AGENT_LONG_TIMEOUT)
            .await?;

        if !resp.success {
            anyhow::bail!("File write failed: {}", resp.message);
        }
        Ok(())
    }

    async fn delete_file(&self, _server_id: &str, path: &str) -> Result<()> {
        let cmd = AgentCommand {
            action: "file.delete".to_string(),
            path: Some(path.to_string()),
            command: None,
            data_url: None,
            validate: None,
        };

        let resp = self
            .send_command_to_agent("file.delete", cmd, AGENT_TIMEOUT)
            .await?;

        if !resp.success {
            anyhow::bail!("File delete failed: {}", resp.message);
        }
        Ok(())
    }

    async fn list_files(&self, _server_id: &str, path: &str) -> Result<Vec<FileEntry>> {
        let cmd = AgentCommand {
            action: "file.list".to_string(),
            path: Some(path.to_string()),
            command: None,
            data_url: None,
            validate: None,
        };

        let resp = self
            .send_command_to_agent("file.list", cmd, AGENT_TIMEOUT)
            .await?;

        if !resp.success {
            anyhow::bail!("Directory listing failed: {}", resp.message);
        }

        let entries: Vec<AgentFileEntry> =
            serde_json::from_value(resp.data).context("Failed to parse file listing")?;

        Ok(entries
            .into_iter()
            .map(|e| FileEntry {
                name: e.name,
                path: e.path,
                is_directory: e.is_directory,
                size_bytes: e.size_bytes,
                modified_at: e.modified_at,
            })
            .collect())
    }

    async fn trigger_steam_update(&self, _server_id: &str) -> Result<()> {
        let cmd = AgentCommand {
            action: "update".to_string(),
            path: None,
            command: None,
            data_url: None,
            validate: Some(true),
        };

        let resp = self
            .send_command_to_agent("update", cmd, AGENT_LONG_TIMEOUT)
            .await?;

        if !resp.success {
            anyhow::bail!("Steam update trigger failed: {}", resp.message);
        }
        Ok(())
    }
}
