use std::time::{SystemTime, UNIX_EPOCH};

use dashmap::DashMap;
use serde::Serialize;
use uuid::Uuid;

use crate::registry::Role;

pub const COOKIE_NAME: &str = "ontoref-session";
const SESSION_SECS: u64 = 30 * 24 * 3600;

#[derive(Clone)]
pub struct SessionEntry {
    /// Stable public identifier — safe to expose in list responses.
    /// Distinct from the bearer token so listing sessions cannot be used to
    /// steal credentials.
    pub id: String,
    pub slug: String,
    pub role: Role,
    /// Label of the key used to authenticate — empty for open/synthetic
    /// sessions.
    pub key_label: String,
    pub expires: u64,
}

/// Safe projection of a `SessionEntry` for list endpoints.
/// The bearer token is never included.
#[derive(Serialize, Clone)]
pub struct SessionView {
    pub id: String,
    pub slug: String,
    pub role: String,
    pub key_label: String,
    pub expires_at: u64,
}

impl From<&SessionEntry> for SessionView {
    fn from(e: &SessionEntry) -> Self {
        SessionView {
            id: e.id.clone(),
            slug: e.slug.clone(),
            role: match e.role {
                Role::Admin => "admin".to_string(),
                Role::Viewer => "viewer".to_string(),
            },
            key_label: e.key_label.clone(),
            expires_at: e.expires,
        }
    }
}

pub enum RevokeResult {
    Revoked,
    NotFound,
    /// The acting session's role/slug does not permit revoking this session.
    Forbidden,
}

pub struct SessionStore {
    /// Keyed by bearer token (UUID v4).
    sessions: DashMap<String, SessionEntry>,
    /// Secondary index: session_id → bearer token, for O(1) revoke-by-id.
    id_index: DashMap<String, String>,
}

impl Default for SessionStore {
    fn default() -> Self {
        Self::new()
    }
}

impl SessionStore {
    pub fn new() -> Self {
        Self {
            sessions: DashMap::new(),
            id_index: DashMap::new(),
        }
    }

    pub fn create(&self, slug: String, role: Role, key_label: String) -> String {
        let token = Uuid::new_v4().to_string();
        let id = Uuid::new_v4().to_string();
        let expires = now_secs() + SESSION_SECS;
        self.sessions.insert(
            token.clone(),
            SessionEntry {
                id: id.clone(),
                slug,
                role,
                key_label,
                expires,
            },
        );
        self.id_index.insert(id, token.clone());
        token
    }

    pub fn get(&self, token: &str) -> Option<SessionEntry> {
        let entry = self.sessions.get(token)?;
        if entry.expires < now_secs() {
            let id = entry.id.clone();
            drop(entry);
            self.sessions.remove(token);
            self.id_index.remove(&id);
            return None;
        }
        Some(entry.clone())
    }

    pub fn revoke(&self, token: &str) {
        if let Some((_, entry)) = self.sessions.remove(token) {
            self.id_index.remove(&entry.id);
        }
    }

    /// Revoke all active sessions for a project slug.
    ///
    /// Called on key rotation so in-flight sessions for the rotated project
    /// are immediately invalidated.
    pub fn revoke_all_for_slug(&self, slug: &str) -> usize {
        let now = now_secs();
        let tokens: Vec<String> = self
            .sessions
            .iter()
            .filter(|e| e.slug == slug && e.expires >= now)
            .map(|e| e.key().clone())
            .collect();
        let count = tokens.len();
        for token in &tokens {
            self.revoke(token);
        }
        count
    }

    /// Return active sessions scoped to a single project slug.
    pub fn list_for_slug(&self, slug: &str) -> Vec<SessionView> {
        let now = now_secs();
        self.sessions
            .iter()
            .filter(|e| e.slug == slug && e.expires >= now)
            .map(|e| SessionView::from(e.value()))
            .collect()
    }

    /// Return all active sessions across all projects.
    pub fn list_all(&self) -> Vec<SessionView> {
        let now = now_secs();
        self.sessions
            .iter()
            .filter(|e| e.expires >= now)
            .map(|e| SessionView::from(e.value()))
            .collect()
    }

    /// Revoke a session by its public `id`.
    ///
    /// Authorization rules:
    /// - Daemon admin (`acting_slug == "_daemon"`) may revoke any session.
    /// - Project admin (`acting_role == Admin`) may only revoke sessions
    ///   belonging to their own project.
    /// - All other callers receive `Forbidden`.
    pub fn revoke_by_id(
        &self,
        session_id: &str,
        acting_slug: &str,
        acting_role: &Role,
    ) -> RevokeResult {
        let Some(token_ref) = self.id_index.get(session_id) else {
            return RevokeResult::NotFound;
        };
        let token = token_ref.clone();
        drop(token_ref);

        let Some(entry) = self.sessions.get(&token) else {
            self.id_index.remove(session_id);
            return RevokeResult::NotFound;
        };

        let authorized = acting_slug == "_daemon"
            || (matches!(acting_role, Role::Admin) && entry.slug == acting_slug);

        if !authorized {
            return RevokeResult::Forbidden;
        }

        drop(entry);
        self.revoke(&token);
        RevokeResult::Revoked
    }
}

/// Extract the value of a named cookie from a raw `Cookie:` header string.
pub fn extract_cookie(header: &str, name: &str) -> Option<String> {
    for part in header.split(';') {
        if let Some((k, v)) = part.trim().split_once('=') {
            if k.trim() == name {
                return Some(v.trim().to_string());
            }
        }
    }
    None
}

fn now_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}
