"""Auth token generation and verification for session authentication."""

from __future__ import annotations

import hmac
import logging
import secrets
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta

from summon_claude.sessions.registry import _MAX_FAILED_ATTEMPTS, SessionRegistry

logger = logging.getLogger(__name__)

_TOKEN_TTL_MINUTES = 5


@dataclass(frozen=True)
class SessionAuth:
    """Authentication token pair for a summon session."""

    short_code: str
    session_id: str
    expires_at: datetime


async def generate_session_token(
    registry: SessionRegistry,
    session_id: str,
) -> SessionAuth:
    """Generate a cryptographic token and human-friendly short code for a session.

    Stores the mapping in SQLite so any process can verify the short code
    (since Socket Mode load-balances slash commands across connections).
    """
    short_code = secrets.token_hex(4)
    expires_at = datetime.now(UTC) + timedelta(minutes=_TOKEN_TTL_MINUTES)

    await registry.store_pending_token(
        short_code=short_code,
        session_id=session_id,
        expires_at=expires_at.isoformat(),
    )

    logger.debug("Generated auth token for session %s", session_id)
    return SessionAuth(
        short_code=short_code,
        session_id=session_id,
        expires_at=expires_at,
    )


async def verify_short_code(registry: SessionRegistry, code: str) -> SessionAuth | None:
    """Verify a short code and return the SessionAuth if valid.

    Uses constant-time comparison across all pending tokens to prevent
    timing side-channel attacks. Atomically deletes the token on success —
    concurrent callers cannot both succeed for the same code (no TOCTOU race).
    """
    code = code.strip().lower()
    now = datetime.now(UTC)

    # Fetch all non-expired pending tokens (small set, typically 0-3)
    all_pending = await registry.get_all_pending_tokens()

    # Iterate ALL entries unconditionally — no early exit — to prevent
    # timing side-channels from revealing which entry matched.
    match = None
    found_expired = False
    found_locked = False
    for entry in all_pending:
        codes_equal = hmac.compare_digest(entry["short_code"].encode(), code.encode())
        if codes_equal:
            if datetime.fromisoformat(entry["expires_at"]) > now:
                if entry.get("failed_attempts", 0) < _MAX_FAILED_ATTEMPTS:
                    match = entry
                else:
                    found_locked = True
            else:
                found_expired = True
            # Do NOT break — always iterate all entries for constant-time behavior

    if not match:
        if found_expired:
            # Accepted timing risk: expired tokens hit delete while unknown codes hit
            # record_failed_auth_attempt. Expired tokens are already useless since
            # atomic_consume_pending_token enforces expiry independently.
            await registry.delete_pending_token(code)
        elif not found_locked:
            # Only record a failure if the code is unknown; locked tokens are already
            # at max attempts and incrementing further would be incorrect.
            await registry.record_failed_auth_attempt(code)
        # If found_locked: do nothing — token is already at max failed attempts
        logger.debug("Auth short code not found, expired, or locked out")
        return None

    # Atomically consume it — concurrent callers cannot both succeed
    consumed = await registry.atomic_consume_pending_token(code, now.isoformat())
    if not consumed:
        return None

    expires_at = datetime.fromisoformat(consumed["expires_at"])
    logger.info("Auth short code verified for session %s", consumed["session_id"])

    return SessionAuth(
        short_code=code,
        session_id=consumed["session_id"],
        expires_at=expires_at,
    )
