import uuid
import time
from dataclasses import dataclass, field

import duckdb
import pyarrow as pa
from fastapi import Request
from starlette.applications import Starlette

SESSION_COOKIE = "n6k_session"
SESSION_HEADER = "x-n6k-session"
SESSION_TTL = 3600  # 1 hour — fallback for orphaned sessions


@dataclass
class Session:
    id: str
    db: duckdb.DuckDBPyConnection
    created_at: float = field(default_factory=time.time)
    last_accessed: float = field(default_factory=time.time)

    def touch(self):
        self.last_accessed = time.time()

    @property
    def expired(self) -> bool:
        return (time.time() - self.last_accessed) > SESSION_TTL


def _init_db(
    cached_tables: dict[str, pa.Table],
    views: dict[str, str] | None = None,
) -> duckdb.DuckDBPyConnection:
    db = duckdb.connect()
    db.execute("SET enable_external_access = false")
    db.execute("CREATE SCHEMA IF NOT EXISTS db")
    for name, arrow_table in cached_tables.items():
        db.register(name, arrow_table)
        db.execute(f"CREATE VIEW db.{name} AS SELECT * FROM {name}")
    for name, sql in (views or {}).items():
        db.execute(f"CREATE VIEW db.{name} AS {sql}")
    return db


def create_shared_db(
    cached_tables: dict[str, pa.Table],
    views: dict[str, str] | None = None,
) -> duckdb.DuckDBPyConnection:
    return _init_db(cached_tables, views)


def create_session(
    cached_tables: dict[str, pa.Table],
    views: dict[str, str] | None = None,
) -> Session:
    session_id = str(uuid.uuid4())
    db = _init_db(cached_tables, views)
    return Session(id=session_id, db=db)


def destroy_session(session: Session):
    try:
        session.db.close()
    except duckdb.Error:
        pass


def get_session(request: Request) -> Session | None:
    """Lookup session from X-N6k-Session header (priority) or cookie (fallback)."""
    sessions: dict[str, Session] = request.app.state.sessions
    session_id = request.headers.get(SESSION_HEADER) or request.cookies.get(SESSION_COOKIE)
    if not session_id or session_id not in sessions:
        return None
    session = sessions[session_id]
    if session.expired:
        destroy_session(session)
        del sessions[session_id]
        return None
    session.touch()
    return session


def reap_expired_sessions(app: Starlette):
    sessions: dict[str, Session] = app.state.sessions
    expired = [sid for sid, s in sessions.items() if s.expired]
    for sid in expired:
        destroy_session(sessions.pop(sid))
