use crate::config::CliConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

/// Keycloak device authorization response
#[derive(Deserialize)]
struct DeviceAuthResponse {
    device_code: String,
    user_code: String,
    verification_uri: Option<String>,
    verification_uri_complete: Option<String>,
    expires_in: u64,
    interval: Option<u64>,
}

/// Keycloak token response (device code + refresh flows)
#[derive(Deserialize)]
struct TokenResponse {
    access_token: String,
    refresh_token: Option<String>,
}

/// Keycloak error response during polling
#[derive(Deserialize)]
struct TokenErrorResponse {
    error: String,
}

/// Cached platform tokens, keyed by api_url
#[derive(Debug, Serialize, Deserialize, Default)]
struct PlatformTokensFile(HashMap<String, PlatformTokens>);

#[derive(Debug, Serialize, Deserialize, Clone)]
struct PlatformTokens {
    access_token: String,
    refresh_token: String,
    keycloak_url: String,
    realm: String,
    client_id: String,
}

fn tokens_path() -> PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    PathBuf::from(home)
        .join(".config")
        .join("cufflink")
        .join("platform-tokens.json")
}

fn load_tokens(api_url: &str) -> Option<PlatformTokens> {
    let path = tokens_path();
    let json = std::fs::read_to_string(path).ok()?;
    let file: PlatformTokensFile = serde_json::from_str(&json).ok()?;
    file.0.get(api_url).cloned()
}

fn save_tokens(api_url: &str, tokens: &PlatformTokens) {
    let path = tokens_path();
    let mut file: PlatformTokensFile = std::fs::read_to_string(&path)
        .ok()
        .and_then(|json| serde_json::from_str(&json).ok())
        .unwrap_or_default();

    file.0.insert(api_url.to_string(), tokens.clone());

    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    if let Ok(json) = serde_json::to_string_pretty(&file) {
        let _ = std::fs::write(&path, json);
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
        }
    }
}

/// Try to refresh the access token using a stored refresh token.
async fn try_refresh(tokens: &PlatformTokens) -> Option<TokenResponse> {
    let client = reqwest::Client::new();
    let token_url = format!(
        "{}/realms/{}/protocol/openid-connect/token",
        tokens.keycloak_url, tokens.realm
    );

    let resp = client
        .post(&token_url)
        .form(&[
            ("grant_type", "refresh_token"),
            ("refresh_token", &tokens.refresh_token),
            ("client_id", &tokens.client_id),
        ])
        .send()
        .await
        .ok()?;

    if resp.status().is_success() {
        resp.json::<TokenResponse>().await.ok()
    } else {
        None
    }
}

/// Discover auth config from the platform API.
/// Returns (keycloak_url, realm, client_id).
async fn discover_auth_config(api_url: &str) -> Option<(String, String, String)> {
    #[derive(Deserialize)]
    struct AuthConfig {
        keycloak_url: String,
        realm: String,
        cli_client_id: Option<String>,
        client_id: Option<String>,
    }

    let client = reqwest::Client::new();
    let resp = client
        .get(format!("{}/api/auth/config", api_url))
        .send()
        .await
        .ok()?;

    if resp.status().is_success() {
        let config: AuthConfig = resp.json().await.ok()?;
        let client_id = config
            .cli_client_id
            .or(config.client_id)
            .unwrap_or_else(|| "cufflink-cli".into());
        Some((config.keycloak_url, config.realm, client_id))
    } else {
        None
    }
}

/// Get a valid platform access token — uses cached refresh token if available,
/// falls back to device code auth only when needed.
async fn platform_auth(config: &CliConfig) -> eyre::Result<String> {
    // Discover auth config from the platform API, fall back to local config/defaults
    let (keycloak_url, realm, client_id) =
        if let Some(discovered) = discover_auth_config(&config.api_url).await {
            discovered
        } else {
            (
                config
                    .keycloak_url
                    .clone()
                    .unwrap_or_else(|| "http://localhost:8180".to_string()),
                config
                    .keycloak_realm
                    .clone()
                    .unwrap_or_else(|| "cufflink-platform".to_string()),
                config
                    .keycloak_client_id
                    .clone()
                    .unwrap_or_else(|| "cufflink-cli".to_string()),
            )
        };
    let keycloak_url = keycloak_url.as_str();
    let realm = realm.as_str();
    let client_id = client_id.as_str();

    // Try refreshing with cached tokens
    if let Some(cached) = load_tokens(&config.api_url) {
        if let Some(refreshed) = try_refresh(&cached).await {
            let new_tokens = PlatformTokens {
                access_token: refreshed.access_token.clone(),
                refresh_token: refreshed
                    .refresh_token
                    .unwrap_or(cached.refresh_token.clone()),
                keycloak_url: cached.keycloak_url,
                realm: cached.realm,
                client_id: cached.client_id,
            };
            save_tokens(&config.api_url, &new_tokens);
            return Ok(refreshed.access_token);
        }
    }

    // No cached tokens or refresh failed — do device code auth
    let client = reqwest::Client::new();

    println!("Authenticating with platform...");
    let device_url = format!(
        "{}/realms/{}/protocol/openid-connect/auth/device",
        keycloak_url, realm
    );

    let resp = client
        .post(&device_url)
        .form(&[("client_id", client_id)])
        .send()
        .await?;

    if !resp.status().is_success() {
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!(
            "Failed to start device auth (is the platform realm '{}' configured?): {}",
            realm,
            body
        );
    }

    let device: DeviceAuthResponse = resp.json().await?;

    let verify_url = device
        .verification_uri_complete
        .as_deref()
        .or(device.verification_uri.as_deref())
        .unwrap_or("(no verification URL)");

    println!();
    println!("  Open this URL in your browser:");
    println!("  {}", verify_url);
    println!();
    println!("  Enter code: {}", device.user_code);
    println!();
    println!("Waiting for authentication...");

    let token_url = format!(
        "{}/realms/{}/protocol/openid-connect/token",
        keycloak_url, realm
    );
    let interval = device.interval.unwrap_or(5);
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(device.expires_in);

    let token_resp = loop {
        if std::time::Instant::now() > deadline {
            eyre::bail!("Authentication timed out. Please try again.");
        }

        tokio::time::sleep(std::time::Duration::from_secs(interval)).await;

        let resp = client
            .post(&token_url)
            .form(&[
                ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
                ("device_code", &device.device_code),
                ("client_id", client_id),
            ])
            .send()
            .await?;

        if resp.status().is_success() {
            let token: TokenResponse = resp.json().await?;
            break token;
        }

        let err: TokenErrorResponse = resp.json().await?;
        match err.error.as_str() {
            "authorization_pending" => continue,
            "slow_down" => {
                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
                continue;
            }
            other => eyre::bail!("Authentication failed: {}", other),
        }
    };

    // Cache tokens for future commands
    if let Some(ref refresh_token) = token_resp.refresh_token {
        save_tokens(
            &config.api_url,
            &PlatformTokens {
                access_token: token_resp.access_token.clone(),
                refresh_token: refresh_token.clone(),
                keycloak_url: keycloak_url.to_string(),
                realm: realm.to_string(),
                client_id: client_id.to_string(),
            },
        );
    }

    println!("Authenticated.");
    Ok(token_resp.access_token)
}

pub async fn create(
    name: &str,
    slug: &str,
    keycloak_url: Option<&str>,
    keycloak_realm: Option<&str>,
    deploy_role: Option<&str>,
    api_url: Option<&str>,
    env: Option<&str>,
) -> eyre::Result<()> {
    let config = CliConfig::for_platform(api_url, env)?;
    let token = platform_auth(&config).await?;

    let client = reqwest::Client::new();
    let mut body = serde_json::json!({
        "name": name,
        "slug": slug,
    });
    if let Some(url) = keycloak_url {
        body["keycloak_url"] = serde_json::json!(url);
    }
    if let Some(realm) = keycloak_realm {
        body["keycloak_realm"] = serde_json::json!(realm);
    }
    if let Some(role) = deploy_role {
        body["deploy_role"] = serde_json::json!(role);
    }

    let resp = client
        .post(format!("{}/api/tenants", config.api_url))
        .header("Authorization", format!("Bearer {}", token))
        .json(&body)
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to create tenant ({}): {}", status, body);
    }

    let tenant: serde_json::Value = resp.json().await?;
    println!();
    println!("  Tenant created:");
    println!(
        "  Name:           {}",
        tenant["name"].as_str().unwrap_or("")
    );
    println!(
        "  Slug:           {}",
        tenant["slug"].as_str().unwrap_or("")
    );
    println!(
        "  Keycloak URL:   {}",
        tenant["keycloak_url"].as_str().unwrap_or("(not set)")
    );
    println!(
        "  Keycloak Realm: {}",
        tenant["keycloak_realm"].as_str().unwrap_or("(not set)")
    );
    println!(
        "  Deploy Role:    {}",
        tenant["deploy_role"].as_str().unwrap_or("")
    );
    println!();
    println!("Developers can now run `cufflink login` with the tenant's Keycloak credentials.");

    Ok(())
}

#[allow(clippy::too_many_arguments)]
pub async fn update(
    slug: &str,
    name: Option<&str>,
    keycloak_url: Option<&str>,
    keycloak_realm: Option<&str>,
    deploy_role: Option<&str>,
    keycloak_ops_client_id: Option<&str>,
    keycloak_ops_client_secret: Option<&str>,
    api_url: Option<&str>,
    env: Option<&str>,
) -> eyre::Result<()> {
    let config = CliConfig::for_platform(api_url, env)?;
    let token = platform_auth(&config).await?;

    let client = reqwest::Client::new();
    let mut body = serde_json::Map::new();
    if let Some(v) = name {
        body.insert("name".into(), serde_json::json!(v));
    }
    if let Some(v) = keycloak_url {
        body.insert("keycloak_url".into(), serde_json::json!(v));
    }
    if let Some(v) = keycloak_realm {
        body.insert("keycloak_realm".into(), serde_json::json!(v));
    }
    if let Some(v) = deploy_role {
        body.insert("deploy_role".into(), serde_json::json!(v));
    }
    if let Some(v) = keycloak_ops_client_id {
        body.insert("keycloak_ops_client_id".into(), serde_json::json!(v));
    }
    if let Some(v) = keycloak_ops_client_secret {
        body.insert("keycloak_ops_client_secret".into(), serde_json::json!(v));
    }

    if body.is_empty() {
        eyre::bail!("Nothing to update. Provide at least one of: --name, --tenant-keycloak-url, --tenant-keycloak-realm, --deploy-role, --keycloak-ops-client-id, --keycloak-ops-client-secret");
    }

    let resp = client
        .put(format!("{}/api/tenants/{}", config.api_url, slug))
        .header("Authorization", format!("Bearer {}", token))
        .json(&body)
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to update tenant ({}): {}", status, body);
    }

    let tenant: serde_json::Value = resp.json().await?;
    println!("Tenant '{}' updated:", slug);
    println!(
        "  Name:           {}",
        tenant["name"].as_str().unwrap_or("")
    );
    println!(
        "  Keycloak URL:   {}",
        tenant["keycloak_url"].as_str().unwrap_or("(not set)")
    );
    println!(
        "  Keycloak Realm: {}",
        tenant["keycloak_realm"].as_str().unwrap_or("(not set)")
    );
    println!(
        "  Deploy Role:    {}",
        tenant["deploy_role"].as_str().unwrap_or("")
    );

    Ok(())
}

pub async fn list(api_url: Option<&str>, env: Option<&str>) -> eyre::Result<()> {
    let config = CliConfig::for_platform(api_url, env)?;
    let token = platform_auth(&config).await?;

    let client = reqwest::Client::new();
    let resp = client
        .get(format!("{}/api/tenants", config.api_url))
        .header("Authorization", format!("Bearer {}", token))
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to list tenants ({}): {}", status, body);
    }

    let tenants: Vec<serde_json::Value> = resp.json().await?;

    if tenants.is_empty() {
        println!("No tenants found.");
        return Ok(());
    }

    use comfy_table::{presets::NOTHING, Table, TableComponent};

    let mut table = Table::new();
    table.load_preset(NOTHING);
    table.set_style(TableComponent::HeaderLines, '-');
    table.set_style(TableComponent::MiddleHeaderIntersections, ' ');
    table.set_header(vec!["SLUG", "NAME", "KEYCLOAK", "DEPLOY ROLE"]);

    for t in &tenants {
        let kc = match (t["keycloak_url"].as_str(), t["keycloak_realm"].as_str()) {
            (Some(url), Some(realm)) => format!("{} / {}", url, realm),
            _ => "(not configured)".to_string(),
        };
        table.add_row(vec![
            t["slug"].as_str().unwrap_or(""),
            t["name"].as_str().unwrap_or(""),
            &kc,
            t["deploy_role"].as_str().unwrap_or(""),
        ]);
    }

    println!("{table}");

    Ok(())
}

pub async fn delete(
    slug: &str,
    yes: bool,
    api_url: Option<&str>,
    env: Option<&str>,
) -> eyre::Result<()> {
    if !yes {
        println!(
            "This will permanently delete tenant '{}' and all its data.",
            slug
        );
        print!("Are you sure? (y/N) ");
        use std::io::Write;
        std::io::stdout().flush()?;
        let mut input = String::new();
        std::io::stdin().read_line(&mut input)?;
        if !input.trim().eq_ignore_ascii_case("y") {
            println!("Cancelled.");
            return Ok(());
        }
    }

    let config = CliConfig::for_platform(api_url, env)?;
    let token = platform_auth(&config).await?;

    let client = reqwest::Client::new();
    let resp = client
        .delete(format!("{}/api/tenants/{}", config.api_url, slug))
        .header("Authorization", format!("Bearer {}", token))
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to delete tenant ({}): {}", status, body);
    }

    println!("Tenant '{}' deleted.", slug);
    Ok(())
}

fn platform_api_auth() -> eyre::Result<String> {
    std::env::var("CUFFLINK_PLATFORM_API_KEY")
        .map(|k| format!("ApiKey {}", k))
        .map_err(|_| eyre::eyre!("CUFFLINK_PLATFORM_API_KEY env var required"))
}

pub async fn create_platform_api_key(
    name: &str,
    api_url: Option<&str>,
    env: Option<&str>,
) -> eyre::Result<()> {
    let config = CliConfig::for_platform(api_url, env)?;
    let auth = match platform_api_auth() {
        Ok(a) => a,
        Err(_) => format!("Bearer {}", platform_auth(&config).await?),
    };

    let client = reqwest::Client::new();
    let resp = client
        .post(format!("{}/api/platform/api-keys", config.api_url))
        .header("Authorization", &auth)
        .json(&serde_json::json!({ "name": name }))
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to create API key ({}): {}", status, body);
    }

    let body: serde_json::Value = resp.json().await?;
    println!("Platform admin API key created:");
    println!("  Name: {}", body["name"].as_str().unwrap_or("?"));
    println!("  ID:   {}", body["id"].as_str().unwrap_or("?"));
    println!("  Key:  {}", body["key"].as_str().unwrap_or("?"));
    println!();
    println!("Save this key — it cannot be retrieved again.");

    Ok(())
}

pub async fn list_platform_api_keys(api_url: Option<&str>, env: Option<&str>) -> eyre::Result<()> {
    let config = CliConfig::for_platform(api_url, env)?;
    let auth = match platform_api_auth() {
        Ok(a) => a,
        Err(_) => format!("Bearer {}", platform_auth(&config).await?),
    };

    let client = reqwest::Client::new();
    let resp = client
        .get(format!("{}/api/platform/api-keys", config.api_url))
        .header("Authorization", &auth)
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to list API keys ({}): {}", status, body);
    }

    let body: serde_json::Value = resp.json().await?;
    let keys = body["keys"].as_array();

    if keys.map(|k| k.is_empty()).unwrap_or(true) {
        println!("No platform admin API keys.");
        return Ok(());
    }

    use comfy_table::{presets::NOTHING, Table, TableComponent};
    let mut table = Table::new();
    table.load_preset(NOTHING);
    table.set_style(TableComponent::HeaderLines, '-');
    table.set_style(TableComponent::MiddleHeaderIntersections, ' ');
    table.set_header(vec!["ID", "NAME", "CREATED", "EXPIRES"]);

    for key in keys.unwrap() {
        table.add_row(vec![
            key["id"].as_str().unwrap_or("?"),
            key["name"].as_str().unwrap_or("?"),
            key["created_at"].as_str().unwrap_or("?"),
            key["expires_at"].as_str().unwrap_or("never"),
        ]);
    }

    println!("{table}");
    Ok(())
}

pub async fn revoke_platform_api_key(
    id: &str,
    api_url: Option<&str>,
    env: Option<&str>,
) -> eyre::Result<()> {
    let config = CliConfig::for_platform(api_url, env)?;
    let auth = match platform_api_auth() {
        Ok(a) => a,
        Err(_) => format!("Bearer {}", platform_auth(&config).await?),
    };

    let client = reqwest::Client::new();
    let resp = client
        .delete(format!("{}/api/platform/api-keys/{}", config.api_url, id))
        .header("Authorization", &auth)
        .send()
        .await?;

    if resp.status().is_success() {
        println!("API key revoked.");
    } else {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to revoke API key ({}): {}", status, body);
    }

    Ok(())
}
