use anyhow::{Context, Result, bail};
use dialoguer::{Confirm, Input, Select};
use log::debug;
use mithril_client::apis::configuration::Configuration;
use mithril_client::apis::{Error as ApiError, profile_api, projects_api};
use reqwest::StatusCode;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::{IsTerminal, Write};
use std::process::{Command, Stdio};

use crate::config::{
    ConfigFile, Profile, get_config_path, load_config_file, load_current_profile, resolve_api_url,
};
use crate::constants::{DEFAULT_API_URL, api_keys_url, billing_settings_url};

// Note: This module intentionally doesn't use api::Client because setup is the
// bootstrapping command that creates the config file api::Client::load() reads from.
// We need to validate user-provided credentials before saving them, so we make
// direct API calls with a temporary Configuration instead.

fn create_api_config(api_key: &str) -> Result<Configuration> {
    let api_base_url = resolve_api_url(DEFAULT_API_URL);

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

    Ok(Configuration {
        base_path: api_base_url,
        // TODO: bake in the python package version here.
        user_agent: Some("mcli/0.1.0".to_string()),
        client,
        basic_auth: None,
        oauth_access_token: None,
        bearer_access_token: Some(api_key.to_string()),
        api_key: None,
    })
}

/// Returns the API URL to store in config, or None if using default.
fn get_api_url_for_config() -> Option<String> {
    let url = resolve_api_url(DEFAULT_API_URL);
    if url == DEFAULT_API_URL {
        None
    } else {
        Some(url)
    }
}

// Billing check types and helpers

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
enum BillingCheckCode {
    Ok,
    CustomerNotFound,
    BillingAddressMissing,
    PaymentMethodMissing,
    PaymentMethodPending,
    PaymentMethodFailed,
    CardRequired,
}

impl BillingCheckCode {
    const fn as_str(self) -> &'static str {
        match self {
            Self::Ok => "ok",
            Self::CustomerNotFound => "customer_not_found",
            Self::BillingAddressMissing => "billing_address_missing",
            Self::PaymentMethodMissing => "payment_method_missing",
            Self::PaymentMethodPending => "payment_method_pending",
            Self::PaymentMethodFailed => "payment_method_failed",
            Self::CardRequired => "card_required",
        }
    }
}

#[derive(Deserialize)]
struct BillingCheckResponse {
    code: BillingCheckCode,
    message: Option<String>,
}

const BILLING_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3);

async fn check_billing_status(
    config: &Configuration,
    user_id: &str,
) -> Result<BillingCheckResponse> {
    // Not using the generated API client because this endpoint is undocumented
    let url = format!("{}/users/{}/billing_check", config.base_path, user_id);
    let response = config
        .client
        .get(&url)
        .bearer_auth(config.bearer_access_token.as_ref().unwrap())
        .send()
        .await
        .context("Failed to check billing status")?;
    response
        .json()
        .await
        .context("Failed to parse billing response")
}

fn non_empty(value: &str) -> Option<String> {
    let value = value.trim();
    if value.is_empty() {
        None
    } else {
        Some(value.to_string())
    }
}

fn resolve_effective_value(
    env_value: Option<String>,
    profile_value: Option<&str>,
) -> Option<String> {
    // Keep `ml setup --check` aligned with `load_config` precedence:
    // env var presence overrides profile fallback, even when the env value is empty.
    // After precedence selection, empty/whitespace is treated as missing.
    env_value
        .or_else(|| profile_value.map(str::to_string))
        .and_then(|value| non_empty(&value))
}

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

    #[test]
    fn uses_env_value_when_set() {
        let resolved = resolve_effective_value(Some("env-key".to_string()), Some("profile-key"));
        assert_eq!(resolved.as_deref(), Some("env-key"));
    }

    #[test]
    fn falls_back_to_profile_when_env_unset() {
        let resolved = resolve_effective_value(None, Some("profile-key"));
        assert_eq!(resolved.as_deref(), Some("profile-key"));
    }

    #[test]
    fn empty_env_overrides_profile() {
        let resolved = resolve_effective_value(Some(String::new()), Some("profile-key"));
        assert_eq!(resolved, None);
    }

    #[test]
    fn whitespace_env_overrides_profile() {
        let resolved = resolve_effective_value(Some("   ".to_string()), Some("profile-key"));
        assert_eq!(resolved, None);
    }

    #[test]
    fn empty_profile_is_missing() {
        let resolved = resolve_effective_value(None, Some(""));
        assert_eq!(resolved, None);
    }
}

fn is_auth_error<T>(error: &ApiError<T>) -> bool {
    matches!(
        error,
        ApiError::ResponseError(content)
            if content.status == StatusCode::UNAUTHORIZED || content.status == StatusCode::FORBIDDEN
    )
}

fn api_error_summary<T>(error: &ApiError<T>) -> String {
    match error {
        ApiError::ResponseError(content) => {
            format!("HTTP {} from API", content.status.as_u16())
        }
        _ => error.to_string(),
    }
}

fn status_token_from_line(line: &str) -> &str {
    let value = line.split_once(": ").map_or(line, |(_, rest)| rest);
    value.split_whitespace().next().unwrap_or(value)
}

async fn check_api_key(
    api_key: Option<&str>,
) -> (bool, String, Option<Configuration>, Option<String>) {
    let Some(api_key_value) = api_key else {
        return (false, "- API key: missing".to_string(), None, None);
    };

    match create_api_config(api_key_value) {
        Ok(config) => match profile_api::get_me_v2_me_get(&config).await {
            Ok(me) => (true, "- API key: ok".to_string(), Some(config), Some(me.id)),
            Err(error) if is_auth_error(&error) => (
                false,
                format!("- API key: invalid ({})", api_error_summary(&error)),
                None,
                None,
            ),
            Err(error) => (
                false,
                format!("- API key: error ({})", api_error_summary(&error)),
                None,
                None,
            ),
        },
        Err(error) => (false, format!("- API key: error ({})", error), None, None),
    }
}

async fn check_project(
    project_id: Option<&str>,
    api_config: Option<&Configuration>,
) -> (bool, String) {
    let Some(project_fid) = project_id else {
        return (false, "- Project: missing".to_string());
    };

    let Some(config) = api_config else {
        return (
            false,
            "- Project: error (requires valid API key)".to_string(),
        );
    };

    match projects_api::get_projects_v2_projects_get(config).await {
        Ok(projects) => {
            if projects.iter().any(|project| project.fid == project_fid) {
                (true, "- Project: ok".to_string())
            } else {
                (
                    false,
                    format!(
                        "- Project: invalid ({} not found for this account)",
                        project_fid
                    ),
                )
            }
        }
        Err(error) => (
            false,
            format!("- Project: error ({})", api_error_summary(&error)),
        ),
    }
}

async fn check_billing(
    user_id: Option<&str>,
    api_config: Option<&Configuration>,
) -> (bool, String) {
    let (Some(current_user_id), Some(config)) = (user_id, api_config) else {
        return (
            false,
            "- Billing: skipped (requires valid API key)".to_string(),
        );
    };

    match check_billing_status(config, current_user_id).await {
        Ok(billing) if billing.code == BillingCheckCode::Ok => (true, "- Billing: ok".to_string()),
        Ok(billing) => {
            let mut detail = billing.code.as_str().to_string();
            if let Some(message) = billing.message.as_deref().map(str::trim)
                && !message.is_empty()
            {
                detail = format!("{}: {}", detail, message);
            }
            (false, format!("- Billing: not_configured ({})", detail))
        }
        Err(error) => (false, format!("- Billing: error ({})", error)),
    }
}

async fn ensure_billing_configured(config: &Configuration) -> Result<()> {
    let me = profile_api::get_me_v2_me_get(config)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to get user info: {:?}", e))?;

    loop {
        let billing = check_billing_status(config, &me.id).await?;

        if billing.code == BillingCheckCode::Ok {
            return Ok(());
        }

        // For pending status, poll until resolved
        if billing.code == BillingCheckCode::PaymentMethodPending {
            println!("Your payment method is being verified. This may take a few moments...");
            loop {
                tokio::time::sleep(BILLING_POLL_INTERVAL).await;
                let billing = check_billing_status(config, &me.id).await?;
                if billing.code == BillingCheckCode::Ok {
                    println!("\x1b[32m✓\x1b[0m Billing configured");
                    println!();
                    return Ok(());
                }
                if billing.code != BillingCheckCode::PaymentMethodPending {
                    break; // Status changed, re-enter main loop
                }
            }
            continue;
        }

        // Display API message if present
        if let Some(message) = &billing.message {
            println!();
            println!("\x1b[33m{}\x1b[0m", message);
        }

        let url = billing_settings_url();
        println!();
        println!("Configure billing at:");
        println!("  \x1b[36m\x1b[4m{}\x1b[0m", url);
        println!();
        println!("Press \x1b[1mEnter\x1b[0m to open in browser...");
        let _ = std::io::stdin().read_line(&mut String::new());
        let _ = open::that(&url);

        println!();
        println!("Press \x1b[1mEnter\x1b[0m when billing setup is complete...");
        let _ = std::io::stdin().read_line(&mut String::new());

        let billing = check_billing_status(config, &me.id).await?;
        if billing.code == BillingCheckCode::Ok {
            println!("\x1b[32m✓\x1b[0m Billing configured");
            println!();
            return Ok(());
        }

        // Still not configured - simple retry prompt
        if let Some(message) = &billing.message {
            println!("\x1b[33m{}\x1b[0m", message);
        }
        println!();
        println!(
            "Billing setup incomplete. Press \x1b[1mEnter\x1b[0m to try again, or Ctrl+C to exit."
        );
        let _ = std::io::stdin().read_line(&mut String::new());
    }
}

enum SetupAction {
    EditCurrentProfile(String),
    CreateNewProfile,
    SwitchProfile(String),
    SwitchProject,
}

async fn prompt_existing_config_action(config: &ConfigFile) -> Result<SetupAction> {
    let current = &config.current_profile;
    let profile_names: Vec<_> = config.profiles.keys().cloned().collect();
    let has_multiple_profiles = profile_names.len() > 1;

    println!("Using profile \x1b[1m{}\x1b[0m", current);
    println!();
    if let Some(profile) = config.profiles.get(current) {
        let masked_key = if profile.api_key.len() > 8 {
            format!(
                "{}...{}",
                &profile.api_key[..4],
                &profile.api_key[profile.api_key.len() - 4..]
            )
        } else {
            "****".to_string()
        };
        println!("        \x1b[2mKey:\x1b[0m {}", masked_key);

        // Resolve project name from API
        let project_display =
            match resolve_project_name(&profile.api_key, &profile.project_id).await {
                Ok(name) => format!("{} ({})", name, profile.project_id),
                Err(_) => profile.project_id.clone(),
            };
        println!("    \x1b[2mProject:\x1b[0m {}", project_display);

        if let Some(ref url) = profile.api_url {
            println!("    \x1b[2mAPI URL:\x1b[0m {}", url);
        }
    }
    println!();

    let mut items = Vec::new();
    let mut switch_profile_index = None;

    if has_multiple_profiles {
        switch_profile_index = Some(items.len());
        items.push("Switch profile".to_string());
    }

    let switch_project_index = items.len();
    items.push("Switch project".to_string());

    let edit_index = items.len();
    items.push(format!("Edit current profile ({})", current));

    let create_index = items.len();
    items.push("Create new profile".to_string());

    let selection = Select::new()
        .with_prompt("What would you like to do?")
        .items(&items)
        .default(0)
        .interact()
        .context("Failed to get user selection")?;

    if switch_profile_index == Some(selection) {
        let other_profiles: Vec<_> = profile_names
            .into_iter()
            .filter(|name| name != current)
            .collect();

        let selection = Select::new()
            .with_prompt("Select profile to switch to")
            .items(&other_profiles)
            .default(0)
            .interact()
            .context("Failed to get user selection")?;

        Ok(SetupAction::SwitchProfile(
            other_profiles[selection].clone(),
        ))
    } else if selection == switch_project_index {
        Ok(SetupAction::SwitchProject)
    } else if selection == edit_index {
        Ok(SetupAction::EditCurrentProfile(current.clone()))
    } else if selection == create_index {
        Ok(SetupAction::CreateNewProfile)
    } else {
        unreachable!()
    }
}

fn prompt_new_profile_name(
    existing_profiles: &HashMap<String, crate::config::Profile>,
) -> Result<String> {
    loop {
        let name: String = Input::new()
            .with_prompt("Enter new profile name")
            .interact_text()
            .context("Failed to read profile name")?;

        let name = name.trim().to_string();

        if name.is_empty() {
            println!("Profile name cannot be empty. Please try again.");
            continue;
        }

        if existing_profiles.contains_key(&name) {
            println!(
                "Profile '{}' already exists. Please choose a different name.",
                name
            );
            continue;
        }

        return Ok(name);
    }
}

async fn prompt_api_key_and_project() -> Result<(String, String, String)> {
    let api_keys_url = api_keys_url();
    println!("Get your API key at:");
    println!("  \x1b[36m\x1b[4m{}\x1b[0m", api_keys_url);
    println!();
    println!("Press \x1b[1mEnter\x1b[0m to open in browser...");
    let _ = std::io::stdin().read_line(&mut String::new());
    let _ = open::that(&api_keys_url);
    println!();

    loop {
        // Prompt for API key
        let api_key: String = Input::new()
            .with_prompt("Enter your API key")
            .interact_text()
            .context("Failed to read API key")?;

        let api_key = api_key.trim().to_string();
        println!();

        if api_key.is_empty() {
            println!("API key cannot be empty. Please try again.");
            println!();
            continue;
        }

        let config = create_api_config(&api_key)?;
        let projects = match projects_api::get_projects_v2_projects_get(&config).await {
            Ok(projects) => projects,
            Err(e) => {
                debug!("API key validation failed: {:?}", e);
                println!();
                println!("Invalid API key. Please check your key and try again.");
                println!();
                continue;
            }
        };

        if projects.is_empty() {
            bail!("No projects found for this account. Please create a project first.");
        }

        // Check billing status before proceeding
        ensure_billing_configured(&config).await?;

        // Interactive project selection
        let project = if projects.len() == 1 {
            let project = &projects[0];
            println!("Using project: {} ({})", project.name, project.fid);
            println!();
            project
        } else {
            let items: Vec<String> = projects
                .iter()
                .map(|p| format!("{} ({})", p.name, p.fid))
                .collect();

            let selection = Select::new()
                .with_prompt("Select project")
                .items(&items)
                .default(0)
                .interact()
                .context("Failed to get user selection")?;

            println!();
            &projects[selection]
        };

        return Ok((api_key, project.fid.clone(), project.name.clone()));
    }
}

async fn prompt_select_project(api_key: &str) -> Result<(String, String)> {
    println!("Fetching projects...");

    let config = create_api_config(api_key)?;
    let projects = projects_api::get_projects_v2_projects_get(&config)
        .await
        .context("Failed to fetch projects")?;

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

    let items: Vec<String> = projects
        .iter()
        .map(|p| format!("{} ({})", p.name, p.fid))
        .collect();

    let selection = Select::new()
        .with_prompt("Select project")
        .items(&items)
        .default(0)
        .interact()
        .context("Failed to get user selection")?;

    println!();
    let project = &projects[selection];
    Ok((project.fid.clone(), project.name.clone()))
}

async fn resolve_project_name(api_key: &str, project_id: &str) -> Result<String> {
    let config = create_api_config(api_key)?;
    let projects = projects_api::get_projects_v2_projects_get(&config)
        .await
        .context("Failed to fetch projects")?;

    projects
        .into_iter()
        .find(|p| p.fid == project_id)
        .map(|p| p.name)
        .context("Project not found")
}

/// Run a non-interactive setup health check.
///
/// Verifies API key, project access, and billing status, then prints a
/// component-by-component summary.
///
/// Exit codes:
/// - 0: all components are ready
/// - 1: one or more components are not ready
pub async fn run_check() -> Result<i32> {
    let env_api_key = std::env::var("MITHRIL_API_KEY").ok();
    let env_project_id = std::env::var("MITHRIL_PROJECT").ok();
    // We only need to read profile config when at least one required setting
    // is not present in env.
    let needs_profile_fallback = env_api_key.is_none() || env_project_id.is_none();

    let (profile, profile_error) = if needs_profile_fallback {
        match load_current_profile() {
            Ok(profile) => (profile, None),
            Err(error) => (None, Some(error.to_string())),
        }
    } else {
        (None, None)
    };

    let api_key = resolve_effective_value(
        env_api_key,
        profile
            .as_ref()
            .map(|current_profile| current_profile.api_key.as_str()),
    );
    let project_id = resolve_effective_value(
        env_project_id,
        profile
            .as_ref()
            .map(|current_profile| current_profile.project_id.as_str()),
    );

    let (api_key_ok, api_key_line, api_config, user_id) = check_api_key(api_key.as_deref()).await;
    let (project_ok, project_line) =
        check_project(project_id.as_deref(), api_config.as_ref()).await;
    let (billing_ok, billing_line) = check_billing(user_id.as_deref(), api_config.as_ref()).await;

    let ready = api_key_ok && project_ok && billing_ok;

    let api_key_status = status_token_from_line(&api_key_line);
    let project_status = status_token_from_line(&project_line);
    let billing_status = status_token_from_line(&billing_line);

    println!(
        "Setup check: {} (api_key={}, project={}, billing={})",
        if ready { "READY" } else { "NOT READY" },
        api_key_status,
        project_status,
        billing_status
    );

    if let Some(error) = profile_error {
        println!("Config note: {}", error);
    }

    Ok(if ready { 0 } else { 1 })
}

fn launch_onboarding_command(invoked_as: &str) -> String {
    let command_name = std::path::Path::new(invoked_as)
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("ml");
    format!("{command_name} launch")
}

fn prompt_launch_onboarding() -> Result<bool> {
    if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
        return Ok(false);
    }

    Confirm::new()
        .with_prompt("Would you like help launching your first workload now?")
        .default(true)
        .interact()
        .context("Failed to get user selection")
}

fn run_launch_onboarding(invoked_as: &str) -> Result<()> {
    let status = Command::new(invoked_as)
        .arg("launch")
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .with_context(|| format!("Failed to run `{}`", launch_onboarding_command(invoked_as)))?;

    if !status.success() {
        let status = status
            .code()
            .map_or("unknown".to_string(), |code| code.to_string());
        eprintln!();
        eprintln!(
            "Launch onboarding exited with status {status}. You can start it later with `{}`.",
            launch_onboarding_command(invoked_as)
        );
    }

    Ok(())
}

pub async fn run(invoked_as: &str) -> Result<()> {
    let config_path = get_config_path()?;
    let existing_config = load_config_file(&config_path)?;

    let config = match existing_config {
        Some(existing) => {
            let action = prompt_existing_config_action(&existing).await?;
            println!();

            match action {
                SetupAction::EditCurrentProfile(name) => {
                    let mut config = existing;
                    let (api_key, project_id, _project_name) = prompt_api_key_and_project().await?;

                    config.profiles.insert(
                        name,
                        Profile {
                            api_key,
                            project_id,
                            api_url: get_api_url_for_config(),
                        },
                    );
                    config
                }
                SetupAction::CreateNewProfile => {
                    let name = prompt_new_profile_name(&existing.profiles)?;
                    println!();

                    let (api_key, project_id, _project_name) = prompt_api_key_and_project().await?;

                    let mut config = existing;
                    config.profiles.insert(
                        name.clone(),
                        Profile {
                            api_key,
                            project_id,
                            api_url: get_api_url_for_config(),
                        },
                    );

                    // Ask if they want to set it as current
                    let set_current = Select::new()
                        .with_prompt(format!("Set '{}' as current profile?", name))
                        .items(&["Yes", "No"])
                        .default(0)
                        .interact()
                        .context("Failed to get user selection")?;

                    if set_current == 0 {
                        config.current_profile = name;
                    }
                    config
                }
                SetupAction::SwitchProfile(name) => {
                    let mut config = existing;
                    config.current_profile = name.clone();
                    println!("Switched to profile: {}", name);
                    config
                }
                SetupAction::SwitchProject => {
                    let current_profile = existing
                        .profiles
                        .get(&existing.current_profile)
                        .context("Current profile not found")?;

                    let (project_id, project_name) =
                        prompt_select_project(&current_profile.api_key).await?;

                    let mut config = existing;
                    let profile = config
                        .profiles
                        .get_mut(&config.current_profile)
                        .context("Current profile not found")?;
                    profile.project_id = project_id;

                    println!("Switched to project: {}", project_name);
                    config
                }
            }
        }
        None => {
            let (api_key, project_id, _project_name) = prompt_api_key_and_project().await?;

            ConfigFile {
                current_profile: "default".to_string(),
                profiles: HashMap::from([(
                    "default".to_string(),
                    Profile {
                        api_key,
                        project_id,
                        api_url: get_api_url_for_config(),
                    },
                )]),
            }
        }
    };

    if let Some(parent) = config_path.parent() {
        std::fs::create_dir_all(parent).context("Failed to create config directory")?;
    }

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

    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        let mut file = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .mode(0o600) // Owner read/write only
            .open(&config_path)
            .with_context(|| format!("Failed to create config at {}", config_path.display()))?;
        file.write_all(yaml_content.as_bytes())
            .with_context(|| format!("Failed to write config to {}", config_path.display()))?;
    }

    // On non-Unix platforms (Windows), fall back to standard write.
    // Windows uses ACLs instead of Unix permission modes, and user home
    // directories typically have restrictive defaults already.
    #[cfg(not(unix))]
    {
        std::fs::write(&config_path, &yaml_content)
            .with_context(|| format!("Failed to write config to {}", config_path.display()))?;
    }

    println!();
    println!("Configuration saved to: {}", config_path.display());
    println!();
    println!("Setup complete.");

    let launch_cmd = launch_onboarding_command(invoked_as);
    if prompt_launch_onboarding()? {
        println!();
        if let Err(error) = run_launch_onboarding(invoked_as) {
            eprintln!("Couldn't start launch onboarding: {error}");
            println!(
                "Run \x1b[1m{}\x1b[0m with no arguments to get started.",
                launch_cmd
            );
        }
    } else {
        println!(
            "Run \x1b[1m{}\x1b[0m with no arguments when you're ready to get started.",
            launch_cmd
        );
    }

    Ok(())
}
