use anyhow::{Context, Result};
use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::io::{self, Write};
use std::time::Duration;
use uuid::Uuid;

use crate::config::EnvironmentConfig;

const POLL_INTERVAL_MS: u64 = 2000;
const MAX_POLL_ATTEMPTS: u32 = 300; // 10 minutes

#[derive(Debug, Serialize, Deserialize)]
struct AuthRequest {
    request_id: String,
    session_name: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct AuthResponse {
    auth_url: String,
    request_id: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct TokenResponse {
    token: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct UserToken {
    pub id: String,
    #[serde(rename = "tokenType")]
    pub token_type: String,
    #[serde(rename = "requestId")]
    pub request_id: Option<String>,
    #[serde(rename = "sessionName")]
    pub session_name: Option<String>,
    #[serde(rename = "expiresAt")]
    pub expires_at: String,
    pub created: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct JwtUser {
    id: String,
    name: String,
    username: String,
    email: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct JwtClaims {
    id: String,
    user: JwtUser,
    #[serde(default)]
    administrator: bool,
    exp: usize,
    iat: usize,
    jti: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ProfileInfo {
    pub token: String,
    pub username: String,
    pub created_at: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct ProfilesData {
    active_profile: String,
    profiles: HashMap<String, ProfileInfo>,
}

fn get_config_dir() -> Result<std::path::PathBuf> {
    let config_dir = dirs::config_dir()
        .context("Could not determine config directory")?
        .join("doqium");

    fs::create_dir_all(&config_dir).context("Could not create config directory")?;

    Ok(config_dir)
}

fn get_token_path() -> Result<std::path::PathBuf> {
    Ok(get_config_dir()?.join("token"))
}

fn get_profiles_path() -> Result<std::path::PathBuf> {
    Ok(get_config_dir()?.join("profiles.json"))
}

fn load_profiles() -> Result<ProfilesData> {
    let profiles_path = get_profiles_path()?;

    if !profiles_path.exists() {
        return Ok(ProfilesData {
            active_profile: "default".to_string(),
            profiles: HashMap::new(),
        });
    }

    let content = fs::read_to_string(&profiles_path).context("Failed to read profiles file")?;
    let profiles: ProfilesData =
        serde_json::from_str(&content).context("Failed to parse profiles file")?;

    Ok(profiles)
}

fn save_profiles(profiles: &ProfilesData) -> Result<()> {
    let profiles_path = get_profiles_path()?;
    let content = serde_json::to_string_pretty(profiles).context("Failed to serialize profiles")?;
    fs::write(&profiles_path, content).context("Failed to save profiles")?;
    Ok(())
}

pub fn load_token() -> Result<Option<String>> {
    // First try the new profiles system
    let profiles = load_profiles()?;
    if let Some(profile) = profiles.profiles.get(&profiles.active_profile) {
        return Ok(Some(profile.token.clone()));
    }

    // Fall back to old token file for backward compatibility
    let token_path = get_token_path()?;
    if !token_path.exists() {
        return Ok(None);
    }

    let token = fs::read_to_string(&token_path).context("Failed to read token file")?;
    Ok(Some(token.trim().to_string()))
}

pub fn load_token_for_profile(profile_name: &str) -> Result<Option<String>> {
    let profiles = load_profiles()?;
    Ok(profiles.profiles.get(profile_name).map(|p| p.token.clone()))
}

pub fn load_token_with_override(profile_override: Option<&str>) -> Result<Option<String>> {
    if let Some(profile_name) = profile_override {
        load_token_for_profile(profile_name)
    } else {
        load_token()
    }
}

fn save_token(token: &str) -> Result<()> {
    let token_path = get_token_path()?;
    fs::write(&token_path, token).context("Failed to save token")?;
    Ok(())
}

fn save_token_to_profile(token: &str, profile_name: &str) -> Result<()> {
    let username = extract_username_from_token(token)?;
    let created_at = chrono::Utc::now().to_rfc3339();

    let mut profiles = load_profiles()?;
    profiles.profiles.insert(
        profile_name.to_string(),
        ProfileInfo {
            token: token.to_string(),
            username,
            created_at,
        },
    );
    profiles.active_profile = profile_name.to_string();
    save_profiles(&profiles)?;
    Ok(())
}

fn extract_username_from_token(token: &str) -> Result<String> {
    let parts: Vec<&str> = token.split('.').collect();
    if parts.len() != 3 {
        anyhow::bail!("Invalid token format");
    }

    let payload = parts[1];
    let decoded = general_purpose::URL_SAFE_NO_PAD
        .decode(payload)
        .context("Failed to decode token")?;
    let claims: JwtClaims =
        serde_json::from_slice(&decoded).context("Failed to parse token claims")?;

    Ok(claims.user.username)
}

fn extract_token_info(token: &str) -> Result<(String, String, String)> {
    let parts: Vec<&str> = token.split('.').collect();
    if parts.len() != 3 {
        anyhow::bail!("Invalid token format");
    }

    let payload = parts[1];
    let decoded = general_purpose::URL_SAFE_NO_PAD
        .decode(payload)
        .context("Failed to decode token")?;
    let claims: JwtClaims =
        serde_json::from_slice(&decoded).context("Failed to parse token claims")?;

    let created_at = chrono::DateTime::from_timestamp(claims.iat as i64, 0)
        .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
        .unwrap_or_else(|| "Unknown".to_string());

    Ok((claims.user.username, claims.user.name, created_at))
}

fn get_session_name() -> String {
    hostname::get()
        .ok()
        .and_then(|name| name.into_string().ok())
        .unwrap_or_else(|| "Unknown Session".to_string())
}

async fn request_authentication(
    request_id: &str,
    env_config: &EnvironmentConfig,
) -> Result<AuthResponse> {
    let mut client_builder = reqwest::Client::builder();

    if env_config.should_accept_invalid_certs() {
        client_builder = client_builder.danger_accept_invalid_certs(true);
    }

    let client = client_builder.build()?;

    let auth_request = AuthRequest {
        request_id: request_id.to_string(),
        session_name: get_session_name(),
    };

    let auth_service_url = env_config.auth_sessions_url();
    let response = client
        .post(&format!("{}/request", auth_service_url))
        .json(&auth_request)
        .send()
        .await
        .context("Failed to request authentication")?;

    if !response.status().is_success() {
        anyhow::bail!("Authentication request failed: {}", response.status());
    }

    response
        .json()
        .await
        .context("Failed to parse authentication response")
}

async fn poll_for_token(
    request_id: &str,
    env_config: &EnvironmentConfig,
) -> Result<Option<String>> {
    let mut client_builder = reqwest::Client::builder();

    if env_config.should_accept_invalid_certs() {
        client_builder = client_builder.danger_accept_invalid_certs(true);
    }

    let client = client_builder.build()?;

    let auth_service_url = env_config.auth_sessions_url();
    let response = client
        .get(&format!("{}/token/{}", auth_service_url, request_id))
        .send()
        .await
        .context("Failed to check authentication status")?;

    // 404 = request not found (error)
    if response.status() == 404 {
        anyhow::bail!("Authentication request not found");
    }

    // 403 = request not authenticated yet (keep polling)
    if response.status() == 403 {
        return Ok(None);
    }

    // Any other error status
    if !response.status().is_success() {
        anyhow::bail!("Token check failed: {}", response.status());
    }

    // 200 = success, we have a token
    let token_response: TokenResponse = response
        .json()
        .await
        .context("Failed to parse token response")?;

    Ok(Some(token_response.token))
}

pub async fn logout(token: &str, profile_name: Option<&str>) -> Result<()> {
    let env_config = EnvironmentConfig::from_env();
    let logout_url = env_config.auth_logout_url();

    let jar = reqwest::cookie::Jar::default();
    let url = logout_url.parse::<reqwest::Url>()?;
    jar.add_cookie_str(&format!("jwtToken={}", token), &url);

    let mut client_builder = reqwest::Client::builder().cookie_provider(std::sync::Arc::new(jar));

    if env_config.should_accept_invalid_certs() {
        client_builder = client_builder.danger_accept_invalid_certs(true);
    }

    let client = client_builder.build()?;

    let response = client
        .post(&logout_url)
        .header(reqwest::header::CONTENT_TYPE, "application/json")
        .send()
        .await
        .context("Failed to logout")?;

    if !response.status().is_success() {
        let status = response.status();
        let error_body = response
            .text()
            .await
            .unwrap_or_else(|_| "Unable to read error".to_string());
        anyhow::bail!("Failed to logout: {} - {}", status, error_body);
    }

    // Delete from profiles if profile name is provided
    if let Some(profile) = profile_name {
        let mut profiles = load_profiles()?;
        profiles.profiles.remove(profile);

        // If we're removing the active profile, switch to another one or default
        if profiles.active_profile == profile {
            profiles.active_profile = profiles
                .profiles
                .keys()
                .next()
                .map(|s| s.to_string())
                .unwrap_or_else(|| "default".to_string());
        }

        save_profiles(&profiles)?;
    } else {
        // Delete local token file for backward compatibility
        let token_path = get_token_path()?;
        if token_path.exists() {
            fs::remove_file(&token_path).context("Failed to delete local token")?;
        }
    }

    Ok(())
}

pub async fn logout_session(token: &str, session_id: &str) -> Result<()> {
    let env_config = EnvironmentConfig::from_env();
    let auth_service_url = env_config.auth_sessions_url();

    let jar = reqwest::cookie::Jar::default();
    let url = format!("{}/tokens/{}", auth_service_url, session_id).parse::<reqwest::Url>()?;
    jar.add_cookie_str(&format!("jwtToken={}", token), &url);

    let mut client_builder = reqwest::Client::builder().cookie_provider(std::sync::Arc::new(jar));

    if env_config.should_accept_invalid_certs() {
        client_builder = client_builder.danger_accept_invalid_certs(true);
    }

    let client = client_builder.build()?;

    let response = client
        .delete(&format!("{}/tokens/{}", auth_service_url, session_id))
        .send()
        .await
        .context("Failed to logout session")?;

    if !response.status().is_success() {
        let status = response.status();
        let error_body = response
            .text()
            .await
            .unwrap_or_else(|_| "Unable to read error".to_string());
        anyhow::bail!("Failed to logout session: {} - {}", status, error_body);
    }

    Ok(())
}

pub async fn list_auth_tokens(token: &str) -> Result<Vec<UserToken>> {
    let env_config = EnvironmentConfig::from_env();
    let auth_service_url = env_config.auth_sessions_url();

    let jar = reqwest::cookie::Jar::default();
    let url = format!("{}/tokens", auth_service_url).parse::<reqwest::Url>()?;
    jar.add_cookie_str(&format!("jwtToken={}", token), &url);

    let mut client_builder = reqwest::Client::builder().cookie_provider(std::sync::Arc::new(jar));

    if env_config.should_accept_invalid_certs() {
        client_builder = client_builder.danger_accept_invalid_certs(true);
    }

    let client = client_builder.build()?;

    let response = client
        .get(&format!("{}/tokens", auth_service_url))
        .send()
        .await
        .context("Failed to fetch auth tokens")?;

    if !response.status().is_success() {
        let status = response.status();
        let error_body = response
            .text()
            .await
            .unwrap_or_else(|_| "Unable to read error".to_string());
        anyhow::bail!("Failed to list auth tokens: {} - {}", status, error_body);
    }

    let tokens: Vec<UserToken> = response
        .json()
        .await
        .context("Failed to parse auth tokens response")?;

    Ok(tokens)
}
pub async fn authenticate(profile_name: Option<&str>) -> Result<String> {
    let env_config = EnvironmentConfig::from_env();

    println!("Starting authentication flow...");
    println!("Environment: {}", env_config.environment);
    println!();

    let request_id = Uuid::new_v4().to_string();

    let auth_response = request_authentication(&request_id, &env_config).await?;

    println!("Please open the following URL in your browser to authenticate:");
    println!();
    println!("    {}", auth_response.auth_url);
    println!();
    println!(
        "Waiting for authentication (timeout: {} minutes)...",
        MAX_POLL_ATTEMPTS * POLL_INTERVAL_MS as u32 / 60000
    );

    let mut attempts = 0;
    loop {
        if attempts >= MAX_POLL_ATTEMPTS {
            println!();
            anyhow::bail!("Authentication timeout. Please try again.");
        }

        tokio::time::sleep(Duration::from_millis(POLL_INTERVAL_MS)).await;

        match poll_for_token(&request_id, &env_config).await {
            Ok(Some(token)) => {
                if let Some(profile) = profile_name {
                    save_token_to_profile(&token, profile)?;
                } else {
                    save_token(&token)?;
                }
                println!();
                return Ok(token);
            }
            Ok(None) => {
                // Still waiting for authentication
                print!(".");
                io::stdout().flush()?;
                attempts += 1;
            }
            Err(e) => {
                // Fatal error (e.g., request not found)
                println!();
                return Err(e);
            }
        }
    }
}

#[allow(dead_code)]
pub async fn get_or_authenticate() -> Result<String> {
    if let Some(token) = load_token()? {
        return Ok(token);
    }

    authenticate(None).await
}

pub async fn get_or_authenticate_with_override(profile_override: Option<&str>) -> Result<String> {
    if let Some(token) = load_token_with_override(profile_override)? {
        return Ok(token);
    }

    if let Some(profile_name) = profile_override {
        anyhow::bail!(
            "Profile '{}' not found. Please login with: doqium-cli auth login --profile {}",
            profile_name,
            profile_name
        );
    }

    authenticate(None).await
}

pub fn get_token_details(token: &str) -> Option<(String, String, String)> {
    extract_token_info(token).ok()
}

pub fn get_active_profile() -> Result<String> {
    let profiles = load_profiles()?;
    Ok(profiles.active_profile)
}

pub fn get_active_profile_with_override(profile_override: Option<&str>) -> Result<String> {
    if let Some(profile) = profile_override {
        Ok(profile.to_string())
    } else {
        get_active_profile()
    }
}

pub fn switch_profile(profile_name: &str) -> Result<()> {
    let mut profiles = load_profiles()?;

    if !profiles.profiles.contains_key(profile_name) {
        anyhow::bail!("Profile '{}' not found", profile_name);
    }

    profiles.active_profile = profile_name.to_string();
    save_profiles(&profiles)?;
    Ok(())
}

pub fn list_profiles() -> Result<Vec<(String, ProfileInfo, bool)>> {
    let profiles = load_profiles()?;
    let mut result: Vec<(String, ProfileInfo, bool)> = profiles
        .profiles
        .iter()
        .map(|(name, info)| (name.clone(), info.clone(), name == &profiles.active_profile))
        .collect();

    result.sort_by(|a, b| a.0.cmp(&b.0));
    Ok(result)
}

pub fn delete_profile(profile_name: &str) -> Result<()> {
    let mut profiles = load_profiles()?;

    if !profiles.profiles.contains_key(profile_name) {
        anyhow::bail!("Profile '{}' not found", profile_name);
    }

    profiles.profiles.remove(profile_name);

    // If we're removing the active profile, switch to another one or default
    if profiles.active_profile == profile_name {
        profiles.active_profile = profiles
            .profiles
            .keys()
            .next()
            .map(|s| s.to_string())
            .unwrap_or_else(|| "default".to_string());
    }

    save_profiles(&profiles)?;
    Ok(())
}

pub fn is_admin_token(token: &str) -> bool {
    // Decode JWT without verification (we only need to check the administrator field)
    // JWT format is: header.payload.signature
    // We only need the payload (claims) part

    let parts: Vec<&str> = token.split('.').collect();
    if parts.len() != 3 {
        return false;
    }

    // Decode the payload (second part)
    let payload = parts[1];

    // JWT uses base64url encoding, but we can try standard base64 with URL-safe alphabet
    match general_purpose::URL_SAFE_NO_PAD.decode(payload) {
        Ok(decoded) => {
            // Parse JSON
            match serde_json::from_slice::<JwtClaims>(&decoded) {
                Ok(claims) => claims.administrator,
                Err(_) => false,
            }
        }
        Err(_) => false,
    }
}
