"""WebAuthn passkey authentication for Otto's admin panel."""

import base64
import hashlib
import hmac
import json
import os
import secrets
import stat
import time
from pathlib import Path
from typing import Any

from filelock import FileLock
from webauthn import (
    generate_authentication_options,
    generate_registration_options,
    options_to_json,
    verify_authentication_response,
    verify_registration_response,
)
from webauthn.helpers import bytes_to_base64url
from webauthn.helpers.structs import PublicKeyCredentialDescriptor

from otto.config import OTTO_HOME
from otto.log import get_logger

log = get_logger("otto.web.auth")

_PASSKEY_FILE: Path = OTTO_HOME / "config" / "passkeys.json"
_LOCK_TIMEOUT = 10
_CHALLENGE_TTL = 300  # 5 minutes
_SESSION_TTL = 604800  # 7 days
_SESSION_COOKIE = "otto_session"


def _b64url_decode(value: str) -> bytes:
    """Decode a base64url string with missing padding."""
    padded = value + "=" * (-len(value) % 4)
    return base64.urlsafe_b64decode(padded)


# ---------------------------------------------------------------------------
# PasskeyStorage — mirrors AuthStorage from otto/auth/__init__.py
# ---------------------------------------------------------------------------


class PasskeyStorage:
    """File-backed credential store for WebAuthn passkeys."""

    def __init__(self, path: Path | None = None) -> None:
        self._path = path or _PASSKEY_FILE
        self._lock = FileLock(str(self._path) + ".lock", timeout=_LOCK_TIMEOUT)
        self._registered_cache: bool | None = None

    def _read(self) -> list[dict[str, Any]]:
        try:
            data = json.loads(self._path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, FileNotFoundError, OSError):
            return []
        if isinstance(data, list):
            return data
        return data.get("credentials", []) if isinstance(data, dict) else []

    def _write_locked(self, credentials: list[dict[str, Any]]) -> None:
        """Write credentials to disk. Caller must hold self._lock."""
        self._path.parent.mkdir(parents=True, exist_ok=True)
        self._path.write_text(json.dumps(credentials, indent=2), encoding="utf-8")
        try:
            os.chmod(self._path, stat.S_IRUSR | stat.S_IWUSR)
        except OSError:
            pass
        self._registered_cache = len(credentials) > 0

    @property
    def is_registered(self) -> bool:
        if self._registered_cache is not None:
            return self._registered_cache
        result = len(self._read()) > 0
        self._registered_cache = result
        return result

    def save_credential(self, credential: dict[str, Any]) -> None:
        with self._lock:
            creds = self._read()
            creds.append(credential)
            self._write_locked(creds)

    def update_credentials(self, credentials: list[dict[str, Any]]) -> None:
        with self._lock:
            self._write_locked(credentials)

    def get_credentials(self) -> list[dict[str, Any]]:
        return self._read()

    def delete_all(self) -> None:
        with self._lock:
            try:
                self._path.unlink()
            except FileNotFoundError:
                pass
            self._registered_cache = False


# ---------------------------------------------------------------------------
# SessionManager — HMAC-signed cookie tokens
# ---------------------------------------------------------------------------


class SessionManager:
    """Create and validate HMAC-signed session tokens."""

    def __init__(self, secret: str) -> None:
        self._secret = secret.encode("utf-8")

    def create_token(self) -> str:
        session_id = secrets.token_hex(16)
        timestamp = str(int(time.time()))
        payload = f"{session_id}:{timestamp}"
        sig = hmac.new(self._secret, payload.encode(), hashlib.sha256).hexdigest()
        return f"{payload}:{sig}"

    def validate_token(self, token: str) -> bool:
        parts = token.split(":")
        if len(parts) != 3:
            return False
        session_id, timestamp_str, sig = parts
        payload = f"{session_id}:{timestamp_str}"
        expected = hmac.new(self._secret, payload.encode(), hashlib.sha256).hexdigest()
        if not hmac.compare_digest(sig, expected):
            return False
        try:
            ts = int(timestamp_str)
        except ValueError:
            return False
        return (time.time() - ts) < _SESSION_TTL


# ---------------------------------------------------------------------------
# Challenge store — in-memory, single-process
# ---------------------------------------------------------------------------

_challenges: dict[str, tuple[bytes, float]] = {}


def _store_challenge(challenge: bytes) -> None:
    key = bytes_to_base64url(challenge)
    now = time.time()
    _challenges[key] = (challenge, now)
    # Prune expired entries
    expired = [k for k, (_, ts) in _challenges.items() if now - ts > _CHALLENGE_TTL]
    for k in expired:
        _challenges.pop(k, None)


def _pop_challenge(challenge_b64: str) -> bytes | None:
    entry = _challenges.pop(challenge_b64, None)
    if entry is None:
        return None
    challenge, ts = entry
    if time.time() - ts > _CHALLENGE_TTL:
        return None
    return challenge


# ---------------------------------------------------------------------------
# WebAuthn handlers
# ---------------------------------------------------------------------------


def get_registration_options_json(rp_id: str) -> str:
    options = generate_registration_options(
        rp_id=rp_id,
        rp_name="Otto",
        user_name="admin",
    )
    _store_challenge(options.challenge)
    return options_to_json(options)


def verify_registration(
    response_json: dict[str, Any],
    expected_origin: str,
    rp_id: str,
    storage: PasskeyStorage,
) -> bool:
    challenge_b64 = None

    # Extract challenge from clientDataJSON
    client_data_b64 = response_json.get("response", {}).get("clientDataJSON", "")
    try:
        client_data = json.loads(_b64url_decode(client_data_b64))
        challenge_b64 = client_data.get("challenge", "")
    except Exception:
        pass

    if not challenge_b64:
        return False

    expected_challenge = _pop_challenge(challenge_b64)
    if expected_challenge is None:
        return False

    try:
        verification = verify_registration_response(
            credential=response_json,
            expected_challenge=expected_challenge,
            expected_rp_id=rp_id,
            expected_origin=expected_origin,
        )
    except Exception as exc:
        log.warning("registration verification failed", error=str(exc))
        return False

    storage.save_credential(
        {
            "id": bytes_to_base64url(verification.credential_id),
            "public_key": bytes_to_base64url(verification.credential_public_key),
            "sign_count": verification.sign_count,
        }
    )
    return True


def get_authentication_options_json(storage: PasskeyStorage, rp_id: str) -> str:
    creds = storage.get_credentials()
    allow_credentials = [
        PublicKeyCredentialDescriptor(id=_b64url_decode(cred.get("id", ""))) for cred in creds
    ]

    options = generate_authentication_options(
        rp_id=rp_id,
        allow_credentials=allow_credentials if allow_credentials else None,
    )
    _store_challenge(options.challenge)
    return options_to_json(options)


def verify_authentication(
    response_json: dict[str, Any],
    expected_origin: str,
    rp_id: str,
    storage: PasskeyStorage,
) -> bool:
    raw_id = response_json.get("rawId", "")
    cred_id_b64 = response_json.get("id", raw_id)

    # Find stored credential
    creds = storage.get_credentials()
    stored = None
    for cred in creds:
        if cred["id"] == cred_id_b64:
            stored = cred
            break
    if stored is None:
        return False

    public_key = _b64url_decode(stored["public_key"])

    # Extract challenge from clientDataJSON
    client_data_b64 = response_json.get("response", {}).get("clientDataJSON", "")
    challenge_b64 = None
    try:
        client_data = json.loads(_b64url_decode(client_data_b64))
        challenge_b64 = client_data.get("challenge", "")
    except Exception:
        pass

    if not challenge_b64:
        return False

    expected_challenge = _pop_challenge(challenge_b64)
    if expected_challenge is None:
        return False

    try:
        verification = verify_authentication_response(
            credential=response_json,
            expected_challenge=expected_challenge,
            expected_rp_id=rp_id,
            expected_origin=expected_origin,
            credential_public_key=public_key,
            credential_current_sign_count=stored.get("sign_count", 0),
        )
    except Exception as exc:
        log.warning("authentication verification failed", error=str(exc))
        return False

    # Update sign count
    stored["sign_count"] = verification.new_sign_count
    storage.update_credentials(creds)
    return True
