"""Token-based authentication for the IEUM API server.

Generates a random auth token on startup and validates it on all protected
routes. The token is displayed in the console and saved to ~/.ieum/auth_token.

Disable authentication by setting IEUM_AUTH_DISABLED=true in the environment.
"""

import logging
import os
import re
import secrets
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any

from fastapi import Cookie, Depends, HTTPException, Query, Request, WebSocket, status
from starlette.responses import JSONResponse, RedirectResponse

logger = logging.getLogger("ieum.server.auth")

# ---------------------------------------------------------------------------
# Token lifecycle
# ---------------------------------------------------------------------------

_AUTH_TOKEN: str | None = None
_AUTH_DISABLED: bool = False
_AUTH_TOKEN_EXPIRES_AT: datetime | None = None
_AUTH_TOKEN_TTL_SECONDS: int = 3600

USER_ID_HEADER_CANDIDATES: tuple[str, ...] = (
    "x-ieum-user-id",
    "x-ieum-user",
)
USER_ID_QUERY_PARAM = "user_id"
_USER_ID_SAFE_PATTERN = re.compile(r"[^a-zA-Z0-9_.-]+")

# Paths that never require authentication
PUBLIC_PATHS: set[str] = {
    "/auth",
    "/api/health",
    "/health",
    "/login",
}

# Paths that serve the login page (redirect to main if already authenticated)
LOGIN_PATHS: set[str] = {
    "/",
    "/login",
}


def _token_file_path() -> Path:
    """Return the path to the persistent token file."""
    return Path.home() / ".ieum" / "auth_token"


def _read_token_ttl_seconds() -> int:
    """Read auth token TTL from environment."""
    raw_ttl = os.environ.get("IEUM_AUTH_TOKEN_TTL_SECONDS", "3600")
    try:
        ttl = int(raw_ttl)
    except ValueError:
        ttl = 3600
    # Safety lower bound: 60 seconds
    return max(ttl, 60)


def generate_auth_token(ttl_seconds: int | None = None) -> str:
    """Generate a cryptographically secure auth token, persist it, and return it.

    The token is also printed to the console so the user can copy it.
    """
    global _AUTH_TOKEN, _AUTH_TOKEN_EXPIRES_AT, _AUTH_TOKEN_TTL_SECONDS
    token = secrets.token_urlsafe(32)
    _AUTH_TOKEN = token
    _AUTH_TOKEN_TTL_SECONDS = ttl_seconds if ttl_seconds is not None else _read_token_ttl_seconds()
    _AUTH_TOKEN_EXPIRES_AT = datetime.now(UTC) + timedelta(seconds=_AUTH_TOKEN_TTL_SECONDS)

    # Persist to disk
    token_path = _token_file_path()
    token_path.parent.mkdir(parents=True, exist_ok=True)
    token_path.write_text(token, encoding="utf-8")
    # Restrict file permissions (owner read/write only)
    try:
        token_path.chmod(0o600)
    except OSError:
        pass  # Windows may not support chmod fully

    return token


def get_auth_token() -> str | None:
    """Return the current auth token (None if auth is disabled)."""
    return _AUTH_TOKEN


def get_auth_token_ttl_seconds() -> int:
    """Return the configured auth token TTL in seconds."""
    return _AUTH_TOKEN_TTL_SECONDS


def get_auth_token_expires_at() -> datetime | None:
    """Return the auth-token expiration time in UTC."""
    return _AUTH_TOKEN_EXPIRES_AT


def is_auth_disabled() -> bool:
    """Return True if authentication has been explicitly disabled."""
    return _AUTH_DISABLED


def init_auth() -> str | None:
    """Initialize authentication. Called once at server startup.

    Returns:
        The generated token, or None if auth is disabled.
    """
    global _AUTH_DISABLED, _AUTH_TOKEN

    disabled_env = os.environ.get("IEUM_AUTH_DISABLED", "").lower()
    if disabled_env in ("true", "1", "yes"):
        _AUTH_DISABLED = True
        logger.info("Authentication is DISABLED (IEUM_AUTH_DISABLED=true)")
        return None

    ttl_seconds = _read_token_ttl_seconds()
    token = generate_auth_token(ttl_seconds=ttl_seconds)
    logger.info("Authentication enabled. Token saved to %s", _token_file_path())
    logger.info("Authentication token TTL: %s seconds", ttl_seconds)
    return token


# ---------------------------------------------------------------------------
# Token extraction helpers
# ---------------------------------------------------------------------------


def normalize_user_id(raw_user_id: str | None) -> str:
    """Normalize/sanitize user identity used for session ownership."""
    user_id = (raw_user_id or "").strip()
    if not user_id:
        return "anonymous"
    sanitized = _USER_ID_SAFE_PATTERN.sub("_", user_id)
    sanitized = sanitized.strip("._-")
    if not sanitized:
        return "anonymous"
    return sanitized[:64]


def get_request_user_id(request: Request, *, required: bool = False) -> str:
    """Extract normalized user id from request headers/query."""
    raw_user_id: str | None = None
    for header in USER_ID_HEADER_CANDIDATES:
        value = request.headers.get(header)
        if value:
            raw_user_id = value
            break
    if raw_user_id is None:
        raw_user_id = request.query_params.get(USER_ID_QUERY_PARAM)

    if required and not raw_user_id:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=(
                "Missing user identity. Provide one of headers "
                f"{', '.join(USER_ID_HEADER_CANDIDATES)} or query param '{USER_ID_QUERY_PARAM}'."
            ),
        )
    return normalize_user_id(raw_user_id)


def get_websocket_user_id(websocket: WebSocket, *, required: bool = False) -> str:
    """Extract normalized user id from websocket headers/query."""
    raw_user_id: str | None = None
    for header in USER_ID_HEADER_CANDIDATES:
        value = websocket.headers.get(header)
        if value:
            raw_user_id = value
            break
    if raw_user_id is None:
        raw_user_id = websocket.query_params.get(USER_ID_QUERY_PARAM)

    if required and not raw_user_id:
        raise ValueError(
            "Missing user identity for websocket connection. "
            f"Provide header {USER_ID_HEADER_CANDIDATES[0]} or query '{USER_ID_QUERY_PARAM}'."
        )
    return normalize_user_id(raw_user_id)


def _extract_token_from_request(request: Request) -> str | None:
    """Extract token from Authorization header, query parameter, or cookie."""
    # 1. Authorization: Bearer <token>
    auth_header = request.headers.get("authorization", "")
    if auth_header.lower().startswith("bearer "):
        return auth_header[7:].strip()

    # 2. Query parameter ?token=<token>
    token_param = request.query_params.get("token")
    if token_param:
        return token_param

    # 3. Cookie
    cookie_token = request.cookies.get("ieum_token")
    if cookie_token:
        return cookie_token

    return None


def _extract_token_from_websocket(websocket: WebSocket) -> str | None:
    """Extract token from WebSocket handshake (query param, header, or cookie)."""
    # 1. Query parameter ?token=<token>
    token_param = websocket.query_params.get("token")
    if token_param:
        return token_param

    # 2. Authorization header (some WebSocket clients support this)
    auth_header = websocket.headers.get("authorization", "")
    if auth_header.lower().startswith("bearer "):
        return auth_header[7:].strip()

    # 3. Cookie
    cookie_token = websocket.cookies.get("ieum_token")
    if cookie_token:
        return cookie_token

    return None


def _validate_token(token: str | None) -> bool:
    """Validate a token against the stored auth token."""
    if _AUTH_DISABLED:
        return True
    if _AUTH_TOKEN is None:
        return True
    if _AUTH_TOKEN_EXPIRES_AT is not None and datetime.now(UTC) >= _AUTH_TOKEN_EXPIRES_AT:
        return False
    if token is None:
        return False
    return secrets.compare_digest(token, _AUTH_TOKEN)


# ---------------------------------------------------------------------------
# FastAPI dependencies
# ---------------------------------------------------------------------------

async def require_auth(request: Request) -> None:
    """FastAPI dependency that enforces token authentication.

    Raises HTTPException 401 if the token is missing or invalid.
    Skip validation if auth is disabled.
    """
    if _AUTH_DISABLED:
        return

    token = _extract_token_from_request(request)
    if not _validate_token(token):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or missing authentication token",
            headers={"WWW-Authenticate": "Bearer"},
        )


async def validate_ws_token(websocket: WebSocket) -> bool:
    """Validate the authentication token for a WebSocket connection.

    Returns True if valid, False otherwise. Does NOT raise -- the caller
    should close the WebSocket with an appropriate code.
    """
    if _AUTH_DISABLED:
        return True
    token = _extract_token_from_websocket(websocket)
    return _validate_token(token)


def is_request_authenticated(request: Request) -> bool:
    """Check if a request carries a valid token (non-raising)."""
    if _AUTH_DISABLED:
        return True
    token = _extract_token_from_request(request)
    return _validate_token(token)
