use anyhow::{bail, Result};
use std::process::Command;
use std::time::Duration;
use std::collections::HashMap;

use crate::kubernetes::KubeSsh;

/// Kubernetes development machine builder with same API as Pod.
///
/// Provides a simple way to provision and manage development pods with SSH access.
/// Uses the exact same builder pattern as regular Kubernetes pods, with automatic
/// SSH enablement added on top.
///
/// # Examples
///
/// ```no_run
/// use herolib_virt::kubernetes::DevelopmentMachine;
///
/// let dev = DevelopmentMachine::new("mydev")
///     .namespace("development")
///     .image("ubuntu:22.04")
///     .cpu(500, 1000)       // requested, limit in millicores
///     .memory(512, 2048)    // requested, limit in MB
///     .env("ENV", "development")
///     .label("role", "developer")
///     .deploy()?;
///
/// // SSH is automatically enabled
/// let ssh_key = dev.get_ssh_key()?;
/// # Ok::<(), anyhow::Error>(())
/// ```
#[derive(Clone)]
pub struct DevelopmentMachine {
    name: String,
    namespace: String,
    image: String,
    cpu_request: u32,
    cpu_limit: u32,
    memory_request: u32,
    memory_limit: u32,
    env_vars: HashMap<String, String>,
    env_from_configmap: Vec<String>,
    env_from_secret: Vec<String>,
    labels: HashMap<String, String>,
    ports: Vec<u16>,
    readiness_http: Option<(String, u16)>,
    liveness_http: Option<(String, u16, u32, u32, u32)>, // path, port, initial_delay, period, failure_threshold
    readiness_tcp: Option<u16>,
    liveness_tcp: Option<(u16, u32, u32, u32)>, // port, initial_delay, period, failure_threshold
    restart_policy: String,
    reset: bool,
    secret_name: String,
}

impl DevelopmentMachine {
    /// Creates a new development machine builder with default settings.
    ///
    /// # Arguments
    ///
    /// * `name` - Name of the development machine (pod name)
    pub fn new<S: Into<String>>(name: S) -> Self {
        let name = name.into();
        let secret_name = format!("{}-ssh-keys", name);

        Self {
            name,
            namespace: "development".to_string(),
            image: "ubuntu:22.04".to_string(),
            cpu_request: 500,
            cpu_limit: 500,
            memory_request: 1024,
            memory_limit: 1024,
            env_vars: HashMap::new(),
            env_from_configmap: Vec::new(),
            env_from_secret: Vec::new(),
            labels: HashMap::new(),
            ports: Vec::new(),
            readiness_http: None,
            liveness_http: None,
            readiness_tcp: None,
            liveness_tcp: None,
            restart_policy: "Always".to_string(),
            reset: false,
            secret_name,
        }
    }

    /// Sets the namespace (default: "development").
    pub fn namespace<S: Into<String>>(mut self, namespace: S) -> Self {
        self.namespace = namespace.into();
        self
    }

    /// Sets the Docker image (default: "ubuntu:22.04").
    pub fn image<S: Into<String>>(mut self, image: S) -> Self {
        self.image = image.into();
        self
    }

    /// Sets CPU resources in millicores (request, limit).
    /// 1000 = 1 core (default: 500m request, 500m limit)
    pub fn cpu(mut self, request: u32, limit: u32) -> Self {
        self.cpu_request = request;
        self.cpu_limit = limit;
        self
    }

    /// Sets memory resources in MB (request, limit).
    /// (default: 1024 MB request, 1024 MB limit)
    pub fn memory(mut self, request: u32, limit: u32) -> Self {
        self.memory_request = request;
        self.memory_limit = limit;
        self
    }

    /// Sets an environment variable (key, value).
    pub fn env<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
        self.env_vars.insert(key.into(), value.into());
        self
    }

    /// Adds all ConfigMap values as environment variables.
    pub fn env_from_configmap<S: Into<String>>(mut self, configmap: S) -> Self {
        self.env_from_configmap.push(configmap.into());
        self
    }

    /// Adds all Secret values as environment variables.
    pub fn env_from_secret<S: Into<String>>(mut self, secret: S) -> Self {
        self.env_from_secret.push(secret.into());
        self
    }

    /// Adds a label (key, value pair).
    pub fn label<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
        self.labels.insert(key.into(), value.into());
        self
    }

    /// Exposes a port on the pod.
    pub fn port(mut self, port: u16) -> Self {
        self.ports.push(port);
        self
    }

    /// Sets HTTP readiness probe.
    /// Returns false if probe fails - pod won't receive traffic.
    pub fn readiness_http<S: Into<String>>(mut self, path: S, port: u16) -> Self {
        self.readiness_http = Some((path.into(), port));
        self
    }

    /// Sets HTTP liveness probe with optional delay, period, and failure threshold.
    /// Pod is restarted if probe fails (default: initial_delay=0, period=10, failure_threshold=3)
    pub fn liveness_http<S: Into<String>>(mut self, path: S, port: u16) -> Self {
        self.liveness_http = Some((path.into(), port, 0, 10, 3));
        self
    }

    /// Sets TCP readiness probe on specified port.
    pub fn readiness_tcp(mut self, port: u16) -> Self {
        self.readiness_tcp = Some(port);
        self
    }

    /// Sets TCP liveness probe on specified port with optional delay, period, and failure threshold.
    /// Pod is restarted if probe fails (default: initial_delay=0, period=10, failure_threshold=3)
    pub fn liveness_tcp(mut self, port: u16) -> Self {
        self.liveness_tcp = Some((port, 0, 10, 3));
        self
    }

    /// Sets initial delay for liveness probe in seconds.
    pub fn initial_delay(mut self, seconds: u32) -> Self {
        if let Some((path, port, _, period, threshold)) = self.liveness_http {
            self.liveness_http = Some((path, port, seconds, period, threshold));
        }
        if let Some((port, _, period, threshold)) = self.liveness_tcp {
            self.liveness_tcp = Some((port, seconds, period, threshold));
        }
        self
    }

    /// Sets period for liveness probe in seconds.
    pub fn period(mut self, seconds: u32) -> Self {
        if let Some((path, port, delay, _, threshold)) = self.liveness_http {
            self.liveness_http = Some((path, port, delay, seconds, threshold));
        }
        if let Some((port, delay, _, threshold)) = self.liveness_tcp {
            self.liveness_tcp = Some((port, delay, seconds, threshold));
        }
        self
    }

    /// Sets failure threshold for liveness probe.
    pub fn failure_threshold(mut self, count: u32) -> Self {
        if let Some((path, port, delay, period, _)) = self.liveness_http {
            self.liveness_http = Some((path, port, delay, period, count));
        }
        if let Some((port, delay, period, _)) = self.liveness_tcp {
            self.liveness_tcp = Some((port, delay, period, count));
        }
        self
    }

    /// Sets restart policy: "always", "onfail", or "never".
    /// Default: "always"
    pub fn restart<S: Into<String>>(mut self, policy: S) -> Self {
        self.restart_policy = policy.into();
        self
    }

    /// If set to true, will delete and recreate the pod (default: false).
    pub fn reset(mut self, reset: bool) -> Self {
        self.reset = reset;
        self
    }

    /// Sets the secret name for SSH keys (default: "{name}-ssh-keys").
    pub fn secret_name<S: Into<String>>(mut self, secret_name: S) -> Self {
        self.secret_name = secret_name.into();
        self
    }

    /// Checks if a development machine pod already exists.
    pub fn exists(&self) -> Result<bool> {
        let cmd = format!(
            "kubectl get pod {} -n {} --ignore-not-found",
            self.name, self.namespace
        );

        let output = Command::new("sh")
            .arg("-c")
            .arg(&cmd)
            .output()?;

        Ok(output.status.success() && !output.stdout.is_empty())
    }

    /// Checks if the pod is running.
    pub fn is_running(&self) -> Result<bool> {
        let cmd = format!(
            "kubectl get pod {} -n {} -o jsonpath='{{.status.phase}}'",
            self.name, self.namespace
        );

        let output = Command::new("sh")
            .arg("-c")
            .arg(&cmd)
            .output()?;

        let phase = String::from_utf8_lossy(&output.stdout);
        Ok(phase == "Running")
    }

    /// Deploys the development machine.
    ///
    /// If the pod already exists and reset is false:
    /// - Waits for it to be running
    /// - Enables SSH if not already enabled
    /// - Verifies SSH connection
    ///
    /// If reset is true or pod doesn't exist:
    /// - Deletes existing pod (if any)
    /// - Deploys new pod
    /// - Enables SSH
    /// - Verifies SSH connection
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Referenced ConfigMaps do not exist
    /// - Referenced Secrets do not exist
    /// - Pod creation fails
    /// - SSH enablement fails
    pub fn deploy(self) -> Result<Self> {
        // Validate that all referenced ConfigMaps exist
        eprintln!("[DevelopmentMachine] Validating referenced ConfigMaps and Secrets...");
        self.validate_configmaps()?;
        self.validate_secrets()?;
        eprintln!("[DevelopmentMachine] Validation passed!");

        // Check if pod exists
        eprintln!("[DevelopmentMachine] Checking if pod {} exists in namespace {}", self.name, self.namespace);
        let exists = self.exists()?;
        eprintln!("[DevelopmentMachine] Pod exists: {}", exists);

        let machine = self;
        
        if exists && !machine.reset {
            // Pod exists and we're not resetting, use it
            eprintln!("[DevelopmentMachine] Pod already exists, waiting for it to be ready...");
            machine.wait_for_pod()?;
            eprintln!("[DevelopmentMachine] Pod is ready!");
        } else {
            // Either pod doesn't exist or we're resetting
            if exists {
                eprintln!("[DevelopmentMachine] Deleting existing pod for reset...");
                machine.delete_pod()?;
                eprintln!("[DevelopmentMachine] Pod deleted");
            }
            eprintln!("[DevelopmentMachine] Creating new pod...");
            machine.create_pod()?;
            eprintln!("[DevelopmentMachine] Pod created, waiting for it to be ready...");
            machine.wait_for_pod()?;
            eprintln!("[DevelopmentMachine] Pod is ready!");
        }

        // Enable SSH on the pod
        eprintln!("[DevelopmentMachine] Enabling SSH on pod...");
        let kubessh = KubeSsh::new(&machine.namespace, &machine.name)
            .with_secret_name(machine.secret_name.clone());

        kubessh.enable_ssh()?;
        eprintln!("[DevelopmentMachine] SSH enabled!");

        // Verify SSH connection works
        eprintln!("[DevelopmentMachine] Verifying SSH connection...");
        machine.verify_ssh_connection()?;
        eprintln!("[DevelopmentMachine] SSH verification successful!");

        eprintln!("[DevelopmentMachine] ✅ Pod {} deployed successfully!", machine.name);
        Ok(machine)
    }

    /// Creates the deployment manifest and deploys the pod.
    fn create_pod(&self) -> Result<()> {
        let manifest = self.generate_manifest()?;

        let cmd = "kubectl apply -f -";
        let mut child = std::process::Command::new("sh")
            .arg("-c")
            .arg(cmd)
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .spawn()?;

        {
            use std::io::Write;
            let stdin = child.stdin.as_mut().ok_or_else(|| {
                anyhow::anyhow!("Failed to open stdin for kubectl apply")
            })?;
            stdin.write_all(manifest.as_bytes())?;
        }

        let output = child.wait_with_output()?;

        if !output.status.success() {
            let error = String::from_utf8_lossy(&output.stderr);
            bail!("Failed to create pod: {}", error);
        }

        Ok(())
    }

    /// Generates the Kubernetes pod manifest.
    fn generate_manifest(&self) -> Result<String> {
        // Start with metadata section
        let mut manifest = format!(
            r#"apiVersion: v1
kind: Pod
metadata:
  name: {}
  namespace: {}
"#,
            self.name,
            self.namespace
        );

        // Add labels to metadata (not to spec)
        if !self.labels.is_empty() {
            manifest.push_str("  labels:\n");
            for (key, value) in &self.labels {
                manifest.push_str(&format!("    {}: {}\n", key, value));
            }
        }

        // Add spec section
        manifest.push_str(&format!(
            r#"spec:
  restartPolicy: {}
  containers:
  - name: {}
    image: {}
    resources:
      requests:
        cpu: {}m
        memory: {}Mi
      limits:
        cpu: {}m
        memory: {}Mi
    stdin: true
    stdinOnce: false
    tty: true
    command: ["/bin/sh", "-c", "sleep infinity"]
"#,
            self.restart_policy,
            self.name,
            self.image,
            self.cpu_request,
            self.memory_request,
            self.cpu_limit,
            self.memory_limit
        ));

        // Add environment variables
        if !self.env_vars.is_empty() || !self.env_from_configmap.is_empty() || !self.env_from_secret.is_empty() {
            manifest.push_str("    env:\n");
            
            for (key, value) in &self.env_vars {
                manifest.push_str(&format!("    - name: {}\n      value: \"{}\"\n", key, value));
            }
            
            for cm in &self.env_from_configmap {
                manifest.push_str(&format!("    - name: {}\n      valueFrom:\n        configMapKeyRef:\n          name: {}\n          key: value\n", cm, cm));
            }
            
            for secret in &self.env_from_secret {
                manifest.push_str(&format!("    - name: {}\n      valueFrom:\n        secretKeyRef:\n          name: {}\n          key: value\n", secret, secret));
            }
        }

        // Add ports
        if !self.ports.is_empty() {
            manifest.push_str("    ports:\n");
            for port in &self.ports {
                manifest.push_str(&format!("    - containerPort: {}\n", port));
            }
        }

        // Add readiness probe
        if let Some((path, port)) = &self.readiness_http {
            manifest.push_str(&format!(
                "    readinessProbe:\n      httpGet:\n        path: {}\n        port: {}\n",
                path, port
            ));
        } else if let Some(port) = self.readiness_tcp {
            manifest.push_str(&format!(
                "    readinessProbe:\n      tcpSocket:\n        port: {}\n",
                port
            ));
        }

        // Add liveness probe
        if let Some((path, port, initial_delay, period, failure_threshold)) = &self.liveness_http {
            manifest.push_str(&format!(
                "    livenessProbe:\n      httpGet:\n        path: {}\n        port: {}\n      initialDelaySeconds: {}\n      periodSeconds: {}\n      failureThreshold: {}\n",
                path, port, initial_delay, period, failure_threshold
            ));
        } else if let Some((port, initial_delay, period, failure_threshold)) = self.liveness_tcp {
            manifest.push_str(&format!(
                "    livenessProbe:\n      tcpSocket:\n        port: {}\n      initialDelaySeconds: {}\n      periodSeconds: {}\n      failureThreshold: {}\n",
                port, initial_delay, period, failure_threshold
            ));
        }

        Ok(manifest)
    }

    /// Waits for the pod to be running (max 60 seconds).
    fn wait_for_pod(&self) -> Result<()> {
        let start = std::time::Instant::now();
        let timeout = Duration::from_secs(60);
        let mut check_count = 0;

        loop {
            check_count += 1;
            if check_count % 6 == 1 {  // Log every 3 seconds (6 * 500ms)
                eprintln!("[DevelopmentMachine] Waiting for pod to be running... ({:.1}s elapsed)", 
                    start.elapsed().as_secs_f64());
            }
            
            if self.is_running()? {
                eprintln!("[DevelopmentMachine] Pod is now running!");
                return Ok(());
            }

            if start.elapsed() > timeout {
                bail!(
                    "Timeout waiting for pod {} to be running (waited {:.1}s)",
                    self.name,
                    start.elapsed().as_secs_f64()
                );
            }

            std::thread::sleep(Duration::from_millis(500));
        }
    }

    /// Deletes the pod.
    fn delete_pod(&self) -> Result<()> {
        let cmd = format!(
            "kubectl delete pod {} -n {} --ignore-not-found",
            self.name, self.namespace
        );

        let output = Command::new("sh")
            .arg("-c")
            .arg(&cmd)
            .output()?;

        if !output.status.success() {
            let error = String::from_utf8_lossy(&output.stderr);
            bail!("Failed to delete pod: {}", error);
        }

        // Wait for pod to be deleted
        std::thread::sleep(Duration::from_secs(2));

        Ok(())
    }

    /// Validates that all referenced ConfigMaps exist in the namespace.
    ///
    /// # Errors
    ///
    /// Returns an error listing all missing ConfigMaps if any are not found.
    fn validate_configmaps(&self) -> Result<()> {
        let mut missing = Vec::new();

        for configmap in &self.env_from_configmap {
            let cmd = format!(
                "kubectl get configmap {} -n {} --ignore-not-found -o name",
                configmap, self.namespace
            );

            let output = Command::new("sh")
                .arg("-c")
                .arg(&cmd)
                .output()?;

            if output.stdout.is_empty() {
                missing.push(configmap.clone());
            }
        }

        if !missing.is_empty() {
            bail!(
                "ConfigMap(s) not found in namespace '{}': {}. Create them before deploying.",
                self.namespace,
                missing.join(", ")
            );
        }

        Ok(())
    }

    /// Validates that all referenced Secrets exist in the namespace.
    ///
    /// # Errors
    ///
    /// Returns an error listing all missing Secrets if any are not found.
    fn validate_secrets(&self) -> Result<()> {
        let mut missing = Vec::new();

        for secret in &self.env_from_secret {
            let cmd = format!(
                "kubectl get secret {} -n {} --ignore-not-found -o name",
                secret, self.namespace
            );

            let output = Command::new("sh")
                .arg("-c")
                .arg(&cmd)
                .output()?;

            if output.stdout.is_empty() {
                missing.push(secret.clone());
            }
        }

        if !missing.is_empty() {
            bail!(
                "Secret(s) not found in namespace '{}': {}. Create them before deploying.",
                self.namespace,
                missing.join(", ")
            );
        }

        Ok(())
    }

    /// Verifies SSH connection to the pod.
    fn verify_ssh_connection(&self) -> Result<()> {
        // Wait a bit for SSH to be fully configured
        std::thread::sleep(Duration::from_secs(2));

        let check_cmd = format!(
            "kubectl exec -n {} {} -- which sshd",
            self.namespace, self.name
        );

        let output = Command::new("sh")
            .arg("-c")
            .arg(&check_cmd)
            .output()?;

        if !output.status.success() {
            bail!("SSH server not responding in pod {}", self.name);
        }

        Ok(())
    }

    /// Gets the SSH private key from the secret.
    pub fn get_ssh_key(&self) -> Result<String> {
        let cmd = format!(
            "kubectl get secret {} -n {} -o jsonpath='{{.data.develsshkey}}' | base64 -d",
            self.secret_name, self.namespace
        );

        let output = Command::new("sh")
            .arg("-c")
            .arg(&cmd)
            .output()?;

        if !output.status.success() {
            bail!("Failed to retrieve SSH key from secret");
        }

        Ok(String::from_utf8(output.stdout)?)
    }

    /// Gets the SSH public key from the secret.
    pub fn get_ssh_public_key(&self) -> Result<String> {
        let cmd = format!(
            "kubectl get secret {} -n {} -o jsonpath='{{.data.develsshkey.pub}}' | base64 -d",
            self.secret_name, self.namespace
        );

        let output = Command::new("sh")
            .arg("-c")
            .arg(&cmd)
            .output()?;

        if !output.status.success() {
            bail!("Failed to retrieve SSH public key from secret");
        }

        Ok(String::from_utf8(output.stdout)?)
    }

    /// Gets the pod name.
    pub fn get_name(&self) -> &str {
        &self.name
    }

    /// Gets the namespace.
    pub fn get_namespace(&self) -> &str {
        &self.namespace
    }

    /// Gets the image.
    pub fn get_image(&self) -> &str {
        &self.image
    }

    /// Gets the CPU request in millicores.
    pub fn get_cpu_request(&self) -> u32 {
        self.cpu_request
    }

    /// Gets the CPU limit in millicores.
    pub fn get_cpu_limit(&self) -> u32 {
        self.cpu_limit
    }

    /// Gets the memory request in MB.
    pub fn get_memory_request(&self) -> u32 {
        self.memory_request
    }

    /// Gets the memory limit in MB.
    pub fn get_memory_limit(&self) -> u32 {
        self.memory_limit
    }

    /// Gets the secret name.
    pub fn get_secret_name(&self) -> &str {
        &self.secret_name
    }

    /// Deletes the development machine.
    pub fn delete(&self) -> Result<()> {
        self.delete_pod()?;

        // Also delete the secret
        let cmd = format!(
            "kubectl delete secret {} -n {} --ignore-not-found",
            self.secret_name, self.namespace
        );

        Command::new("sh")
            .arg("-c")
            .arg(&cmd)
            .output()?;

        Ok(())
    }

    /// Gets detailed status information.
    pub fn status(&self) -> Result<String> {
        let cmd = format!(
            "kubectl describe pod {} -n {}",
            self.name, self.namespace
        );

        let output = Command::new("sh")
            .arg("-c")
            .arg(&cmd)
            .output()?;

        if !output.status.success() {
            bail!("Failed to get pod status");
        }

        Ok(String::from_utf8(output.stdout)?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_development_machine_creation() {
        let dev = DevelopmentMachine::new("test-dev");
        assert_eq!(dev.get_name(), "test-dev");
        assert_eq!(dev.get_namespace(), "development");
        assert_eq!(dev.get_image(), "ubuntu:22.04");
        assert_eq!(dev.get_secret_name(), "test-dev-ssh-keys");
    }

    #[test]
    fn test_development_machine_full_builder() {
        let dev = DevelopmentMachine::new("mydev")
            .namespace("custom-ns")
            .image("ubuntu:20.04")
            .cpu(1000, 2000)
            .memory(2048, 4096)
            .env("ENV", "development")
            .label("role", "developer")
            .port(8080)
            .restart("always");

        assert_eq!(dev.get_name(), "mydev");
        assert_eq!(dev.get_namespace(), "custom-ns");
        assert_eq!(dev.get_image(), "ubuntu:20.04");
        assert_eq!(dev.get_cpu_request(), 1000);
        assert_eq!(dev.get_cpu_limit(), 2000);
        assert_eq!(dev.get_memory_request(), 2048);
        assert_eq!(dev.get_memory_limit(), 4096);
    }
}
