use anyhow::{Result, bail};
use chrono::{DateTime, Utc};
use mithril_client::models::bid_model::Status as BidModelStatus;
use mithril_client::models::instance_model::Status as InstanceStatus;
use mithril_client::models::kubernetes_cluster_model::Status as K8sClusterStatus;
use mithril_client::models::{BidModel, KubernetesClusterModel};
use std::io::Write;
use std::time::{Duration, Instant};
use tokio::signal::unix::{SignalKind, signal};

use crate::api;

pub mod ssh_keys;
pub use ssh_keys::find_matching_ssh_key;

#[macro_export]
macro_rules! timed_api_call {
    ($name:expr, $call:expr) => {{
        let start = std::time::Instant::now();
        let result = $call.await.map_err(|e| anyhow::anyhow!("{:?}", e))?;
        log::debug!("{} took {:.2?}", $name, start.elapsed());
        result
    }};
}

pub fn cleanup_terminal() {
    print!("\x1b[?25h"); // Show cursor
    print!("\x1b[0m"); // Reset colors/attributes
    std::io::Write::flush(&mut std::io::stdout()).ok();
}

pub fn setup_signal_handlers() {
    tokio::spawn(async {
        let mut sigint = signal(SignalKind::interrupt()).expect("Failed to setup SIGINT handler");
        let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM handler");
        let mut sighup = signal(SignalKind::hangup()).expect("Failed to setup SIGHUP handler");

        tokio::select! {
            _ = sigint.recv() => {
                cleanup_terminal();
                std::process::exit(130);  // 128 + SIGINT(2)
            }
            _ = sigterm.recv() => {
                cleanup_terminal();
                std::process::exit(143);  // 128 + SIGTERM(15)
            }
            _ = sighup.recv() => {
                cleanup_terminal();
                std::process::exit(129);  // 128 + SIGHUP(1)
            }
        }
    });
}

pub fn calculate_age(created_at: &str) -> String {
    let parsed = DateTime::parse_from_rfc3339(created_at);

    if let Ok(created) = parsed {
        let now = Utc::now();
        let duration = now.signed_duration_since(created);

        let days = duration.num_days();
        let hours = duration.num_hours() % 24;
        let minutes = duration.num_minutes() % 60;

        if days > 0 {
            format!("{days}d {hours}h ago")
        } else if hours > 0 {
            format!("{hours}h {minutes}m ago")
        } else {
            format!("{minutes}m ago")
        }
    } else {
        "unknown age".to_string()
    }
}

/// Extension trait for BidModelStatus to provide Display-like functionality
/// (since mithril-client is auto-generated and we can't modify it)
pub trait BidStatusExt {
    fn as_str(&self) -> &'static str;
    fn is_active(&self) -> bool;
    fn priority(&self) -> u8;
    fn colorize(&self) -> String;
}

impl BidStatusExt for BidModelStatus {
    fn as_str(&self) -> &'static str {
        match self {
            BidModelStatus::Open => "Open",
            BidModelStatus::Allocated => "Allocated",
            BidModelStatus::Preempting => "Preempting",
            BidModelStatus::Terminated => "Terminated",
            BidModelStatus::Paused => "Paused",
        }
    }

    fn is_active(&self) -> bool {
        !matches!(self, BidModelStatus::Terminated)
    }

    /// Sort priority: Allocated (best) -> Open -> Preempting -> Paused -> Terminated
    fn priority(&self) -> u8 {
        match self {
            BidModelStatus::Allocated => 0,
            BidModelStatus::Open => 1,
            BidModelStatus::Preempting => 2,
            BidModelStatus::Paused => 3,
            BidModelStatus::Terminated => 4,
        }
    }

    fn colorize(&self) -> String {
        let s = self.as_str();
        match self {
            BidModelStatus::Allocated => format!("\x1b[32m{s}\x1b[0m"), // Green
            BidModelStatus::Open | BidModelStatus::Preempting => {
                format!("\x1b[33m{s}\x1b[0m") // Yellow
            }
            BidModelStatus::Paused => format!("\x1b[35m{s}\x1b[0m"), // Magenta
            BidModelStatus::Terminated => format!("\x1b[31m{s}\x1b[0m"), // Red
        }
    }
}

/// Sort bids by status priority (active first), then by creation date (newest first)
pub fn sort_bids_by_priority(bids: &mut [BidModel]) {
    bids.sort_by(|a, b| {
        a.status
            .priority()
            .cmp(&b.status.priority())
            .then_with(|| b.created_at.cmp(&a.created_at))
    });
}

/// Extension trait for K8sClusterStatus
pub trait K8sClusterStatusExt {
    fn as_str(&self) -> &'static str;
    fn priority(&self) -> u8;
    fn colorize(&self) -> String;
}

impl K8sClusterStatusExt for K8sClusterStatus {
    fn as_str(&self) -> &'static str {
        match self {
            K8sClusterStatus::Available => "Available",
            K8sClusterStatus::Pending => "Pending",
            K8sClusterStatus::Terminated => "Terminated",
        }
    }

    fn priority(&self) -> u8 {
        match self {
            K8sClusterStatus::Available => 0,
            K8sClusterStatus::Pending => 1,
            K8sClusterStatus::Terminated => 2,
        }
    }

    fn colorize(&self) -> String {
        let s = self.as_str();
        match self {
            K8sClusterStatus::Available => format!("\x1b[32m{s}\x1b[0m"), // Green
            K8sClusterStatus::Pending => format!("\x1b[33m{s}\x1b[0m"),   // Yellow
            K8sClusterStatus::Terminated => format!("\x1b[31m{s}\x1b[0m"), // Red
        }
    }
}

/// Sort k8s clusters by status priority (active first), then by creation date (newest first)
pub fn sort_k8s_clusters_by_priority(clusters: &mut [KubernetesClusterModel]) {
    clusters.sort_by(|a, b| {
        a.status
            .priority()
            .cmp(&b.status.priority())
            .then_with(|| b.created_at.cmp(&a.created_at))
    });
}

/// Extension trait for InstanceStatus
pub trait InstanceStatusExt {
    fn is_active(&self) -> bool;
}

impl InstanceStatusExt for InstanceStatus {
    fn is_active(&self) -> bool {
        matches!(
            self,
            InstanceStatus::StatusNew
                | InstanceStatus::StatusConfirmed
                | InstanceStatus::StatusScheduled
                | InstanceStatus::StatusInitializing
                | InstanceStatus::StatusStarting
                | InstanceStatus::StatusRunning
        )
    }
}

const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];

pub fn format_price_per_hour(price: &str) -> String {
    format!("{price}/hr")
}

pub fn format_optional_price_per_hour(price: &Option<String>) -> String {
    price
        .as_ref()
        .map(|p| format_price_per_hour(p))
        .unwrap_or_else(|| "-".to_string())
}

/// Wait for SSH to become available on the given destination.
/// Returns Ok(()) when SSH is ready, or error on timeout.
pub async fn wait_for_ssh_ready(ssh_destination: &str) -> Result<()> {
    let poll_interval = Duration::from_millis(2000);
    let timeout = Duration::from_secs(120); // 2 minutes for SSH to come up
    let start = Instant::now();
    let mut frame_idx = 0;

    print!("Waiting for SSH to become available... (press Ctrl+C to cancel)");
    std::io::stdout().flush().ok();

    loop {
        if start.elapsed() > timeout {
            println!();
            bail!("Timeout waiting for SSH to become available (2 minutes)");
        }

        // Try SSH with a short connection timeout
        let result = tokio::process::Command::new("ssh")
            .args([
                "-o",
                "ConnectTimeout=5",
                "-o",
                "StrictHostKeyChecking=no",
                "-o",
                "BatchMode=yes",
                &format!("ubuntu@{ssh_destination}"),
                "true",
            ])
            .output()
            .await;

        if let Ok(output) = result
            && output.status.success()
        {
            println!("\r\x1b[KSSH is ready!");
            return Ok(());
        }

        let spinner = SPINNER_FRAMES[frame_idx % SPINNER_FRAMES.len()];
        frame_idx += 1;

        print!(
            "\r\x1b[K{} [{}] Waiting for SSH on {}...",
            spinner,
            format_elapsed(start.elapsed()),
            ssh_destination
        );
        std::io::stdout().flush().ok();

        tokio::time::sleep(poll_interval).await;
    }
}

fn format_elapsed(duration: Duration) -> String {
    let secs = duration.as_secs();
    let mins = secs / 60;
    let secs = secs % 60;
    format!("{mins:02}:{secs:02}")
}

/// Result of waiting for instances
pub enum WaitResult {
    /// All instances are running, ready to use
    Ready,
    /// Bid was terminated
    Terminated,
}

/// Wait for a bid's instances to become available with SSH destinations.
/// Shows a spinner and status updates. Returns when instances are ready or bid terminates.
pub async fn wait_for_bid_instances(
    client: &api::Client,
    bid_fid: &str,
    bid_name: &str,
    expected_instances: Option<i32>,
) -> Result<WaitResult> {
    println!("Waiting for instance(s) to start... (press Ctrl+C to cancel)");

    let poll_interval = Duration::from_millis(2000);
    let timeout = Duration::from_secs(600); // 10 minutes
    let start = Instant::now();
    let mut frame_idx = 0;

    loop {
        if start.elapsed() > timeout {
            println!();
            bail!("Timeout waiting for instances to start (10 minutes)");
        }

        let bid = client.get_bid(bid_fid).await?;
        let status = &bid.status;
        let expected = expected_instances.unwrap_or(bid.instance_quantity);

        // Fetch actual instance details to check SSH destinations
        let instances = client.fetch_instances_by_bid(&bid.project, bid_fid).await?;

        // Count instances that have SSH destinations (actually ready)
        let ready_count = instances
            .iter()
            .filter(|i| i.ssh_destination.as_ref().is_some_and(|s| !s.is_empty()))
            .count();
        let total_count = instances.len();

        let spinner = SPINNER_FRAMES[frame_idx % SPINNER_FRAMES.len()];
        frame_idx += 1;

        let status_detail = if total_count > ready_count {
            format!(
                "{} | Instances: {}/{} ({} provisioning)",
                status.as_str(),
                ready_count,
                expected,
                total_count - ready_count
            )
        } else {
            format!(
                "{} | Instances: {}/{}",
                status.as_str(),
                ready_count,
                expected
            )
        };

        print!(
            "\r\x1b[K{} [{}] {}",
            spinner,
            format_elapsed(start.elapsed()),
            status_detail
        );
        std::io::stdout().flush().ok();

        match status {
            BidModelStatus::Terminated => {
                println!();
                return Ok(WaitResult::Terminated);
            }
            BidModelStatus::Allocated => {
                if ready_count as i32 >= expected {
                    println!();
                    println!();
                    println!("All {ready_count} instance(s) are now running!");
                    println!("Use 'mcli ssh {bid_name}' to connect.");
                    return Ok(WaitResult::Ready);
                }
            }
            _ => {}
        }

        tokio::time::sleep(poll_interval).await;
    }
}
