use anyhow::{Context, Result, bail};
use dialoguer::{Confirm, Select};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process::Command;

use crate::api;
use crate::util::{K8sClusterStatusExt, find_matching_ssh_key, sort_k8s_clusters_by_priority};
use mithril_client::models::KubernetesClusterModel;
use mithril_client::models::kubernetes_cluster_model::Status as K8sClusterStatus;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct KubeConfig {
    #[serde(rename = "apiVersion", default)]
    api_version: String,
    #[serde(default)]
    kind: String,
    #[serde(default)]
    clusters: Vec<ClusterEntry>,
    #[serde(default)]
    contexts: Vec<ContextEntry>,
    #[serde(default)]
    users: Vec<UserEntry>,
    #[serde(rename = "current-context", default)]
    current_context: String,
    #[serde(flatten)]
    extra: BTreeMap<String, serde_yaml::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct ClusterEntry {
    name: String,
    cluster: ClusterData,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct ClusterData {
    server: String,
    #[serde(
        rename = "certificate-authority-data",
        skip_serializing_if = "Option::is_none"
    )]
    certificate_authority_data: Option<String>,
    #[serde(flatten)]
    extra: BTreeMap<String, serde_yaml::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct ContextEntry {
    name: String,
    context: ContextData,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct ContextData {
    cluster: String,
    user: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    namespace: Option<String>,
    #[serde(flatten)]
    extra: BTreeMap<String, serde_yaml::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct UserEntry {
    name: String,
    user: serde_yaml::Value,
}

pub async fn run(
    cluster_name: Option<String>,
    identity: Option<String>,
    yes: bool,
    no_backup: bool,
    skip_validation: bool,
) -> Result<()> {
    let client = api::Client::load()?;
    let project = client.resolve_project(Some(&client.project_id)).await?;

    let cluster = if let Some(name_or_fid) = cluster_name {
        client
            .resolve_k8s_cluster(&project.fid, &name_or_fid)
            .await?
    } else {
        select_cluster_interactive(&client, &project.fid).await?
    };

    let kube_host = cluster
        .kube_host
        .as_ref()
        .filter(|h| !h.is_empty())
        .ok_or_else(|| {
            anyhow::anyhow!(
                "Cluster '{}' does not have a kube_host yet. It may still be provisioning.",
                cluster.name
            )
        })?;

    let context_name = format!("mithril:{}", cluster.name);
    let local_kubeconfig_path = get_kubeconfig_path()?;

    // Determine identity file
    let identity_path = if let Some(ref path) = identity {
        Some(PathBuf::from(path))
    } else {
        find_ssh_key(&client, &project.fid, &cluster.ssh_keys).await?
    };

    println!(
        "Fetching kubeconfig from cluster '{}' ({})...",
        cluster.name, kube_host
    );

    // Fetch remote kubeconfig
    let remote_config = fetch_remote_kubeconfig(kube_host, identity_path.as_ref())?;

    // Rewrite kubeconfig
    let rewritten_config = rewrite_kubeconfig(remote_config, &cluster.name, kube_host)?;

    // Check for conflicts with existing config
    let local_config = load_local_kubeconfig(&local_kubeconfig_path)?;
    if let Some((old_entries, new_entries)) =
        check_kubeconfig_conflict(&local_config, &rewritten_config, &context_name)
    {
        println!("\nExisting kubeconfig entries for '{context_name}' will be replaced:");
        show_kubeconfig_diff(&old_entries, &new_entries);

        if !yes {
            let confirmed = Confirm::new()
                .with_prompt("Do you want to continue?")
                .default(true)
                .interact()
                .context("Failed to get user confirmation")?;

            if !confirmed {
                println!("Aborted.");
                return Ok(());
            }
        }
    }

    // Create backup
    if local_kubeconfig_path.exists() && !no_backup {
        let backup_path = create_backup(&local_kubeconfig_path)?;
        println!("Created backup: {}", backup_path.display());
    }

    // Merge and save
    let merged_config =
        merge_kubeconfigs(&local_kubeconfig_path, &rewritten_config, &context_name)?;
    save_kubeconfig(&local_kubeconfig_path, &merged_config)?;
    println!("Updated kubeconfig at {}", local_kubeconfig_path.display());

    // Validate
    if !skip_validation {
        println!("Validating kubeconfig...");
        match validate_kubeconfig(&context_name) {
            Ok(()) => println!("Kubeconfig validated successfully."),
            Err(e) => {
                eprintln!("Warning: Kubeconfig validation failed: {e}");
                eprintln!(
                    "The kubeconfig was saved, but you may need to check your cluster connectivity."
                );
            }
        }
    }

    println!("\nTo use this context:");
    println!("  kubectl config use-context {context_name}");

    Ok(())
}

fn get_kubeconfig_path() -> Result<PathBuf> {
    if let Ok(path) = std::env::var("KUBECONFIG") {
        // Take the first path if multiple are specified
        let first_path = path.split(':').next().unwrap_or(&path);
        return Ok(PathBuf::from(first_path));
    }

    let home = std::env::var("HOME").context("HOME environment variable not set")?;
    Ok(PathBuf::from(home).join(".kube").join("config"))
}

async fn select_cluster_interactive(
    client: &api::Client,
    project_fid: &str,
) -> Result<KubernetesClusterModel> {
    println!("Fetching clusters...");

    let clusters = client.fetch_k8s_clusters(project_fid).await?;

    if clusters.is_empty() {
        bail!("No clusters found");
    }

    let mut active_clusters: Vec<_> = clusters
        .into_iter()
        .filter(|c| c.status != K8sClusterStatus::Terminated)
        .collect();

    if active_clusters.is_empty() {
        bail!("No active clusters found");
    }

    sort_k8s_clusters_by_priority(&mut active_clusters);

    let selection = Select::new()
        .with_prompt("Select cluster")
        .items(
            &active_clusters
                .iter()
                .map(|c| {
                    format!(
                        "{} | {} | {} | {}",
                        c.name,
                        c.region,
                        c.status.as_str(),
                        c.kube_host.as_deref().unwrap_or("-")
                    )
                })
                .collect::<Vec<_>>(),
        )
        .default(0)
        .interact()
        .context("Failed to get user selection")?;

    Ok(active_clusters.into_iter().nth(selection).unwrap())
}

async fn find_ssh_key(
    client: &api::Client,
    project_fid: &str,
    cluster_ssh_key_fids: &[String],
) -> Result<Option<PathBuf>> {
    if cluster_ssh_key_fids.is_empty() {
        return Ok(None);
    }

    let ssh_keys = client.fetch_ssh_keys(project_fid).await?;

    let authorized_keys: Vec<_> = ssh_keys
        .into_iter()
        .filter(|k| cluster_ssh_key_fids.contains(&k.fid))
        .collect();

    if authorized_keys.is_empty() {
        return Ok(None);
    }

    Ok(find_matching_ssh_key(&authorized_keys).map(|(_, key_path)| key_path))
}

fn fetch_remote_kubeconfig(kube_host: &str, identity: Option<&PathBuf>) -> Result<KubeConfig> {
    let ssh_target = format!("ubuntu@{kube_host}");

    let mut cmd = Command::new("ssh");
    if let Some(key_path) = identity {
        cmd.arg("-i").arg(key_path);
    }
    cmd.arg("-o").arg("StrictHostKeyChecking=accept-new");
    cmd.arg("-o").arg("ConnectTimeout=10");
    cmd.arg(&ssh_target);
    cmd.arg("cat").arg("~/.kube/config");

    let output = cmd.output().context("Failed to execute SSH command")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to fetch kubeconfig via SSH: {stderr}");
    }

    let config: KubeConfig = serde_yaml::from_slice(&output.stdout)
        .context("Failed to parse remote kubeconfig as YAML")?;

    Ok(config)
}

fn replace_server_url(url: &str, new_host: &str) -> String {
    // Parse URL like https://127.0.0.1:6443
    if let Some(scheme_end) = url.find("://") {
        let scheme = &url[..scheme_end];
        let rest = &url[scheme_end + 3..];

        // Find port if present
        if let Some(port_start) = rest.rfind(':') {
            let port = &rest[port_start..];
            return format!("{scheme}://{new_host}{port}");
        } else {
            return format!("{scheme}://{new_host}");
        }
    }
    url.to_string()
}

fn rewrite_kubeconfig(
    mut config: KubeConfig,
    cluster_name: &str,
    kube_host: &str,
) -> Result<KubeConfig> {
    let context_name = format!("mithril:{cluster_name}");

    // Rewrite clusters
    for cluster_entry in &mut config.clusters {
        cluster_entry.cluster.server = replace_server_url(&cluster_entry.cluster.server, kube_host);
        cluster_entry.name = context_name.clone();
    }

    // Rewrite contexts
    for context_entry in &mut config.contexts {
        context_entry.name = context_name.clone();
        context_entry.context.cluster = context_name.clone();
        context_entry.context.user = context_name.clone();
    }

    // Rewrite users
    for user_entry in &mut config.users {
        user_entry.name = context_name.clone();
    }

    config.current_context = context_name;

    Ok(config)
}

fn load_local_kubeconfig(path: &PathBuf) -> Result<Option<KubeConfig>> {
    if !path.exists() {
        return Ok(None);
    }

    let contents = std::fs::read_to_string(path).context("Failed to read local kubeconfig")?;

    let config: KubeConfig =
        serde_yaml::from_str(&contents).context("Failed to parse local kubeconfig")?;

    Ok(Some(config))
}

fn check_kubeconfig_conflict(
    local_config: &Option<KubeConfig>,
    new_config: &KubeConfig,
    context_name: &str,
) -> Option<(OldEntries, NewEntries)> {
    let local = local_config.as_ref()?;

    let old_cluster = local.clusters.iter().find(|c| c.name == context_name)?;
    let old_context = local.contexts.iter().find(|c| c.name == context_name);
    let old_user = local.users.iter().find(|u| u.name == context_name);

    let new_cluster = new_config
        .clusters
        .iter()
        .find(|c| c.name == context_name)?;
    let new_context = new_config.contexts.iter().find(|c| c.name == context_name);
    let new_user = new_config.users.iter().find(|u| u.name == context_name);

    // Check if anything differs
    if Some(old_cluster) != Some(new_cluster) || old_context != new_context || old_user != new_user
    {
        return Some((
            OldEntries {
                cluster: Some(old_cluster.clone()),
                context: old_context.cloned(),
                user: old_user.cloned(),
            },
            NewEntries {
                cluster: Some(new_cluster.clone()),
                context: new_context.cloned(),
                user: new_user.cloned(),
            },
        ));
    }

    None
}

struct OldEntries {
    cluster: Option<ClusterEntry>,
    context: Option<ContextEntry>,
    user: Option<UserEntry>,
}

struct NewEntries {
    cluster: Option<ClusterEntry>,
    context: Option<ContextEntry>,
    user: Option<UserEntry>,
}

fn show_kubeconfig_diff(old: &OldEntries, new: &NewEntries) {
    if let Some(ref cluster) = old.cluster {
        println!("  Old cluster server: {}", cluster.cluster.server);
    }
    if let Some(ref cluster) = new.cluster {
        println!("  New cluster server: {}", cluster.cluster.server);
    }
    if old.context.is_some() || new.context.is_some() {
        println!("  Context entries will be updated");
    }
    if old.user.is_some() || new.user.is_some() {
        println!("  User credentials will be updated");
    }
}

fn create_backup(path: &PathBuf) -> Result<PathBuf> {
    let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
    let backup_name = format!(
        "{}.backup.{}",
        path.file_name().unwrap_or_default().to_string_lossy(),
        timestamp
    );
    let backup_path = path.parent().unwrap_or(path).join(backup_name);

    std::fs::copy(path, &backup_path).context("Failed to create backup")?;

    Ok(backup_path)
}

#[allow(clippy::ptr_arg)]
fn merge_kubeconfigs(
    local_kubeconfig_path: &PathBuf,
    new_config: &KubeConfig,
    context_name: &str,
) -> Result<KubeConfig> {
    // Write new config to a temp file
    let temp_dir = std::env::temp_dir();
    let temp_path = temp_dir.join(format!("mcli-kubeconfig-{}.yaml", std::process::id()));
    let yaml = serde_yaml::to_string(new_config).context("Failed to serialize new kubeconfig")?;
    std::fs::write(&temp_path, &yaml).context("Failed to write temporary kubeconfig")?;

    // Use kubectl's native merge with KUBECONFIG env var
    // Leftmost file takes precedence, so put remote first
    let kubeconfig_env = if local_kubeconfig_path.exists() {
        format!(
            "{}:{}",
            temp_path.display(),
            local_kubeconfig_path.display()
        )
    } else {
        temp_path.display().to_string()
    };

    let output = Command::new("kubectl")
        .arg("config")
        .arg("view")
        .arg("--flatten")
        .env("KUBECONFIG", &kubeconfig_env)
        .output()
        .context("Failed to execute kubectl. Is kubectl installed?")?;

    // Clean up temp file
    let _ = std::fs::remove_file(&temp_path);

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("kubectl config view failed: {stderr}");
    }

    let mut merged: KubeConfig = serde_yaml::from_slice(&output.stdout)
        .context("Failed to parse merged kubeconfig from kubectl")?;

    // Set current context to the new context
    merged.current_context = context_name.to_string();

    Ok(merged)
}

fn save_kubeconfig(path: &PathBuf, config: &KubeConfig) -> Result<()> {
    // Ensure parent directory exists
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).context("Failed to create kubeconfig directory")?;
    }

    let yaml = serde_yaml::to_string(config).context("Failed to serialize kubeconfig")?;

    std::fs::write(path, yaml).context("Failed to write kubeconfig")?;

    Ok(())
}

fn validate_kubeconfig(context_name: &str) -> Result<()> {
    let output = Command::new("kubectl")
        .arg("version")
        .arg("--context")
        .arg(context_name)
        .arg("--request-timeout=5s")
        .output()
        .context("Failed to execute kubectl")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("kubectl validation failed: {stderr}");
    }

    Ok(())
}
