"""Durable active-session registry used to revoke signed operator sessions."""

import hashlib
import secrets
import sqlite3
from dataclasses import dataclass
from pathlib import Path
from typing import Callable

from src.private_state import connect_private_sqlite


class SessionStoreError(RuntimeError):
    """Raised when session state cannot be read or changed safely."""


class SessionStatus(str):
    """String-compatible status carrying the server-confirmed idle deadline."""

    idle_expires_at: int | None

    def __new__(cls, value: str, idle_expires_at: int | None = None):
        instance = super().__new__(cls, value)
        instance.idle_expires_at = idle_expires_at
        return instance


@dataclass(frozen=True)
class ActiveDevice:
    management_id: str
    device_label: str
    created_at: int
    expires_at: int
    current: bool


class SessionStore:
    def __init__(
        self,
        path: str | Path,
        *,
        clock: Callable[[], float],
        lock_timeout_seconds: float = 0.1,
    ) -> None:
        self.path = Path(path)
        self.clock = clock
        self.lock_timeout_seconds = lock_timeout_seconds

    @staticmethod
    def _digest(session_id: str) -> str:
        return hashlib.sha256(session_id.encode()).hexdigest()

    def _connect(self, *, initialize: bool = False) -> sqlite3.Connection:
        try:
            if initialize:
                connection = connect_private_sqlite(
                    self.path, timeout=self.lock_timeout_seconds
                )
            else:
                connection = connect_private_sqlite(
                    self.path,
                    timeout=self.lock_timeout_seconds,
                    existing_only=True,
                )
            if initialize:
                connection.execute(
                    """
                    CREATE TABLE IF NOT EXISTS active_sessions (
                        session_hash TEXT PRIMARY KEY,
                        expires_at INTEGER NOT NULL,
                        management_id TEXT,
                        device_label TEXT,
                        created_at INTEGER,
                        last_active_at INTEGER
                    )
                    """
                )
                connection.execute(
                    """
                    CREATE TABLE IF NOT EXISTS step_up_grants (
                        grant_hash TEXT PRIMARY KEY,
                        session_hash TEXT NOT NULL,
                        action TEXT NOT NULL,
                        target TEXT NOT NULL,
                        expires_at INTEGER NOT NULL
                    )
                    """
                )
                connection.execute(
                    "CREATE INDEX IF NOT EXISTS step_up_grants_session_hash "
                    "ON step_up_grants(session_hash)"
                )
                columns = {
                    row[1] for row in connection.execute("PRAGMA table_info(active_sessions)")
                }
                additions = {
                    "management_id": "TEXT",
                    "device_label": "TEXT",
                    "created_at": "INTEGER",
                    "last_active_at": "INTEGER",
                }
                for name, column_type in additions.items():
                    if name not in columns:
                        connection.execute(
                            f"ALTER TABLE active_sessions ADD COLUMN {name} {column_type}"
                        )
                connection.execute(
                    "UPDATE active_sessions SET management_id = lower(hex(randomblob(16))) "
                    "WHERE management_id IS NULL"
                )
                connection.execute(
                    "UPDATE active_sessions SET device_label = 'Existing device' "
                    "WHERE device_label IS NULL"
                )
                connection.execute(
                    "UPDATE active_sessions SET created_at = ? WHERE created_at IS NULL",
                    (int(self.clock()),),
                )
                connection.execute(
                    "UPDATE active_sessions SET last_active_at = ? WHERE last_active_at IS NULL",
                    (int(self.clock()),),
                )
                connection.execute(
                    "CREATE UNIQUE INDEX IF NOT EXISTS active_sessions_management_id "
                    "ON active_sessions(management_id)"
                )
            return connection
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc

    def activate(
        self,
        session_id: str,
        expires_at: int,
        *,
        device_label: str = "This device",
        management_id: str | None = None,
    ) -> None:
        label = " ".join(str(device_label).split())[:64] or "This device"
        now = int(self.clock())
        try:
            with self._connect(initialize=True) as connection:
                connection.execute(
                    "DELETE FROM active_sessions WHERE expires_at <= ?", (now,)
                )
                connection.execute(
                    "INSERT INTO active_sessions("
                    "session_hash, expires_at, management_id, device_label, created_at, last_active_at"
                    ") VALUES (?, ?, ?, ?, ?, ?)",
                    (
                        self._digest(session_id),
                        expires_at,
                        management_id or secrets.token_urlsafe(18),
                        label,
                        now,
                        now,
                    ),
                )
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc

    def is_active(self, session_id: str, expires_at: int) -> bool:
        now = int(self.clock())
        try:
            with self._connect() as connection:
                row = connection.execute(
                    "SELECT expires_at FROM active_sessions WHERE session_hash = ?",
                    (self._digest(session_id),),
                ).fetchone()
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc
        return row is not None and row[0] == expires_at and expires_at > now

    def status(
        self, session_id: str, expires_at: int, *, idle_timeout_seconds: int
    ) -> str:
        now = int(self.clock())
        query = (
            "SELECT expires_at, last_active_at FROM active_sessions "
            "WHERE session_hash = ?"
        )
        parameters = (self._digest(session_id),)
        try:
            try:
                with self._connect() as connection:
                    row = connection.execute(query, parameters).fetchone()
            except sqlite3.OperationalError as exc:
                if "no such column: last_active_at" not in str(exc):
                    raise
                with self._connect(initialize=True) as connection:
                    row = connection.execute(query, parameters).fetchone()
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc
        if row is None or row[0] != expires_at or expires_at <= now:
            return SessionStatus("revoked")
        idle_expires_at = min(row[0], row[1] + max(1, idle_timeout_seconds))
        if idle_expires_at <= now:
            return SessionStatus("idle", idle_expires_at)
        return SessionStatus("active", idle_expires_at)

    def touch(
        self, session_id: str, expires_at: int, *, idle_timeout_seconds: int
    ) -> bool:
        now = int(self.clock())
        try:
            with self._connect() as connection:
                cursor = connection.execute(
                    "UPDATE active_sessions SET last_active_at = ? "
                    "WHERE session_hash = ? AND expires_at = ? AND expires_at > ? "
                    "AND last_active_at + ? > ?",
                    (
                        now,
                        self._digest(session_id),
                        expires_at,
                        now,
                        max(1, idle_timeout_seconds),
                        now,
                    ),
                )
                return cursor.rowcount == 1
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc

    def revoke(self, session_id: str) -> None:
        try:
            with self._connect(initialize=True) as connection:
                connection.execute(
                    "DELETE FROM step_up_grants WHERE session_hash = ?",
                    (self._digest(session_id),),
                )
                connection.execute(
                    "DELETE FROM active_sessions WHERE session_hash = ?",
                    (self._digest(session_id),),
                )
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc

    def list_active(self, current_session_id: str) -> list[ActiveDevice]:
        now = int(self.clock())
        current_hash = self._digest(current_session_id)
        try:
            with self._connect() as connection:
                rows = connection.execute(
                    "SELECT management_id, device_label, created_at, expires_at, session_hash "
                    "FROM active_sessions WHERE expires_at > ? "
                    "ORDER BY expires_at DESC, created_at DESC",
                    (now,),
                ).fetchall()
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc
        return [
            ActiveDevice(
                management_id=row[0],
                device_label=row[1],
                created_at=row[2],
                expires_at=row[3],
                current=secrets.compare_digest(row[4], current_hash),
            )
            for row in rows
        ]

    def management_id(self, session_id: str) -> str:
        try:
            with self._connect() as connection:
                row = connection.execute(
                    "SELECT management_id FROM active_sessions WHERE session_hash = ?",
                    (self._digest(session_id),),
                ).fetchone()
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc
        if row is None:
            raise SessionStoreError("Session is no longer active")
        return row[0]

    def managed_status(self, management_id: str, *, idle_timeout_seconds: int) -> str:
        """Return the authorization state for a durable device identifier."""
        return self.managed_statuses(
            [management_id], idle_timeout_seconds=idle_timeout_seconds
        )[management_id]

    def managed_statuses(
        self, management_ids, *, idle_timeout_seconds: int
    ) -> dict[str, str]:
        """Return authorization states for durable device identifiers in one read."""
        requested = set(management_ids)
        if not requested:
            return {}
        now = int(self.clock())
        try:
            with self._connect() as connection:
                placeholders = ",".join("?" for _ in requested)
                rows = connection.execute(
                    "SELECT management_id, expires_at, last_active_at "
                    f"FROM active_sessions WHERE management_id IN ({placeholders})",
                    tuple(requested),
                ).fetchall()
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc
        known = {row[0]: row[1:] for row in rows if row[0] in requested}
        idle_timeout = max(1, idle_timeout_seconds)
        statuses = {}
        for management_id in requested:
            row = known.get(management_id)
            if row is None or row[0] <= now:
                statuses[management_id] = "revoked"
            elif row[1] + idle_timeout <= now:
                statuses[management_id] = "idle"
            else:
                statuses[management_id] = "active"
        return statuses

    def revoke_managed(self, management_id: str) -> bool:
        try:
            with self._connect() as connection:
                row = connection.execute(
                    "SELECT session_hash FROM active_sessions WHERE management_id = ?",
                    (management_id,),
                ).fetchone()
                if row is None:
                    return False
                connection.execute(
                    "DELETE FROM step_up_grants WHERE session_hash = ?", (row[0],)
                )
                cursor = connection.execute(
                    "DELETE FROM active_sessions WHERE management_id = ?", (management_id,)
                )
                return cursor.rowcount == 1
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc

    def revoke_all(self) -> None:
        try:
            with self._connect(initialize=True) as connection:
                connection.execute("DELETE FROM step_up_grants")
                connection.execute("DELETE FROM active_sessions")
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc

    def mint_step_up(
        self,
        session_id: str,
        *,
        action: str,
        target: str,
        ttl_seconds: int,
    ) -> str:
        now = int(self.clock())
        grant = secrets.token_urlsafe(32)
        session_hash = self._digest(session_id)
        try:
            with self._connect(initialize=True) as connection:
                active = connection.execute(
                    "SELECT 1 FROM active_sessions "
                    "WHERE session_hash = ? AND expires_at > ?",
                    (session_hash, now),
                ).fetchone()
                if active is None:
                    raise SessionStoreError("Session is no longer active")
                connection.execute(
                    "DELETE FROM step_up_grants WHERE expires_at <= ?", (now,)
                )
                connection.execute(
                    "INSERT INTO step_up_grants("
                    "grant_hash, session_hash, action, target, expires_at"
                    ") VALUES (?, ?, ?, ?, ?)",
                    (
                        self._digest(grant),
                        session_hash,
                        action,
                        target,
                        now + max(1, ttl_seconds),
                    ),
                )
        except SessionStoreError:
            raise
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc
        return grant

    def consume_step_up(
        self,
        grant: str,
        session_id: str,
        *,
        action: str,
        target: str,
    ) -> bool:
        now = int(self.clock())
        try:
            with self._connect() as connection:
                connection.execute(
                    "DELETE FROM step_up_grants WHERE expires_at <= ?", (now,)
                )
                cursor = connection.execute(
                    "DELETE FROM step_up_grants WHERE grant_hash = ? "
                    "AND session_hash = ? AND action = ? AND target = ? "
                    "AND expires_at > ? AND EXISTS ("
                    "SELECT 1 FROM active_sessions "
                    "WHERE active_sessions.session_hash = step_up_grants.session_hash "
                    "AND active_sessions.expires_at > ?)",
                    (
                        self._digest(grant),
                        self._digest(session_id),
                        action,
                        target,
                        now,
                        now,
                    ),
                )
                return cursor.rowcount == 1
        except (OSError, sqlite3.Error) as exc:
            raise SessionStoreError("Session registry is temporarily unavailable") from exc
