use axum::http::{HeaderMap, header};
use loco_rs::prelude::*;
use serde::Serialize;
use std::{
    collections::HashMap,
    sync::{LazyLock, Mutex},
};
use uuid::Uuid;

const DEFAULT_ADMIN_USERNAME: &str = "admin";
const DEFAULT_ADMIN_PASSWORD: &str = "admin123";
const DEFAULT_SESSION_COOKIE_NAME: &str = "termi_admin_session";
const DEFAULT_SESSION_TTL_SECONDS: i64 = 43_200;

#[derive(Clone, Debug, Serialize)]
pub struct AdminIdentity {
    pub username: String,
    pub email: Option<String>,
    pub source: String,
    pub provider: Option<String>,
    pub groups: Vec<String>,
}

#[derive(Clone, Debug)]
struct LocalSessionRecord {
    username: String,
    email: Option<String>,
    expires_at: chrono::DateTime<chrono::Utc>,
}

static ADMIN_SESSIONS: LazyLock<Mutex<HashMap<String, LocalSessionRecord>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

fn env_flag(name: &str, default: bool) -> bool {
    std::env::var(name)
        .ok()
        .map(|value| match value.trim().to_ascii_lowercase().as_str() {
            "1" | "true" | "yes" | "on" => true,
            "0" | "false" | "no" | "off" => false,
            _ => default,
        })
        .unwrap_or(default)
}

fn session_cookie_name() -> String {
    std::env::var("TERMI_ADMIN_SESSION_COOKIE")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| DEFAULT_SESSION_COOKIE_NAME.to_string())
}

fn proxy_shared_secret() -> Option<String> {
    std::env::var("TERMI_ADMIN_PROXY_SHARED_SECRET")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn session_ttl_seconds() -> i64 {
    std::env::var("TERMI_ADMIN_SESSION_TTL_SECONDS")
        .ok()
        .and_then(|value| value.trim().parse::<i64>().ok())
        .filter(|value| *value > 0)
        .unwrap_or(DEFAULT_SESSION_TTL_SECONDS)
}

fn header_value(headers: &HeaderMap, key: &'static str) -> Option<String> {
    headers
        .get(key)
        .and_then(|value| value.to_str().ok())
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
}

fn split_groups(value: Option<String>) -> Vec<String> {
    value
        .unwrap_or_default()
        .split([',', ';', ' '])
        .map(str::trim)
        .filter(|item| !item.is_empty())
        .map(ToString::to_string)
        .collect()
}

fn cookie_value(headers: &HeaderMap, name: &str) -> Option<String> {
    headers
        .get(header::COOKIE)
        .and_then(|value| value.to_str().ok())
        .and_then(|raw| {
            raw.split(';').find_map(|part| {
                let (key, value) = part.trim().split_once('=')?;
                (key == name).then(|| value.trim().to_string())
            })
        })
}

fn resolve_proxy_identity(headers: &HeaderMap) -> Option<AdminIdentity> {
    if !proxy_auth_enabled() {
        return None;
    }

    if let Some(expected_secret) = proxy_shared_secret() {
        let provided_secret = header_value(headers, "X-Termi-Proxy-Secret")?;
        if provided_secret != expected_secret {
            tracing::warn!("proxy auth secret mismatch, ignoring forwarded admin identity headers");
            return None;
        }
    }

    let username = [
        header_value(headers, "Remote-User"),
        header_value(headers, "X-Forwarded-User"),
        header_value(headers, "X-Auth-Request-User"),
    ]
    .into_iter()
    .flatten()
    .find(|value| !value.is_empty())?;

    let email = [
        header_value(headers, "Remote-Email"),
        header_value(headers, "X-Forwarded-Email"),
        header_value(headers, "X-Auth-Request-Email"),
    ]
    .into_iter()
    .flatten()
    .find(|value| !value.is_empty());

    let provider = if header_value(headers, "Remote-User").is_some() {
        Some("TinyAuth".to_string())
    } else {
        Some("Proxy SSO".to_string())
    };

    Some(AdminIdentity {
        username,
        email,
        source: "proxy".to_string(),
        provider,
        groups: split_groups(
            header_value(headers, "Remote-Groups")
                .or_else(|| header_value(headers, "X-Forwarded-Groups")),
        ),
    })
}

fn resolve_local_identity(headers: &HeaderMap) -> Option<AdminIdentity> {
    let token = cookie_value(headers, &session_cookie_name())?;
    let now = chrono::Utc::now();
    let mut guard = ADMIN_SESSIONS.lock().ok()?;

    let session = match guard.get(&token) {
        Some(session) if session.expires_at > now => session.clone(),
        Some(_) => {
            guard.remove(&token);
            return None;
        }
        None => return None,
    };

    Some(AdminIdentity {
        username: session.username,
        email: session.email,
        source: "local".to_string(),
        provider: Some("Built-in admin session".to_string()),
        groups: Vec::new(),
    })
}

pub(crate) fn admin_username() -> String {
    std::env::var("TERMI_ADMIN_USERNAME").unwrap_or_else(|_| DEFAULT_ADMIN_USERNAME.to_string())
}

pub(crate) fn admin_password() -> String {
    std::env::var("TERMI_ADMIN_PASSWORD").unwrap_or_else(|_| DEFAULT_ADMIN_PASSWORD.to_string())
}

pub(crate) fn proxy_auth_enabled() -> bool {
    env_flag("TERMI_ADMIN_TRUST_PROXY_AUTH", false)
}

pub(crate) fn local_login_enabled() -> bool {
    env_flag("TERMI_ADMIN_LOCAL_LOGIN_ENABLED", true)
}

pub(crate) fn validate_admin_credentials(username: &str, password: &str) -> bool {
    username == admin_username() && password == admin_password()
}

pub(crate) fn resolve_admin_identity(headers: &HeaderMap) -> Option<AdminIdentity> {
    resolve_proxy_identity(headers).or_else(|| resolve_local_identity(headers))
}

pub(crate) fn check_auth(headers: &HeaderMap) -> Result<AdminIdentity> {
    resolve_admin_identity(headers).ok_or_else(|| Error::Unauthorized("Not logged in".to_string()))
}

pub(crate) fn start_local_session(username: &str) -> (AdminIdentity, String, String) {
    let token = Uuid::new_v4().to_string();
    let expires_at = chrono::Utc::now() + chrono::Duration::seconds(session_ttl_seconds());

    let record = LocalSessionRecord {
        username: username.to_string(),
        email: None,
        expires_at,
    };

    if let Ok(mut sessions) = ADMIN_SESSIONS.lock() {
        sessions.insert(token.clone(), record);
    }

    let cookie = format!(
        "{}={}; Path=/; HttpOnly; SameSite=Lax; Max-Age={}",
        session_cookie_name(),
        token,
        session_ttl_seconds()
    );

    (
        AdminIdentity {
            username: username.to_string(),
            email: None,
            source: "local".to_string(),
            provider: Some("Built-in admin session".to_string()),
            groups: Vec::new(),
        },
        token,
        cookie,
    )
}

pub(crate) fn clear_local_session(headers: &HeaderMap) {
    let Some(token) = cookie_value(headers, &session_cookie_name()) else {
        return;
    };

    if let Ok(mut sessions) = ADMIN_SESSIONS.lock() {
        sessions.remove(&token);
    }
}

pub(crate) fn clear_local_session_cookie() -> String {
    format!(
        "{}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
        session_cookie_name()
    )
}
