from __future__ import annotations

import base64
import hashlib
import hmac
import os
import secrets
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Protocol

from fastapi import HTTPException, Request, status
from sqlalchemy import create_engine, select, text
from sqlalchemy.engine import Engine
from sqlalchemy.exc import SQLAlchemyError

from .database import HockeyDatabase
from .models import HockeyWebSession


HOCKEY_COOKIE_NAME = "hockey_auth_token"
HOCKEY_SESSION_SECONDS = 4 * 60 * 60
HOCKEY_SESSION_TOUCH_SECONDS = 60


@dataclass(frozen=True, slots=True)
class HockeyUser:
    id: str
    login: str
    display_name: str
    is_active: bool = True
    is_admin: bool = False
    role: str = "operator"


class SharedAuthAdapter(Protocol):
    async def current_user(self, request: Request) -> HockeyUser | None:
        ...

    def login(self, username: str, password: str) -> tuple[str, HockeyUser] | None:
        ...

    def logout(self, token: str) -> None:
        ...


class WFLAuthAdapter:
    """Authenticate against WFL accounts and keep web sessions in hockey DB.

    The WFL engine is strictly read-only: no WFL accounts, password hashes or
    auth sessions are copied or modified. Only the WFL user id and snapshots
    needed for audit/display are stored in the hockey database.
    """

    def __init__(self, database_url: str | None, hockey_database: HockeyDatabase) -> None:
        self.hockey_database = hockey_database
        self.engine: Engine | None = None
        if database_url:
            self.engine = create_engine(
                database_url,
                future=True,
                pool_pre_ping=True,
            )

    def login(self, username: str, password: str) -> tuple[str, HockeyUser] | None:
        username = username.strip()
        if not username or not password:
            return None
        row = self._wfl_account(username=username)
        if row is None or not row["is_active"]:
            return None
        if not _verify_wfl_password(password, str(row["password_hash"] or "")):
            return None

        user = _user_from_row(row)
        token = secrets.token_urlsafe(48)
        now = datetime.utcnow()
        with self.hockey_database.session() as session:
            session.add(
                HockeyWebSession(
                    token_hash=_token_hash(token),
                    wfl_user_id=user.id,
                    login_snapshot=user.login,
                    role_snapshot=user.role,
                    created_at=now,
                    last_activity_at=now,
                    expires_at=now + timedelta(seconds=HOCKEY_SESSION_SECONDS),
                )
            )
        return token, user

    async def current_user(self, request: Request) -> HockeyUser | None:
        token = request.cookies.get(HOCKEY_COOKIE_NAME, "").strip()
        if not token:
            return None

        now = datetime.utcnow()
        with self.hockey_database.session() as session:
            web_session = session.scalar(
                select(HockeyWebSession).where(
                    HockeyWebSession.token_hash == _token_hash(token)
                )
            )
            if (
                web_session is None
                or web_session.revoked_at is not None
                or web_session.expires_at < now
            ):
                return None
            wfl_user_id = web_session.wfl_user_id

            row = self._wfl_account(user_id=wfl_user_id)
            if row is None or not row["is_active"]:
                web_session.revoked_at = now
                return None

            if (
                now - web_session.last_activity_at
            ).total_seconds() >= HOCKEY_SESSION_TOUCH_SECONDS:
                web_session.last_activity_at = now
                web_session.expires_at = now + timedelta(
                    seconds=HOCKEY_SESSION_SECONDS
                )
            return _user_from_row(row)

    def logout(self, token: str) -> None:
        if not token:
            return
        with self.hockey_database.session() as session:
            web_session = session.scalar(
                select(HockeyWebSession).where(
                    HockeyWebSession.token_hash == _token_hash(token)
                )
            )
            if web_session is not None and web_session.revoked_at is None:
                web_session.revoked_at = datetime.utcnow()

    def _wfl_account(
        self,
        *,
        username: str | None = None,
        user_id: str | None = None,
    ):
        if self.engine is None:
            raise HTTPException(
                status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                detail="WFL_DATABASE_URL is not configured in the hockey .env",
            )
        where = "u.username = :value" if username is not None else "u.id = :value"
        value = username if username is not None else user_id
        fields = "u.id, u.username, u.is_active, u.role"
        if username is not None:
            fields += ", u.password_hash"
        try:
            with self.engine.connect() as connection:
                return connection.execute(
                    text(
                        f"""
                        SELECT {fields}
                        FROM admin_users AS u
                        WHERE {where}
                        LIMIT 1
                        """
                    ),
                    {"value": value},
                ).mappings().first()
        except SQLAlchemyError:
            raise HTTPException(
                status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                detail="WFL authentication database is unavailable",
            )


class LocalBrowserUserAdapter:
    """Explicit development fallback; never enabled in WFL mode."""

    def __init__(self) -> None:
        self._tokens: dict[str, HockeyUser] = {}

    async def current_user(self, request: Request) -> HockeyUser | None:
        user_id = request.headers.get("X-Hockey-User", "").strip()
        if user_id:
            role = request.headers.get("X-Hockey-Role", "operator").strip().lower()
            return HockeyUser(
                id=user_id,
                login=user_id,
                display_name=user_id,
                is_admin=role == "admin",
                role=role,
            )
        return self._tokens.get(request.cookies.get(HOCKEY_COOKIE_NAME, ""))

    def login(self, username: str, password: str) -> tuple[str, HockeyUser] | None:
        if not username.strip() or not password:
            return None
        user = HockeyUser(
            id=username.strip(),
            login=username.strip(),
            display_name=username.strip(),
        )
        token = secrets.token_urlsafe(32)
        self._tokens[token] = user
        return token, user

    def logout(self, token: str) -> None:
        self._tokens.pop(token, None)


def create_auth_adapter(hockey_database: HockeyDatabase) -> SharedAuthAdapter:
    mode = os.getenv("HOCKEY_AUTH_MODE", "wfl").strip().lower()
    if mode == "dev":
        return LocalBrowserUserAdapter()
    if mode != "wfl":
        raise RuntimeError("HOCKEY_AUTH_MODE must be 'wfl' or 'dev'")
    database_url = os.getenv("WFL_DATABASE_URL", "").strip() or None
    return WFLAuthAdapter(database_url, hockey_database)


class HockeyAuthDependencies:
    def __init__(self, adapter: SharedAuthAdapter) -> None:
        self.adapter = adapter

    async def optional_user(self, request: Request) -> HockeyUser | None:
        return await self.adapter.current_user(request)

    async def require_user(self, request: Request) -> HockeyUser:
        user = await self.optional_user(request)
        if user is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Hockey authentication required",
            )
        return user

    async def require_admin(self, request: Request) -> HockeyUser:
        user = await self.require_user(request)
        if not user.is_admin:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="WFL administrator role required",
            )
        return user


def _user_from_row(row) -> HockeyUser:
    role = str(row["role"] or "operator").strip().lower()
    username = str(row["username"] or "").strip()
    return HockeyUser(
        id=str(row["id"]),
        login=username,
        display_name=username,
        is_active=bool(row["is_active"]),
        is_admin=role == "admin",
        role=role,
    )


def _verify_wfl_password(password: str, stored_hash: str) -> bool:
    try:
        algorithm, iterations_raw, salt_b64, hash_b64 = stored_hash.split("$", 3)
        if algorithm != "pbkdf2_sha256":
            return False
        iterations = int(iterations_raw)
        salt = base64.b64decode(salt_b64)
        expected = base64.b64decode(hash_b64)
    except Exception:
        return False
    actual = hashlib.pbkdf2_hmac(
        "sha256", password.encode("utf-8"), salt, iterations
    )
    return hmac.compare_digest(actual, expected)


def _token_hash(token: str) -> str:
    return hashlib.sha256(token.encode("utf-8")).hexdigest()
