use anyhow::{Context, Result, bail};
use chrono::Utc;
use dialoguer::MultiSelect;
use mithril_client::models::NewSshKeyModel;

use crate::api;
use crate::util::{
    self, BidStatusExt, WaitResult, find_matching_ssh_key, format_optional_price_per_hour,
    format_price_per_hour,
};

/// Workaround: collect keys marked as required by the platform.
/// These must always be included when creating a bid.
/// TODO(required-keys): Remove when backend handles required keys automatically.
fn get_required_ssh_keys(keys: &[NewSshKeyModel]) -> Vec<&NewSshKeyModel> {
    keys.iter().filter(|k| k.required).collect()
}

fn select_ssh_keys_interactive(keys: &[NewSshKeyModel]) -> Result<Vec<&NewSshKeyModel>> {
    // TODO(required-keys): Required keys are always included, so exclude from selection
    let selectable_keys: Vec<_> = keys.iter().filter(|k| !k.required).collect();

    if selectable_keys.is_empty() {
        return Ok(vec![]);
    }

    let items: Vec<String> = selectable_keys
        .iter()
        .map(|k| {
            let key_type = k.public_key.split_whitespace().next().unwrap_or("unknown");
            format!("{} ({})", k.name, key_type)
        })
        .collect();

    let selections = MultiSelect::new()
        .with_prompt("Select SSH key(s) to authorize")
        .items(&items)
        .interact()
        .context("Failed to get user selection")?;

    if selections.is_empty() {
        bail!("At least one SSH key must be selected");
    }

    Ok(selections.into_iter().map(|i| selectable_keys[i]).collect())
}

#[allow(clippy::too_many_arguments)]
pub async fn run(
    instance_type: String,
    region: String,
    name: Option<String>,
    max_price_per_hour: f64,
    num_instances: i32,
    project: Option<String>,
    k8s: Option<String>,
    wait: bool,
    dry_run: bool,
    watch: bool,
    json: bool,
) -> Result<()> {
    let client = api::Client::load()?;

    // Resolve project (use config project_id as default)
    let project_filter = project.as_deref().or(Some(client.project_id.as_str()));
    let project = client.resolve_project(project_filter).await?;
    println!("Using project: {} ({})", project.name, project.fid);

    // Fetch SSH keys and try to match local default key
    let ssh_keys = client.fetch_ssh_keys(&project.fid).await?;
    if ssh_keys.is_empty() {
        bail!(
            "No SSH keys found for project '{}'. Please add SSH keys before creating instances.",
            project.name
        );
    }

    // TODO(required-keys): Always include required keys
    let required_keys = get_required_ssh_keys(&ssh_keys);
    let mut ssh_key_fids: Vec<String> = required_keys.iter().map(|k| k.fid.clone()).collect();
    if !required_keys.is_empty() {
        let names: Vec<_> = required_keys.iter().map(|k| k.name.as_str()).collect();
        println!("Including required SSH key(s): {}", names.join(", "));
    }

    // Try to match local key or prompt user for non-required keys
    if let Some((matched_key, _matched_private_key_path)) = find_matching_ssh_key(&ssh_keys) {
        if !ssh_key_fids.contains(&matched_key.fid) {
            println!("Using SSH key: {} (matched local key)", matched_key.name);
            ssh_key_fids.push(matched_key.fid.clone());
        }
    } else {
        let selected = select_ssh_keys_interactive(&ssh_keys)?;
        if !selected.is_empty() {
            println!("No local SSH key matched platform keys.");
            let names: Vec<_> = selected.iter().map(|k| k.name.as_str()).collect();
            println!("Using SSH key(s): {}", names.join(", "));
            for key in selected {
                if !ssh_key_fids.contains(&key.fid) {
                    ssh_key_fids.push(key.fid.clone());
                }
            }
        }
    }

    if ssh_key_fids.is_empty() {
        bail!("At least one SSH key must be selected");
    }

    // Resolve k8s cluster name to FID if provided
    let k8s_fid = if let Some(ref cluster_name) = k8s {
        let cluster = client
            .resolve_k8s_cluster(&project.fid, cluster_name)
            .await?;
        println!("Using K8s cluster: {} ({})", cluster.name, cluster.fid);
        Some(cluster.fid)
    } else {
        None
    };

    // Generate name if not provided
    let bid_name = name.unwrap_or_else(|| {
        let timestamp = Utc::now().format("%Y%m%d-%H%M%S");
        format!("mcli-{timestamp}")
    });

    // Validate and resolve instance type to FID
    println!();
    println!("Validating...");
    let (instance_type_fid, instance_info, auction) = client
        .validate_instance_region(&instance_type, &region)
        .await?;

    if dry_run {
        println!();
        println!("Dry run - would create bid:");
        println!("  Name:           {bid_name}");
        println!("  Project:        {} ({})", project.name, project.fid);
        println!(
            "  Instance Type:  {} ({})",
            instance_info.name, instance_type_fid
        );
        println!("  Region:         {region}");
        println!("  Max Price:      ${max_price_per_hour:.2}/hr");
        println!("  Num Instances:  {num_instances}");
        if let Some(ref cluster_fid) = k8s_fid {
            println!("  K8s Cluster:    {cluster_fid}");
        }

        println!();
        println!("Spot Market:");
        println!("  Capacity:   {} instances", auction.capacity);
        println!(
            "  Spot Price: {}",
            format_price_per_hour(&auction.last_instance_price)
        );
        let win_price = format_optional_price_per_hour(&auction.lowest_allocated_price);
        if win_price != "-" {
            println!("  Win Price:  {win_price}");
        }

        if let Ok(spot_price) = auction.last_instance_price.parse::<f64>()
            && max_price_per_hour < spot_price
        {
            println!();
            println!(
                "\x1b[33mWarning: Max price ${:.2}/hr is below spot price {}\x1b[0m",
                max_price_per_hour,
                format_price_per_hour(&auction.last_instance_price)
            );
        }

        println!();
        println!("Configuration valid. No bid created (dry run).");
        return Ok(());
    }

    // Build the request with resolved FID
    let request = api::build_create_bid_request(
        &project.fid,
        &region,
        &instance_type_fid,
        max_price_per_hour,
        num_instances,
        &bid_name,
        ssh_key_fids,
        k8s_fid,
    );

    println!();
    println!("Creating bid '{bid_name}'...");
    let bid = client.create_bid(request).await?;

    if json {
        let output = serde_json::json!({
            "bid": {
                "fid": bid.fid,
                "name": bid.name,
                "project": bid.project,
                "instance_type": bid.instance_type,
                "region": bid.region,
                "status": bid.status.as_str(),
                "limit_price": bid.limit_price,
                "instance_quantity": bid.instance_quantity,
                "created_at": bid.created_at,
            }
        });
        println!("{}", serde_json::to_string_pretty(&output)?);
        return Ok(());
    }

    println!();
    println!("Bid created successfully!");
    println!("  Name:   {}", bid.name);
    println!("  FID:    {}", bid.fid);
    println!("  Status: {}", bid.status.as_str());
    println!();

    if wait || watch {
        match util::wait_for_bid_instances(&client, &bid.fid, &bid.name, Some(num_instances))
            .await?
        {
            WaitResult::Ready => {}
            WaitResult::Terminated => bail!("Bid terminated - unable to start instances"),
        }
    } else {
        println!("Use 'mcli instance info {}' to check status.", bid.name);
        println!("Use 'mcli ssh {}' to connect once running.", bid.name);
    }

    Ok(())
}
