use anyhow::{Context, Result, bail};
use dialoguer::Select;
use mithril_client::apis::configuration::Configuration;
use mithril_client::apis::{
    instance_types_api, instances_api, kubernetes_clusters_api, profile_api, projects_api,
    spot_api, ssh_keys_api,
};
use mithril_client::models::{
    AuctionModel, BidModel, CreateBidRequest, InstanceModel, InstanceTypeModel,
    KubernetesClusterModel, LaunchSpecificationModel, NewSshKeyModel, ProjectModel,
    TeammateResponse,
};
use std::collections::HashMap;

use crate::config::load_config;
use crate::timed_api_call;
use crate::util::BidStatusExt;

/// API client for the Mithril platform
pub struct Client {
    http: Configuration,
    pub project_id: String,
}

impl Client {
    /// Load client configuration from env vars and config file
    /// Priority: env vars → config file → default
    pub fn load() -> Result<Self> {
        let config = load_config().map_err(|e| anyhow::anyhow!("{}", e))?;

        let reqwest_client = reqwest::Client::builder()
            .connection_verbose(true)
            .build()
            .context("Failed to build HTTP client")?;

        let http = Configuration {
            base_path: config.api_url,
            user_agent: Some("mcli/0.1.0".to_string()),
            client: reqwest_client,
            basic_auth: None,
            oauth_access_token: None,
            bearer_access_token: Some(config.api_key),
            api_key: None,
        };

        Ok(Self {
            http,
            project_id: config.project_id,
        })
    }

    pub async fn fetch_projects(&self) -> Result<Vec<ProjectModel>> {
        let projects = timed_api_call!(
            "GET /v2/projects",
            projects_api::get_projects_v2_projects_get(&self.http)
        );
        Ok(projects)
    }

    pub async fn fetch_teammates(&self) -> Result<Vec<TeammateResponse>> {
        let teammates = timed_api_call!(
            "GET /v2/me/teammates",
            profile_api::get_my_teammates_v2_me_teammates_get(&self.http)
        );
        Ok(teammates)
    }

    pub async fn fetch_instance_types(&self) -> Result<HashMap<String, InstanceTypeModel>> {
        let response = timed_api_call!(
            "GET /v2/instance-types",
            instance_types_api::get_instance_types_v2_instance_types_get(&self.http)
        );

        Ok(response
            .into_iter()
            .map(|it| (it.fid.clone(), it))
            .collect())
    }

    pub async fn fetch_spot_availability(&self) -> Result<Vec<AuctionModel>> {
        let auctions = timed_api_call!(
            "GET /v2/spot/availability",
            spot_api::get_auctions_v2_spot_availability_get(&self.http)
        );
        Ok(auctions)
    }

    /// Validate instance type and region, returning (FID, model, auction).
    pub async fn validate_instance_region(
        &self,
        user_instance_type: &str,
        region: &str,
    ) -> Result<(String, InstanceTypeModel, AuctionModel)> {
        let (instance_types, auctions) =
            tokio::try_join!(self.fetch_instance_types(), self.fetch_spot_availability(),)?;

        // Find all matching instance types
        let matches = find_matching_instance_types(user_instance_type, &instance_types);

        if matches.is_empty() {
            let mut valid_names: Vec<_> =
                instance_types.values().map(|it| it.name.as_str()).collect();
            valid_names.sort();
            valid_names.dedup();
            bail!(
                "Unknown instance type '{}'. Valid types: {}",
                user_instance_type,
                valid_names.join(", ")
            );
        }

        // Filter matches by availability in the specified region
        let matches_in_region: Vec<_> = matches
            .iter()
            .filter_map(|(fid, it)| {
                auctions
                    .iter()
                    .find(|a| &a.instance_type == *fid && a.region == region)
                    .map(|auction| ((*fid).clone(), (*it).clone(), auction.clone()))
            })
            .collect();

        match matches_in_region.len() {
            0 => {
                // No matches in the specified region - show available regions for matched types
                let match_fids: Vec<_> = matches.iter().map(|(fid, _)| fid.as_str()).collect();
                let available_regions: Vec<String> = auctions
                    .iter()
                    .filter(|a| match_fids.contains(&a.instance_type.as_str()))
                    .map(|a| a.region.clone())
                    .collect::<std::collections::HashSet<_>>()
                    .into_iter()
                    .collect();

                if available_regions.is_empty() {
                    bail!("Instance type '{user_instance_type}' is not available in any region");
                } else {
                    bail!(
                        "Instance type '{}' is not available in region '{}'. Available: {}",
                        user_instance_type,
                        region,
                        available_regions.join(", ")
                    );
                }
            }
            1 => {
                let (fid, it, auction) = matches_in_region.into_iter().next().unwrap();
                Ok((fid, it, auction))
            }
            _ => {
                // Multiple matches in region - show options
                let mut options: Vec<_> = matches_in_region
                    .iter()
                    .map(|(_, it, _)| it.name.as_str())
                    .collect();
                options.sort();
                options.dedup();
                bail!(
                    "Ambiguous instance type '{}'. Did you mean: {}?",
                    user_instance_type,
                    options.join(", ")
                )
            }
        }
    }

    pub async fn fetch_instances_for_project(
        &self,
        project_fid: &str,
    ) -> Result<Vec<(InstanceModel, String)>> {
        let mut all_instances = Vec::new();
        let mut next_cursor = None;

        loop {
            let response = timed_api_call!(
                &format!("GET /v2/instances?project={project_fid}"),
                instances_api::get_instances_v2_instances_get(
                    &self.http,
                    project_fid,
                    next_cursor.clone(),
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                )
            );

            all_instances.extend(response.data);

            if response.next_cursor.is_none() {
                break;
            }
            next_cursor = response.next_cursor.map(serde_json::Value::String);
        }

        Ok(all_instances
            .into_iter()
            .map(|inst| (inst, project_fid.to_string()))
            .collect())
    }

    pub async fn search_bids_by_name(&self, bid_name: &str) -> Result<Vec<BidModel>> {
        let mut all_bids = Vec::new();
        let mut next_cursor: Option<serde_json::Value> = None;

        loop {
            let response = timed_api_call!(
                &format!("GET /v2/spot/bids?name={bid_name}"),
                spot_api::get_bids_v2_spot_bids_get(
                    &self.http,
                    next_cursor.clone(),
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    Some(bid_name),
                    None,
                )
            );

            all_bids.extend(response.data);

            if response.next_cursor.is_none() {
                break;
            }
            next_cursor = response.next_cursor.map(serde_json::Value::String);
        }

        Ok(all_bids)
    }

    pub async fn find_instances_by_bid_name(
        &self,
        bid_name: &str,
    ) -> Result<(Vec<InstanceModel>, BidModel, String)> {
        println!("Looking for bid '{bid_name}'...");

        let all_matches = self.search_bids_by_name(bid_name).await?;

        if all_matches.is_empty() {
            bail!("Bid '{bid_name}' not found");
        }

        let bid = if all_matches.len() == 1 {
            all_matches.into_iter().next().unwrap()
        } else {
            println!("Found {} bids with name '{}':", all_matches.len(), bid_name);
            let selection = Select::new()
                .with_prompt("Select which bid to connect to")
                .items(
                    &all_matches
                        .iter()
                        .map(|bid| {
                            format!(
                                "{} (project: {}, region: {}, status: {})",
                                bid.name,
                                bid.project,
                                bid.region.as_deref().unwrap_or("unknown"),
                                bid.status.as_str()
                            )
                        })
                        .collect::<Vec<_>>(),
                )
                .default(0)
                .interact()
                .context("Failed to get user selection")?;

            all_matches.into_iter().nth(selection).unwrap()
        };

        let project_fid = bid.project.clone();

        println!(
            "Found bid '{}' (FID: {}) in project '{}'",
            bid.name, bid.fid, project_fid
        );
        println!("Fetching instances for this bid...");

        let mut all_instances = Vec::new();
        let mut next_cursor = None;

        loop {
            let response = timed_api_call!(
                &format!(
                    "GET /v2/instances?project={}&bid_fid_in={}",
                    project_fid, bid.fid
                ),
                instances_api::get_instances_v2_instances_get(
                    &self.http,
                    &project_fid,
                    next_cursor.clone(),
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    Some(vec![bid.fid.clone()]),
                    None,
                    None,
                )
            );

            all_instances.extend(response.data);

            if response.next_cursor.is_none() {
                break;
            }
            next_cursor = response.next_cursor.map(serde_json::Value::String);
        }

        if all_instances.is_empty() {
            bail!(
                "No instances found for bid '{}'. The bid may not have any instances yet.",
                bid.name
            );
        }

        all_instances.sort_by(|a, b| natord::compare(&a.name, &b.name));

        println!(
            "Found {} instance(s) for bid '{}'",
            all_instances.len(),
            bid.name
        );

        Ok((all_instances, bid, project_fid))
    }

    pub async fn fetch_bids(
        &self,
        project: Option<&str>,
        status: Option<&str>,
        limit: Option<i32>,
    ) -> Result<Vec<BidModel>> {
        let mut all_bids = Vec::new();
        let mut next_cursor: Option<serde_json::Value> = None;

        let query_desc = match (project, status) {
            (Some(p), Some(s)) => format!("GET /v2/spot/bids?project={p}&status={s}"),
            (Some(p), None) => format!("GET /v2/spot/bids?project={p}"),
            (None, Some(s)) => format!("GET /v2/spot/bids?status={s}"),
            (None, None) => "GET /v2/spot/bids".to_string(),
        };

        loop {
            let response = timed_api_call!(
                &query_desc,
                spot_api::get_bids_v2_spot_bids_get(
                    &self.http,
                    next_cursor.clone(),
                    None,
                    None,
                    project,
                    None,
                    None,
                    status,
                    None,
                    limit,
                )
            );

            all_bids.extend(response.data);

            if response.next_cursor.is_none() || limit.is_some() {
                break;
            }
            next_cursor = response.next_cursor.map(serde_json::Value::String);
        }

        Ok(all_bids)
    }

    pub async fn fetch_ssh_keys(&self, project: &str) -> Result<Vec<NewSshKeyModel>> {
        let keys = timed_api_call!(
            &format!("GET /v2/ssh-keys?project={project}"),
            ssh_keys_api::get_ssh_keys_v2_ssh_keys_get(&self.http, project)
        );
        Ok(keys)
    }

    pub async fn create_bid(&self, request: CreateBidRequest) -> Result<BidModel> {
        let bid = timed_api_call!(
            "POST /v2/spot/bids",
            spot_api::create_bid_v2_spot_bids_post(&self.http, request)
        );
        Ok(bid)
    }

    pub async fn cancel_bid(&self, bid_fid: &str) -> Result<()> {
        timed_api_call!(
            &format!("DELETE /v2/spot/bids/{bid_fid}"),
            spot_api::cancel_bid_v2_spot_bids_bid_fid_delete(&self.http, bid_fid)
        );
        Ok(())
    }

    pub async fn get_bid(&self, bid_fid: &str) -> Result<BidModel> {
        let bid = timed_api_call!(
            &format!("GET /v2/spot/bids/{bid_fid}"),
            spot_api::get_bid_v2_spot_bids_bid_fid_get(&self.http, bid_fid)
        );
        Ok(bid)
    }

    pub async fn fetch_instances_by_bid(
        &self,
        project_fid: &str,
        bid_fid: &str,
    ) -> Result<Vec<InstanceModel>> {
        let mut all_instances = Vec::new();
        let mut next_cursor = None;

        loop {
            let response = timed_api_call!(
                &format!("GET /v2/instances?project={project_fid}&bid_fid_in={bid_fid}"),
                instances_api::get_instances_v2_instances_get(
                    &self.http,
                    project_fid,
                    next_cursor.clone(),
                    None,                            // sort_by
                    None,                            // sort_dir
                    None,                            // instance_type
                    None,                            // region
                    None,                            // status_in
                    None,                            // order_type_in
                    Some(vec![bid_fid.to_string()]), // bid_fid_in
                    None,                            // reservation_fid_in
                    None,                            // limit
                )
            );

            all_instances.extend(response.data);

            if response.next_cursor.is_none() {
                break;
            }
            next_cursor = response.next_cursor.map(serde_json::Value::String);
        }

        Ok(all_instances)
    }

    pub async fn resolve_project(&self, name_filter: Option<&str>) -> Result<ProjectModel> {
        let projects = self.fetch_projects().await?;

        if projects.is_empty() {
            bail!("No projects found for this account");
        }

        // If name filter provided, find matching project
        if let Some(name) = name_filter {
            let matches: Vec<_> = projects
                .into_iter()
                .filter(|p| p.name == name || p.fid == name)
                .collect();

            return match matches.len() {
                0 => bail!("Project '{name}' not found"),
                1 => Ok(matches.into_iter().next().unwrap()),
                _ => bail!("Multiple projects match '{name}', use FID to disambiguate"),
            };
        }

        // Auto-select if only one project
        if projects.len() == 1 {
            return Ok(projects.into_iter().next().unwrap());
        }

        // Interactive selection
        let selection = Select::new()
            .with_prompt("Select project")
            .items(
                &projects
                    .iter()
                    .map(|p| format!("{} ({})", p.name, p.fid))
                    .collect::<Vec<_>>(),
            )
            .default(0)
            .interact()
            .context("Failed to get user selection")?;

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

    pub async fn fetch_k8s_clusters(&self, project: &str) -> Result<Vec<KubernetesClusterModel>> {
        let clusters = timed_api_call!(
            &format!("GET /v2/kubernetes/clusters?project={project}"),
            kubernetes_clusters_api::get_kubernetes_clusters_v2_kubernetes_clusters_get(
                &self.http, project
            )
        );
        Ok(clusters)
    }

    pub async fn fetch_k8s_cluster(&self, cluster_fid: &str) -> Result<KubernetesClusterModel> {
        let cluster = timed_api_call!(
            &format!("GET /v2/kubernetes/clusters/{cluster_fid}"),
            kubernetes_clusters_api::get_kubernetes_cluster_v2_kubernetes_clusters_cluster_fid_get(
                &self.http,
                cluster_fid
            )
        );
        Ok(cluster)
    }

    pub async fn resolve_k8s_cluster(
        &self,
        project: &str,
        name_or_fid: &str,
    ) -> Result<KubernetesClusterModel> {
        if name_or_fid.starts_with("clust_") {
            return self.fetch_k8s_cluster(name_or_fid).await;
        }

        let clusters = self.fetch_k8s_clusters(project).await?;
        let matches: Vec<_> = clusters
            .into_iter()
            .filter(|c| c.name == name_or_fid)
            .collect();

        match matches.len() {
            0 => bail!("Cluster '{name_or_fid}' not found"),
            1 => Ok(matches.into_iter().next().unwrap()),
            _ => {
                println!(
                    "Found {} clusters with name '{}':",
                    matches.len(),
                    name_or_fid
                );
                let selection = Select::new()
                    .with_prompt("Select cluster")
                    .items(
                        &matches
                            .iter()
                            .map(|c| format!("{} ({}, {})", c.name, c.region, c.fid))
                            .collect::<Vec<_>>(),
                    )
                    .default(0)
                    .interact()
                    .context("Failed to get user selection")?;
                Ok(matches.into_iter().nth(selection).unwrap())
            }
        }
    }
}

/// Parse shorthand like "8xh100" -> ("h100", 8) or "a100" -> ("a100", 1)
fn parse_instance_shorthand(input: &str) -> (String, i32) {
    // Pattern: "NUMx<gpu>" e.g. "8xh100" -> ("h100", 8)
    if let Some(pos) = input.find('x')
        && let Ok(count) = input[..pos].parse::<i32>()
    {
        let gpu_type = input[pos + 1..].to_string();
        if !gpu_type.is_empty() {
            return (gpu_type, count);
        }
    }
    // Pattern: "<gpu>.NUMx" e.g. "h200.8x" -> ("h200", 8)
    if let Some(pos) = input.rfind('.') {
        let suffix = &input[pos + 1..];
        if let Some(num_str) = suffix.strip_suffix('x')
            && let Ok(count) = num_str.parse::<i32>()
        {
            let gpu_type = input[..pos].to_string();
            return (gpu_type, count);
        }
    }
    // Default: single GPU
    (input.to_string(), 1)
}

/// Find all instance types matching user input.
/// Returns all matches (may be empty or have duplicates by name from different FIDs).
fn find_matching_instance_types<'a>(
    user_input: &str,
    instance_types: &'a HashMap<String, InstanceTypeModel>,
) -> Vec<(&'a String, &'a InstanceTypeModel)> {
    let input_lower = user_input.to_lowercase();

    // Direct FID match
    if let Some(it) = instance_types.get(user_input) {
        return vec![(
            instance_types
                .get_key_value(user_input)
                .map(|(k, _)| k)
                .unwrap(),
            it,
        )];
    }

    // Exact name match (case-insensitive)
    let exact_matches: Vec<_> = instance_types
        .iter()
        .filter(|(_, it)| it.name.to_lowercase() == input_lower)
        .collect();
    if !exact_matches.is_empty() {
        return exact_matches;
    }

    // Normalize user input: "8xh100" -> ("h100", 8), "a100" -> ("a100", 1)
    let (gpu_type, count) = parse_instance_shorthand(&input_lower);

    // Find matching instance type by GPU type and count
    instance_types
        .iter()
        .filter(|(_, it)| {
            let name_lower = it.name.to_lowercase();
            it.num_gpus == count && name_lower.contains(&gpu_type)
        })
        .collect()
}

#[allow(clippy::too_many_arguments)]
pub fn build_create_bid_request(
    project: &str,
    region: &str,
    instance_type: &str,
    limit_price: f64,
    instance_quantity: i32,
    name: &str,
    ssh_key_fids: Vec<String>,
    k8s_cluster: Option<String>,
) -> CreateBidRequest {
    let launch_spec = LaunchSpecificationModel {
        volumes: vec![],
        ssh_keys: ssh_key_fids,
        startup_script: None,
        kubernetes_cluster: k8s_cluster,
        image_version: None,
        memory_gb: None,
    };

    CreateBidRequest {
        project: project.to_string(),
        region: region.to_string(),
        instance_type: instance_type.to_string(),
        limit_price: format!("${limit_price:.2}"),
        instance_quantity,
        name: name.to_string(),
        launch_specification: Box::new(launch_spec),
    }
}
