"""Durable throttling state for operator sign-in attempts."""

import hashlib
import ipaddress
import math
import sqlite3
from pathlib import Path
from typing import Callable

from src.private_state import connect_private_sqlite


class LoginAttemptStoreError(RuntimeError):
    """Raised when sign-in throttling state cannot be accessed safely."""


def client_source(peer_host: str, forwarded_for: str, trusted_proxy_cidrs: str) -> str:
    """Resolve a canonical client IP without trusting arbitrary forwarding headers."""
    try:
        peer = ipaddress.ip_address(peer_host)
        trusted = [
            ipaddress.ip_network(value.strip())
            for value in trusted_proxy_cidrs.split(",")
            if value.strip()
        ]
    except ValueError:
        return peer_host
    if not any(peer in network for network in trusted):
        return str(peer)
    try:
        forwarded = [
            ipaddress.ip_address(value.strip())
            for value in forwarded_for.split(",")
            if value.strip()
        ]
    except ValueError:
        return str(peer)
    for address in reversed(forwarded):
        if not any(address in network for network in trusted):
            return str(address)
    return str(forwarded[0]) if forwarded else str(peer)


class LoginAttemptStore:
    def __init__(
        self,
        path: str | Path,
        *,
        clock: Callable[[], float],
        max_failures: int,
        window_seconds: int,
        max_entries: int = 10_000,
        lock_timeout_seconds: float = 0.1,
        alert_bucket_seconds: int = 60 * 60,
        alert_retention_seconds: int = 30 * 24 * 60 * 60,
        max_alert_buckets: int = 720,
    ) -> None:
        self.path = Path(path)
        self.clock = clock
        self.max_failures = max(1, max_failures)
        self.window_seconds = max(1, window_seconds)
        self.max_entries = max(1, max_entries)
        self.lock_timeout_seconds = lock_timeout_seconds
        self.alert_bucket_seconds = max(1, alert_bucket_seconds)
        self.alert_retention_seconds = max(1, alert_retention_seconds)
        self.max_alert_buckets = max(1, max_alert_buckets)

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

    def _connect(self) -> sqlite3.Connection:
        try:
            connection = connect_private_sqlite(self.path, timeout=self.lock_timeout_seconds)
            connection.execute(
                """
                CREATE TABLE IF NOT EXISTS login_attempts (
                    source_hash TEXT PRIMARY KEY,
                    failures INTEGER NOT NULL,
                    window_started_at REAL NOT NULL
                )
                """
            )
            connection.execute(
                """
                CREATE TABLE IF NOT EXISTS login_admissions (
                    bucket TEXT NOT NULL,
                    source_hash TEXT NOT NULL,
                    attempts INTEGER NOT NULL,
                    window_started_at REAL NOT NULL,
                    PRIMARY KEY (bucket, source_hash)
                )
                """
            )
            connection.execute(
                """
                CREATE TABLE IF NOT EXISTS login_alerts (
                    method TEXT NOT NULL,
                    bucket_started_at INTEGER NOT NULL,
                    failed_count INTEGER NOT NULL DEFAULT 0,
                    blocked_count INTEGER NOT NULL DEFAULT 0,
                    first_at INTEGER NOT NULL,
                    last_at INTEGER NOT NULL,
                    PRIMARY KEY (method, bucket_started_at)
                )
                """
            )
            return connection
        except (OSError, sqlite3.Error) as exc:
            raise LoginAttemptStoreError(
                "Sign-in throttling is temporarily unavailable"
            ) from exc

    def retry_after(self, source: str) -> int:
        now = self.clock()
        try:
            with self._connect() as connection:
                row = connection.execute(
                    "SELECT failures, window_started_at FROM login_attempts WHERE source_hash = ?",
                    (self._digest(source),),
                ).fetchone()
        except (OSError, sqlite3.Error) as exc:
            raise LoginAttemptStoreError(
                "Sign-in throttling is temporarily unavailable"
            ) from exc
        if row is None or row[0] < self.max_failures:
            return 0
        remaining = row[1] + self.window_seconds - now
        return max(0, math.ceil(remaining))

    def admit(self, bucket: str, source: str, *, limit: int) -> int:
        """Consume one fixed-window admission or return seconds until retry."""
        now = self.clock()
        source_hash = self._digest(source)
        limit = max(1, limit)
        try:
            with self._connect() as connection:
                connection.execute("BEGIN IMMEDIATE")
                connection.execute(
                    "DELETE FROM login_admissions WHERE window_started_at + ? <= ?",
                    (self.window_seconds, now),
                )
                row = connection.execute(
                    "SELECT attempts, window_started_at FROM login_admissions "
                    "WHERE bucket = ? AND source_hash = ?",
                    (bucket, source_hash),
                ).fetchone()
                if row is not None and row[0] >= limit:
                    return max(1, math.ceil(row[1] + self.window_seconds - now))
                if row is None:
                    connection.execute(
                        "INSERT INTO login_admissions VALUES (?, ?, 1, ?)",
                        (bucket, source_hash, now),
                    )
                else:
                    connection.execute(
                        "UPDATE login_admissions SET attempts = attempts + 1 "
                        "WHERE bucket = ? AND source_hash = ?",
                        (bucket, source_hash),
                    )
                connection.execute(
                    """
                    DELETE FROM login_admissions
                    WHERE (bucket, source_hash) NOT IN (
                        SELECT bucket, source_hash FROM login_admissions
                        ORDER BY window_started_at DESC, rowid DESC LIMIT ?
                    )
                    """,
                    (self.max_entries,),
                )
                return 0
        except (OSError, sqlite3.Error) as exc:
            raise LoginAttemptStoreError(
                "Sign-in throttling is temporarily unavailable"
            ) from exc

    def _record_alert(
        self, connection: sqlite3.Connection, method: str, *, blocked: bool, now: float
    ) -> None:
        safe_method = method if method in {"token", "passkey"} else "unknown"
        occurred_at = int(now)
        bucket = occurred_at - (occurred_at % self.alert_bucket_seconds)
        failed = 0 if blocked else 1
        blocked_count = 1 if blocked else 0
        connection.execute(
            """
            INSERT INTO login_alerts(
                method, bucket_started_at, failed_count, blocked_count, first_at, last_at
            ) VALUES (?, ?, ?, ?, ?, ?)
            ON CONFLICT(method, bucket_started_at) DO UPDATE SET
                failed_count = failed_count + excluded.failed_count,
                blocked_count = blocked_count + excluded.blocked_count,
                first_at = MIN(first_at, excluded.first_at),
                last_at = MAX(last_at, excluded.last_at)
            """,
            (safe_method, bucket, failed, blocked_count, occurred_at, occurred_at),
        )
        connection.execute(
            "DELETE FROM login_alerts WHERE (method, bucket_started_at) NOT IN "
            "(SELECT method, bucket_started_at FROM login_alerts "
            "ORDER BY bucket_started_at DESC LIMIT ?)",
            (self.max_alert_buckets,),
        )

    def record_failure(self, source: str, *, method: str = "token") -> None:
        now = self.clock()
        source_hash = self._digest(source)
        try:
            with self._connect() as connection:
                connection.execute("BEGIN IMMEDIATE")
                connection.execute(
                    "DELETE FROM login_attempts WHERE window_started_at + ? <= ?",
                    (self.window_seconds, now),
                )
                row = connection.execute(
                    "SELECT failures, window_started_at FROM login_attempts WHERE source_hash = ?",
                    (source_hash,),
                ).fetchone()
                if row is None or row[1] + self.window_seconds <= now:
                    connection.execute(
                        "INSERT OR REPLACE INTO login_attempts VALUES (?, 1, ?)",
                        (source_hash, now),
                    )
                else:
                    connection.execute(
                        "UPDATE login_attempts SET failures = failures + 1 WHERE source_hash = ?",
                        (source_hash,),
                    )
                connection.execute(
                    """
                    DELETE FROM login_attempts
                    WHERE source_hash NOT IN (
                        SELECT source_hash FROM login_attempts
                        ORDER BY window_started_at DESC, rowid DESC LIMIT ?
                    )
                    """,
                    (self.max_entries,),
                )
                self._record_alert(connection, method, blocked=False, now=now)
        except (OSError, sqlite3.Error) as exc:
            raise LoginAttemptStoreError(
                "Sign-in throttling is temporarily unavailable"
            ) from exc

    def record_blocked(self, *, method: str = "token") -> None:
        now = self.clock()
        try:
            with self._connect() as connection:
                connection.execute("BEGIN IMMEDIATE")
                self._record_alert(connection, method, blocked=True, now=now)
        except (OSError, sqlite3.Error) as exc:
            raise LoginAttemptStoreError(
                "Sign-in throttling is temporarily unavailable"
            ) from exc

    def list_alerts(self, *, limit: int = 24) -> list[dict[str, int | str]]:
        bounded_limit = min(100, max(1, limit))
        now = int(self.clock())
        try:
            with self._connect() as connection:
                connection.execute(
                    "DELETE FROM login_alerts WHERE last_at < ?",
                    (now - self.alert_retention_seconds,),
                )
                rows = connection.execute(
                    "SELECT method, failed_count, blocked_count, first_at, last_at "
                    "FROM login_alerts ORDER BY bucket_started_at DESC LIMIT ?",
                    (bounded_limit,),
                ).fetchall()
        except (OSError, sqlite3.Error) as exc:
            raise LoginAttemptStoreError(
                "Sign-in throttling is temporarily unavailable"
            ) from exc
        return [
            {
                "method": row[0],
                "failed_count": row[1],
                "blocked_count": row[2],
                "first_at": row[3],
                "last_at": row[4],
            }
            for row in rows
        ]

    def clear(self, source: str) -> None:
        try:
            with self._connect() as connection:
                connection.execute(
                    "DELETE FROM login_attempts WHERE source_hash = ?",
                    (self._digest(source),),
                )
        except (OSError, sqlite3.Error) as exc:
            raise LoginAttemptStoreError(
                "Sign-in throttling is temporarily unavailable"
            ) from exc
