// OAuth2 PKCE flow for Compte Maximus (Logto) integration.
//
// Architecture:
// - The desktop app is registered as a "Native App" in Logto (public
//   client, no secret).
// - OAuth2 Authorization Code + PKCE flow via the system browser.
// - Deep-link callback: simpl-resultat://auth/callback?code=...
// - Tokens are persisted through `token_store` which prefers the OS
//   keychain (Credential Manager / Secret Service) and falls back to a
//   restricted file only when no prior keychain success has been
//   recorded. See `token_store.rs` for details.
//
// The PKCE verifier is held in memory via Tauri managed state, so it
// cannot be intercepted by another process. It is cleared after the
// callback exchange.

use serde::{Deserialize, Serialize};
use std::fs;
use std::sync::Mutex;
use tauri::Manager;

use super::account_cache;
use super::token_store::{
    self, auth_dir, chrono_now, write_restricted, StoredTokens,
};

// Logto endpoint — overridable via env var for development.
fn logto_endpoint() -> String {
    std::env::var("LOGTO_ENDPOINT")
        .unwrap_or_else(|_| "https://auth.lacompagniemaximus.com".to_string())
}

// Logto app ID for the desktop native app.
fn logto_app_id() -> String {
    std::env::var("LOGTO_APP_ID").unwrap_or_else(|_| "sr-desktop-native".to_string())
}

const REDIRECT_URI: &str = "simpl-resultat://auth/callback";
const LAST_CHECK_FILE: &str = "last_check";
const CHECK_INTERVAL_SECS: i64 = 86400; // 24 hours

/// PKCE state held in memory during the OAuth2 flow.
pub struct OAuthState {
    pub code_verifier: Mutex<Option<String>>,
}

/// Account info exposed to the frontend.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountInfo {
    pub email: String,
    pub name: Option<String>,
    pub picture: Option<String>,
    pub subscription_status: Option<String>,
}

fn generate_pkce() -> (String, String) {
    use rand::Rng;
    let mut rng = rand::thread_rng();
    let verifier: String = (0..64)
        .map(|_| {
            let idx = rng.gen_range(0..62);
            let c = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[idx];
            c as char
        })
        .collect();

    use sha2::{Digest, Sha256};
    let hash = Sha256::digest(verifier.as_bytes());
    let challenge = base64_url_encode(&hash);

    (verifier, challenge)
}

fn base64_url_encode(data: &[u8]) -> String {
    use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
    URL_SAFE_NO_PAD.encode(data)
}

/// Start the OAuth2 PKCE flow. Generates a code verifier/challenge, stores the verifier
/// in memory, and returns the authorization URL to open in the system browser.
#[tauri::command]
pub fn start_oauth(app: tauri::AppHandle) -> Result<String, String> {
    let (verifier, challenge) = generate_pkce();

    // Store verifier in managed state
    let state = app.state::<OAuthState>();
    *state.code_verifier.lock().map_err(|e| format!("Mutex poisoned: {}", e))? = Some(verifier);

    let endpoint = logto_endpoint();
    let client_id = logto_app_id();

    let url = format!(
        "{}/oidc/auth?client_id={}&redirect_uri={}&response_type=code&code_challenge={}&code_challenge_method=S256&scope=openid%20profile%20email%20offline_access",
        endpoint,
        urlencoding::encode(&client_id),
        urlencoding::encode(REDIRECT_URI),
        urlencoding::encode(&challenge),
    );

    Ok(url)
}

/// Exchange the authorization code for tokens. Called from the deep-link callback handler.
#[tauri::command]
pub async fn handle_auth_callback(app: tauri::AppHandle, code: String) -> Result<AccountInfo, String> {
    let verifier = {
        let state = app.state::<OAuthState>();
        let verifier = state.code_verifier.lock().map_err(|e| format!("Mutex poisoned: {}", e))?.take();
        verifier.ok_or("No pending OAuth flow (verifier missing)")?
    };

    let endpoint = logto_endpoint();
    let client_id = logto_app_id();

    let client = reqwest::Client::new();
    let resp = client
        .post(format!("{}/oidc/token", endpoint))
        .form(&[
            ("grant_type", "authorization_code"),
            ("client_id", &client_id),
            ("redirect_uri", REDIRECT_URI),
            ("code", &code),
            ("code_verifier", &verifier),
        ])
        .timeout(std::time::Duration::from_secs(15))
        .send()
        .await
        .map_err(|e| format!("Token exchange failed: {}", e))?;

    if !resp.status().is_success() {
        let body = resp.text().await.unwrap_or_default();
        return Err(format!("Token exchange error: {}", body));
    }

    let token_resp: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| format!("Invalid token response: {}", e))?;

    let access_token = token_resp["access_token"]
        .as_str()
        .ok_or("Missing access_token")?
        .to_string();
    let refresh_token = token_resp["refresh_token"].as_str().map(|s| s.to_string());
    let id_token = token_resp["id_token"].as_str().map(|s| s.to_string());
    let expires_in = token_resp["expires_in"].as_i64().unwrap_or(3600);
    let expires_at = chrono_now() + expires_in;

    // Persist tokens through token_store (prefers keychain over file).
    let tokens = StoredTokens {
        access_token: access_token.clone(),
        refresh_token,
        id_token,
        expires_at,
    };
    token_store::save(&app, &tokens)?;

    // Fetch user info
    let account = fetch_userinfo(&endpoint, &access_token).await?;

    // Store account info with an HMAC signature so the license gating
    // path can trust the cached subscription_status without re-calling
    // Logto on every entitlement check.
    account_cache::save(&app, &account)?;

    Ok(account)
}

/// Refresh the access token using the stored refresh token.
#[tauri::command]
pub async fn refresh_auth_token(app: tauri::AppHandle) -> Result<AccountInfo, String> {
    let tokens = token_store::load(&app)?.ok_or_else(|| "Not authenticated".to_string())?;

    let refresh_token = tokens
        .refresh_token
        .ok_or("No refresh token available")?;

    let endpoint = logto_endpoint();
    let client_id = logto_app_id();

    let client = reqwest::Client::new();
    let resp = client
        .post(format!("{}/oidc/token", endpoint))
        .form(&[
            ("grant_type", "refresh_token"),
            ("client_id", &client_id),
            ("refresh_token", &refresh_token),
        ])
        .timeout(std::time::Duration::from_secs(15))
        .send()
        .await
        .map_err(|e| format!("Token refresh failed: {}", e))?;

    if !resp.status().is_success() {
        // Clear stored tokens on refresh failure.
        let _ = token_store::delete(&app);
        let _ = account_cache::delete(&app);
        return Err("Session expired, please sign in again".to_string());
    }

    let token_resp: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| format!("Invalid token response: {}", e))?;

    let new_access = token_resp["access_token"]
        .as_str()
        .ok_or("Missing access_token")?
        .to_string();
    let new_refresh = token_resp["refresh_token"].as_str().map(|s| s.to_string());
    let expires_in = token_resp["expires_in"].as_i64().unwrap_or(3600);

    let new_tokens = StoredTokens {
        access_token: new_access.clone(),
        refresh_token: new_refresh.or(Some(refresh_token)),
        id_token: token_resp["id_token"].as_str().map(|s| s.to_string()),
        expires_at: chrono_now() + expires_in,
    };

    token_store::save(&app, &new_tokens)?;

    let account = fetch_userinfo(&endpoint, &new_access).await?;
    account_cache::save(&app, &account)?;

    Ok(account)
}

/// Read cached account info without network call. Used for UI display
/// only — accepts both signed (v0.8+) and legacy (v0.7.x) payloads so
/// upgraded users still see their name/email immediately. The license
/// gating path uses `account_cache::load_verified` instead.
#[tauri::command]
pub fn get_account_info(app: tauri::AppHandle) -> Result<Option<AccountInfo>, String> {
    account_cache::load_unverified(&app)
}

/// Log out: clear all stored tokens and account info, including the
/// HMAC key so the next session starts with a fresh cryptographic
/// anchor.
#[tauri::command]
pub fn logout(app: tauri::AppHandle) -> Result<(), String> {
    token_store::delete(&app)?;
    account_cache::delete(&app)?;
    Ok(())
}

/// Check subscription status if the last check was more than 24h ago.
/// Returns the refreshed account info, or the cached info if no check was needed.
/// Graceful: returns Ok(None) if not authenticated, silently skips on network errors.
#[tauri::command]
pub async fn check_subscription_status(
    app: tauri::AppHandle,
) -> Result<Option<AccountInfo>, String> {
    // Not authenticated — nothing to check. This also triggers migration
    // from a legacy tokens.json file into the keychain when present,
    // because token_store::load() performs the migration eagerly.
    if token_store::load(&app)?.is_none() {
        return Ok(None);
    }

    let dir = auth_dir(&app)?;
    let last_check_path = dir.join(LAST_CHECK_FILE);
    let now = chrono_now();

    // Check if we need to verify (more than 24h since last check)
    if last_check_path.exists() {
        if let Ok(raw) = fs::read_to_string(&last_check_path) {
            if let Ok(ts) = raw.trim().parse::<i64>() {
                if now - ts < CHECK_INTERVAL_SECS {
                    // Recent check — return cached account info
                    return get_account_info(app);
                }
            }
        }
    }

    // Try to refresh the token to get fresh subscription status
    match refresh_auth_token(app.clone()).await {
        Ok(account) => {
            // Update last check timestamp
            let _ = write_restricted(&last_check_path, &now.to_string());
            Ok(Some(account))
        }
        Err(_) => {
            // Network error or expired session — graceful degradation.
            // Still update the timestamp to avoid hammering on every launch.
            let _ = write_restricted(&last_check_path, &now.to_string());
            get_account_info(app)
        }
    }
}

async fn fetch_userinfo(endpoint: &str, access_token: &str) -> Result<AccountInfo, String> {
    let client = reqwest::Client::new();
    let resp = client
        .get(format!("{}/oidc/me", endpoint))
        .bearer_auth(access_token)
        .timeout(std::time::Duration::from_secs(10))
        .send()
        .await
        .map_err(|e| format!("Userinfo fetch failed: {}", e))?;

    if !resp.status().is_success() {
        return Err("Cannot fetch user info".to_string());
    }

    let info: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| format!("Invalid userinfo response: {}", e))?;

    Ok(AccountInfo {
        email: info["email"]
            .as_str()
            .unwrap_or_default()
            .to_string(),
        name: info["name"].as_str().map(|s| s.to_string()),
        picture: info["picture"].as_str().map(|s| s.to_string()),
        subscription_status: info["custom_data"]["subscription_status"]
            .as_str()
            .map(|s| s.to_string()),
    })
}
