use serde::Deserialize;
use tauri::State;

use crate::app_error::AppCommandError;
use crate::db::service::app_metadata_service;
use crate::db::AppDatabase;
use crate::models::{
    GitDetectResult, GitHubAccountsSettings, GitHubTokenValidation, GitSettings,
};

const GIT_SETTINGS_KEY: &str = "git_settings";
const GITHUB_ACCOUNTS_KEY: &str = "github_accounts";

// ---------------------------------------------------------------------------
// Git detection
// ---------------------------------------------------------------------------

async fn run_git_version(git_path: &str) -> Result<GitDetectResult, AppCommandError> {
    let output = crate::process::tokio_command(git_path)
        .arg("--version")
        .output()
        .await
        .map_err(|_| {
            AppCommandError::not_found(format!("Cannot execute git at: {git_path}"))
        })?;

    if !output.status.success() {
        return Ok(GitDetectResult {
            installed: false,
            version: None,
            path: Some(git_path.to_string()),
        });
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let version = stdout
        .trim()
        .strip_prefix("git version ")
        .unwrap_or(stdout.trim())
        .to_string();

    Ok(GitDetectResult {
        installed: true,
        version: Some(version),
        path: Some(git_path.to_string()),
    })
}

async fn detect_git_path() -> Option<String> {
    let which_cmd = if cfg!(target_os = "windows") {
        "where"
    } else {
        "which"
    };

    let output = crate::process::tokio_command(which_cmd)
        .arg("git")
        .output()
        .await
        .ok()?;

    if !output.status.success() {
        return None;
    }

    let path = String::from_utf8_lossy(&output.stdout)
        .lines()
        .next()?
        .trim()
        .to_string();

    if path.is_empty() {
        None
    } else {
        Some(path)
    }
}

pub(crate) async fn detect_git_core(
    conn: &sea_orm::DatabaseConnection,
) -> Result<GitDetectResult, AppCommandError> {
    let settings = load_git_settings(conn).await?;

    if let Some(custom) = &settings.custom_path {
        let trimmed = custom.trim();
        if !trimmed.is_empty() {
            return run_git_version(trimmed).await;
        }
    }

    if let Some(path) = detect_git_path().await {
        return run_git_version(&path).await;
    }

    match run_git_version("git").await {
        Ok(result) if result.installed => Ok(result),
        _ => Ok(GitDetectResult {
            installed: false,
            version: None,
            path: None,
        }),
    }
}

#[tauri::command]
pub async fn detect_git(
    db: State<'_, AppDatabase>,
) -> Result<GitDetectResult, AppCommandError> {
    detect_git_core(&db.conn).await
}

#[tauri::command]
pub async fn test_git_path(path: String) -> Result<GitDetectResult, AppCommandError> {
    let trimmed = path.trim();
    if trimmed.is_empty() {
        return Err(AppCommandError::invalid_input("Git path cannot be empty"));
    }
    run_git_version(trimmed).await
}

// ---------------------------------------------------------------------------
// Git settings
// ---------------------------------------------------------------------------

async fn load_git_settings(
    conn: &sea_orm::DatabaseConnection,
) -> Result<GitSettings, AppCommandError> {
    let raw = app_metadata_service::get_value(conn, GIT_SETTINGS_KEY)
        .await
        .map_err(AppCommandError::from)?;

    match raw {
        Some(raw) => serde_json::from_str::<GitSettings>(&raw).map_err(|e| {
            AppCommandError::configuration_invalid("Failed to parse stored git settings")
                .with_detail(e.to_string())
        }),
        None => Ok(GitSettings::default()),
    }
}

#[tauri::command]
pub async fn get_git_settings(
    db: State<'_, AppDatabase>,
) -> Result<GitSettings, AppCommandError> {
    load_git_settings(&db.conn).await
}

#[tauri::command]
pub async fn update_git_settings(
    settings: GitSettings,
    db: State<'_, AppDatabase>,
) -> Result<GitSettings, AppCommandError> {
    let serialized = serde_json::to_string(&settings).map_err(|e| {
        AppCommandError::invalid_input("Failed to serialize git settings")
            .with_detail(e.to_string())
    })?;

    app_metadata_service::upsert_value(&db.conn, GIT_SETTINGS_KEY, &serialized)
        .await
        .map_err(AppCommandError::from)?;

    Ok(settings)
}

// ---------------------------------------------------------------------------
// GitHub accounts
// ---------------------------------------------------------------------------

async fn load_github_accounts(
    conn: &sea_orm::DatabaseConnection,
) -> Result<GitHubAccountsSettings, AppCommandError> {
    let raw = app_metadata_service::get_value(conn, GITHUB_ACCOUNTS_KEY)
        .await
        .map_err(AppCommandError::from)?;

    match raw {
        Some(raw) => serde_json::from_str::<GitHubAccountsSettings>(&raw).map_err(|e| {
            AppCommandError::configuration_invalid("Failed to parse stored GitHub accounts")
                .with_detail(e.to_string())
        }),
        None => Ok(GitHubAccountsSettings::default()),
    }
}

#[tauri::command]
pub async fn get_github_accounts(
    db: State<'_, AppDatabase>,
) -> Result<GitHubAccountsSettings, AppCommandError> {
    load_github_accounts(&db.conn).await
}

#[tauri::command]
pub async fn update_github_accounts(
    settings: GitHubAccountsSettings,
    db: State<'_, AppDatabase>,
) -> Result<GitHubAccountsSettings, AppCommandError> {
    let serialized = serde_json::to_string(&settings).map_err(|e| {
        AppCommandError::invalid_input("Failed to serialize GitHub accounts")
            .with_detail(e.to_string())
    })?;

    app_metadata_service::upsert_value(&db.conn, GITHUB_ACCOUNTS_KEY, &serialized)
        .await
        .map_err(AppCommandError::from)?;

    Ok(settings)
}

// ---------------------------------------------------------------------------
// Keyring token management
// ---------------------------------------------------------------------------

#[tauri::command]
pub async fn save_account_token(
    account_id: String,
    token: String,
) -> Result<(), AppCommandError> {
    crate::keyring_store::set_token(&account_id, &token)
        .map_err(|e| AppCommandError::io_error("Failed to save token to keyring").with_detail(e))
}

#[tauri::command]
pub async fn get_account_token(
    account_id: String,
) -> Result<Option<String>, AppCommandError> {
    Ok(crate::keyring_store::get_token(&account_id))
}

#[tauri::command]
pub async fn delete_account_token(
    account_id: String,
) -> Result<(), AppCommandError> {
    crate::keyring_store::delete_token(&account_id)
        .map_err(|e| AppCommandError::io_error("Failed to delete token from keyring").with_detail(e))
}

// ---------------------------------------------------------------------------
// GitHub token validation
// ---------------------------------------------------------------------------

#[derive(Debug, Deserialize)]
struct GitHubUserResponse {
    login: String,
    avatar_url: Option<String>,
}

#[tauri::command]
pub async fn validate_github_token(
    server_url: String,
    token: String,
) -> Result<GitHubTokenValidation, AppCommandError> {
    let trimmed_url = server_url.trim().trim_end_matches('/');
    let trimmed_token = token.trim();

    if trimmed_token.is_empty() {
        return Err(AppCommandError::invalid_input("Token cannot be empty"));
    }

    // Build API URL: github.com uses api.github.com, enterprise uses {url}/api/v3
    let api_url = if trimmed_url.is_empty()
        || trimmed_url == "https://github.com"
        || trimmed_url == "http://github.com"
    {
        "https://api.github.com/user".to_string()
    } else {
        format!("{trimmed_url}/api/v3/user")
    };

    let response = reqwest::Client::new()
        .get(&api_url)
        .header("Authorization", format!("Bearer {trimmed_token}"))
        .header("User-Agent", "codeg")
        .header("Accept", "application/vnd.github+json")
        .send()
        .await
        .map_err(|e| AppCommandError::network("Failed to connect to GitHub API").with_detail(e.to_string()))?;

    if !response.status().is_success() {
        let status = response.status().as_u16();
        let body = response.text().await.unwrap_or_default();
        let message = if status == 401 {
            "Invalid or expired token".to_string()
        } else {
            format!("GitHub API returned status {status}: {body}")
        };
        return Ok(GitHubTokenValidation {
            success: false,
            username: None,
            scopes: vec![],
            avatar_url: None,
            message: Some(message),
        });
    }

    // Parse scopes from x-oauth-scopes header
    let scopes: Vec<String> = response
        .headers()
        .get("x-oauth-scopes")
        .and_then(|v| v.to_str().ok())
        .map(|s| {
            s.split(',')
                .map(|scope| scope.trim().to_string())
                .filter(|scope| !scope.is_empty())
                .collect()
        })
        .unwrap_or_default();

    let user = response
        .json::<GitHubUserResponse>()
        .await
        .map_err(|e| {
            AppCommandError::network("Failed to parse GitHub API response")
                .with_detail(e.to_string())
        })?;

    Ok(GitHubTokenValidation {
        success: true,
        username: Some(user.login),
        scopes,
        avatar_url: user.avatar_url,
        message: None,
    })
}
