use std::sync::Arc;

use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::http::header;
use rand::Rng;

use crate::state::AppState;

pub struct Session {
    pub token: String,
    pub display_name: Option<String>,
    pub is_new: bool,
    pub cookie_max_age: u64,
}

impl Session {
    pub fn cookie_header(&self) -> String {
        let secure = std::env::var("SECURE_COOKIES").as_deref() == Ok("true");
        let same_site = if secure { "Strict" } else { "Lax" };
        let secure_flag = if secure { "; Secure" } else { "" };

        if self.cookie_max_age == 0 {
            format!("cruciverb_session={}; Path=/; HttpOnly; SameSite={}{}", self.token, same_site, secure_flag)
        } else {
            format!(
                "cruciverb_session={}; Path=/; HttpOnly; SameSite={}; Max-Age={}{}",
                self.token, same_site, self.cookie_max_age, secure_flag
            )
        }
    }
}

impl FromRequestParts<Arc<AppState>> for Session {
    type Rejection = std::convert::Infallible;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &Arc<AppState>,
    ) -> Result<Self, Self::Rejection> {
        let token = parts
            .headers
            .get(header::COOKIE)
            .and_then(|v| v.to_str().ok())
            .and_then(|cookies| {
                cookies.split(';')
                    .map(|c| c.trim())
                    .find(|c| c.starts_with("cruciverb_session="))
                    .map(|c| c["cruciverb_session=".len()..].to_string())
            });

        let user_agent_hash = parts
            .headers
            .get(header::USER_AGENT)
            .and_then(|v| v.to_str().ok())
            .map(ua_hash)
            .unwrap_or_default();

        let dbi = state.db.lock().await;
        let (mut token, mut name, mut is_new) = match token {
            Some(t) if !t.is_empty() => {
                let (token, name) = crate::db::get_or_create_session(&dbi.db, &t);
                (token, name, false)
            }
            _ => {
                let new_token = generate_token();
                let (token, name) = crate::db::get_or_create_session(&dbi.db, &new_token);
                (token, name, true)
            }
        };

        // session binding: when enabled (and a passkey exists), the cookie only
        // works from the user agent it was bound to; a mismatch rotates to a
        // fresh session instead of letting a stolen cookie ride along
        let mut settings_json = crate::db::get_settings(&dbi.db, &token);
        {
            let settings_value: serde_json::Value =
                serde_json::from_str(&settings_json).unwrap_or_default();
            let binding_enabled = settings_value
                .get("sessionBinding")
                .and_then(|v| v.as_bool())
                .unwrap_or(false)
                && crate::db::has_passkey(&dbi.db, &token);
            if binding_enabled && !user_agent_hash.is_empty() {
                let stored = crate::db::get_user_agent_hash(&dbi.db, &token);
                match stored {
                    None => crate::db::set_user_agent_hash(&dbi.db, &token, &user_agent_hash),
                    Some(s) if s != user_agent_hash => {
                        let new_token = generate_token();
                        let (t2, n2) = crate::db::get_or_create_session(&dbi.db, &new_token);
                        crate::db::set_user_agent_hash(&dbi.db, &t2, &user_agent_hash);
                        token = t2;
                        name = n2;
                        is_new = true;
                        settings_json = crate::db::get_settings(&dbi.db, &token);
                    }
                    _ => {}
                }
            }
        }

        // read cookie lifetime from user settings (default 30 days)
        let cookie_max_age = serde_json::from_str::<serde_json::Value>(&settings_json)
            .ok()
            .and_then(|v| v.get("cookieLifetime")?.as_u64())
            .unwrap_or(2_592_000);

        Ok(Session { token, display_name: name, is_new, cookie_max_age })
    }
}

fn generate_token() -> String {
    let mut rng = rand::rng();
    let bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
    bytes.iter().map(|b| format!("{:02x}", b)).collect()
}

fn ua_hash(ua: &str) -> String {
    use sha2::{Digest, Sha256};
    hex::encode(Sha256::digest(ua.as_bytes()))
}
