"""Bounded, privacy-preserving journal of operator security activity."""

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

from src.private_state import connect_private_sqlite
from src.state_encryption import (
    PrivateStateCipher,
    PrivateStateEncryptionError,
    private_state_encryption_key,
)


class SecurityEventStoreError(RuntimeError):
    """Raised when security activity cannot be persisted or read safely."""


@dataclass(frozen=True)
class SecurityEvent:
    id: int
    kind: str
    method: str | None
    device_label: str | None
    target: str | None
    created_at: int
    status: str


@dataclass(frozen=True)
class SecurityEventPage:
    events: list[SecurityEvent]
    next_cursor: int | None


class SecurityEventStore:
    def __init__(
        self,
        path: str | Path,
        *,
        clock: Callable[[], float],
        max_events: int = 10_000,
        retention_seconds: int = 90 * 24 * 60 * 60,
        lock_timeout_seconds: float = 0.1,
        encryption_key: bytes | None = None,
    ) -> None:
        self.path = Path(path)
        self.clock = clock
        self.max_events = max(1, max_events)
        self.retention_seconds = max(1, retention_seconds)
        self.lock_timeout_seconds = lock_timeout_seconds
        try:
            self._cipher = PrivateStateCipher(
                encryption_key if encryption_key is not None else private_state_encryption_key(),
                store="security-events",
            )
        except PrivateStateEncryptionError as exc:
            raise SecurityEventStoreError(
                "Security activity is temporarily unavailable"
            ) from exc

    @staticmethod
    def _bounded(value: str | None, limit: int) -> str | None:
        if value is None:
            return None
        normalized = " ".join(str(value).split())[:limit]
        return normalized or None

    def _connect(self) -> sqlite3.Connection:
        try:
            connection = connect_private_sqlite(self.path, timeout=self.lock_timeout_seconds)
            connection.execute(
                """
                CREATE TABLE IF NOT EXISTS security_events (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    payload TEXT,
                    created_at INTEGER NOT NULL,
                    status TEXT NOT NULL DEFAULT 'completed',
                    operation_id TEXT
                )
                """
            )
            columns = {
                row[1] for row in connection.execute("PRAGMA table_info(security_events)")
            }
            if "status" not in columns:
                connection.execute(
                    "ALTER TABLE security_events ADD COLUMN status TEXT NOT NULL DEFAULT 'completed'"
                )
            if "operation_id" not in columns:
                connection.execute(
                    "ALTER TABLE security_events ADD COLUMN operation_id TEXT"
                )
            plaintext_columns = {"kind", "method", "device_label", "target"}
            if "payload" not in columns or plaintext_columns.intersection(columns):
                self._migrate_plaintext(
                    connection, has_payload="payload" in columns
                )
            connection.execute(
                "CREATE INDEX IF NOT EXISTS security_events_created "
                "ON security_events(created_at DESC, id DESC)"
            )
            connection.execute(
                "CREATE UNIQUE INDEX IF NOT EXISTS security_events_operation "
                "ON security_events(operation_id) WHERE operation_id IS NOT NULL"
            )
            return connection
        except (OSError, sqlite3.Error) as exc:
            raise SecurityEventStoreError(
                "Security activity is temporarily unavailable"
            ) from exc

    def _migrate_plaintext(
        self, connection: sqlite3.Connection, *, has_payload: bool
    ) -> None:
        connection.execute("PRAGMA secure_delete = ON")
        payload_column = "payload," if has_payload else "NULL AS payload,"
        rows = connection.execute(
            f"SELECT id, {payload_column} kind, method, device_label, target, "
            "created_at, status, operation_id FROM security_events ORDER BY id"
        ).fetchall()
        connection.execute(
            """
            CREATE TABLE security_events_encrypted (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                payload TEXT,
                created_at INTEGER NOT NULL,
                status TEXT NOT NULL DEFAULT 'completed',
                operation_id TEXT
            )
            """
        )
        for (
            event_id,
            payload,
            kind,
            method,
            device_label,
            target,
            created_at,
            status,
            operation_id,
        ) in rows:
            connection.execute(
                "INSERT INTO security_events_encrypted "
                "(id, payload, created_at, status, operation_id) VALUES (?, ?, ?, ?, ?)",
                (
                    event_id,
                    payload
                    or self._seal_event(
                        event_id, kind, method, device_label, target
                    ),
                    created_at,
                    status,
                    operation_id,
                ),
            )
        connection.execute("DROP TABLE security_events")
        connection.execute(
            "ALTER TABLE security_events_encrypted RENAME TO security_events"
        )

    def _prune(self, connection: sqlite3.Connection, now: int) -> None:
        connection.execute(
            "DELETE FROM security_events WHERE created_at < ?",
            (now - self.retention_seconds,),
        )
        connection.execute(
            "DELETE FROM security_events WHERE id NOT IN "
            "(SELECT id FROM security_events ORDER BY id DESC LIMIT ?)",
            (self.max_events,),
        )

    def _seal_event(
        self,
        event_id: int,
        kind: str,
        method: str | None,
        device_label: str | None,
        target: str | None,
    ) -> str:
        return self._cipher.seal(
            {
                "kind": self._bounded(kind, 48) or "security_event",
                "method": self._bounded(method, 32),
                "device_label": self._bounded(device_label, 64),
                "target": self._bounded(target, 255),
            },
            binding=f"event:{event_id}",
        )

    def record(
        self,
        kind: str,
        *,
        method: str | None = None,
        device_label: str | None = None,
        target: str | None = None,
    ) -> None:
        now = int(self.clock())
        try:
            with self._connect() as connection:
                cursor = connection.execute(
                    "INSERT INTO security_events(created_at, status) VALUES (?, 'completed')",
                    (now,),
                )
                event_id = cursor.lastrowid
                payload = self._seal_event(
                    event_id, kind, method, device_label, target
                )
                connection.execute(
                    "UPDATE security_events SET payload = ? WHERE id = ?",
                    (payload, event_id),
                )
                self._prune(connection, now)
        except (OSError, sqlite3.Error) as exc:
            raise SecurityEventStoreError(
                "Security activity is temporarily unavailable"
            ) from exc

    def reserve(
        self,
        kind: str,
        *,
        method: str | None = None,
        device_label: str | None = None,
        target: str | None = None,
    ) -> str:
        now = int(self.clock())
        operation_id = secrets.token_urlsafe(24)
        try:
            with self._connect() as connection:
                cursor = connection.execute(
                    "INSERT INTO security_events(created_at, status, operation_id) "
                    "VALUES (?, 'pending', ?)",
                    (now, operation_id),
                )
                event_id = cursor.lastrowid
                connection.execute(
                    "UPDATE security_events SET payload = ? WHERE id = ?",
                    (
                        self._seal_event(
                            event_id, kind, method, device_label, target
                        ),
                        event_id,
                    ),
                )
                self._prune(connection, now)
        except (OSError, sqlite3.Error) as exc:
            raise SecurityEventStoreError(
                "Security activity is temporarily unavailable"
            ) from exc
        return operation_id

    def finalize(self, operation_id: str) -> None:
        try:
            with self._connect() as connection:
                cursor = connection.execute(
                    "UPDATE security_events SET status = 'completed' WHERE operation_id = ?",
                    (operation_id,),
                )
                if cursor.rowcount != 1:
                    raise SecurityEventStoreError(
                        "Security activity reservation was not found"
                    )
        except (OSError, sqlite3.Error) as exc:
            raise SecurityEventStoreError(
                "Security activity is temporarily unavailable"
            ) from exc

    def discard(self, operation_id: str) -> None:
        try:
            with self._connect() as connection:
                connection.execute(
                    "DELETE FROM security_events WHERE operation_id = ? AND status = 'pending'",
                    (operation_id,),
                )
        except (OSError, sqlite3.Error) as exc:
            raise SecurityEventStoreError(
                "Security activity is temporarily unavailable"
            ) from exc

    def list(self, *, limit: int = 50, cursor: int | None = None) -> SecurityEventPage:
        bounded_limit = min(100, max(1, limit))
        parameters: list[int] = []
        where = ""
        if cursor is not None:
            where = "WHERE id < ?"
            parameters.append(cursor)
        parameters.append(bounded_limit + 1)
        try:
            with self._connect() as connection:
                rows = connection.execute(
                    "SELECT id, payload, created_at, status "
                    f"FROM security_events {where} ORDER BY id DESC LIMIT ?",
                    parameters,
                ).fetchall()
        except (OSError, sqlite3.Error) as exc:
            raise SecurityEventStoreError(
                "Security activity is temporarily unavailable"
            ) from exc
        has_more = len(rows) > bounded_limit
        visible = rows[:bounded_limit]
        events = []
        try:
            for event_id, payload, created_at, status in visible:
                value, _legacy = self._cipher.open(
                    payload, binding=f"event:{event_id}"
                )
                if not isinstance(value, dict) or not isinstance(value.get("kind"), str):
                    raise PrivateStateEncryptionError(
                        "private state could not be decrypted"
                    )
                events.append(
                    SecurityEvent(
                        event_id,
                        value["kind"],
                        value.get("method"),
                        value.get("device_label"),
                        value.get("target"),
                        created_at,
                        status,
                    )
                )
        except PrivateStateEncryptionError as exc:
            raise SecurityEventStoreError(
                "Security activity is temporarily unavailable"
            ) from exc
        return SecurityEventPage(
            events=events,
            next_cursor=visible[-1][0] if has_more else None,
        )
