"""Authentication middleware that emits audit-friendly auth events.

This service encapsulates bearer-token authentication checks and emits
``AUTH_SUCCESS`` / ``AUTH_FAILURE`` domain events through an injected
``EventBus`` so the audit pipeline can persist login outcomes.

Spec alignment:
    - docs/specification.md § Event System
    - docs/specification.md § Audit Logging (auth_success / auth_failure)
"""

from __future__ import annotations

import hmac
from typing import TYPE_CHECKING

import structlog

from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.types import EventType
from cleveragents.shared.redaction import redact_value

if TYPE_CHECKING:
    from cleveragents.infrastructure.events.protocol import EventBus

_logger = structlog.get_logger(__name__)
_TOKEN_PREFIX_LEN = 6
_SHORT_TOKEN_MASK = "***..."


def _token_prefix(token: str) -> str:
    """Return a safe token prefix for audit details.

    Never returns the full token value. Tokens shorter than or equal to the
    prefix length are collapsed to a constant mask to avoid full disclosure.
    """
    if len(token) <= _TOKEN_PREFIX_LEN:
        return _SHORT_TOKEN_MASK
    return f"{token[:_TOKEN_PREFIX_LEN]}..."


def _normalize_optional_text(field_name: str, value: str | None) -> str | None:
    """Normalize optional text fields with guard-first validation."""
    if value is None:
        return None
    if not isinstance(value, str):
        raise TypeError(f"{field_name} must be a string or None")
    normalized = value.strip()
    if not normalized:
        raise ValueError(f"{field_name} must not be empty when provided")
    return normalized


class TokenAuthMiddleware:
    """Authenticate bearer tokens and emit AUTH_* events.

    This class is intentionally transport-agnostic. A future server-mode
    middleware can delegate authentication decisions to this component and
    inherit consistent event emission semantics.
    """

    def __init__(
        self,
        expected_token: str | None,
        event_bus: EventBus | None = None,
    ) -> None:
        """Initialize middleware with an expected token and optional event bus.

        Args:
            expected_token: Configured bearer token for successful auth.
            event_bus: Event bus used for AUTH_SUCCESS/AUTH_FAILURE emission.
        """
        self._expected_token = _normalize_optional_text(
            "expected_token", expected_token
        )
        self._event_bus = event_bus

    def authenticate(
        self,
        token: str,
        *,
        identity: str | None = None,
        ip_address: str | None = None,
    ) -> bool:
        """Authenticate *token* and emit a corresponding auth event.

        Args:
            token: Bearer token supplied by the caller.
            identity: Optional user identity / principal for audit context.
            ip_address: Optional client IP address for audit context.

        Returns:
            ``True`` if authentication succeeds, otherwise ``False``.
        """
        if not isinstance(token, str):
            raise TypeError("token must be a string")
        normalized_token = token.strip()
        if not normalized_token:
            raise ValueError("token must not be empty")

        normalized_identity = _normalize_optional_text("identity", identity)
        normalized_ip = _normalize_optional_text("ip_address", ip_address)

        if self._expected_token is None:
            self._emit_auth_failure(
                attempted_identity=normalized_identity,
                ip_address=normalized_ip,
                token_prefix=_token_prefix(normalized_token),
                reason="token_not_configured",
            )
            return False

        if hmac.compare_digest(normalized_token, self._expected_token):
            self._emit_auth_success(
                user_identity=normalized_identity,
                ip_address=normalized_ip,
                token_prefix=_token_prefix(normalized_token),
            )
            return True

        self._emit_auth_failure(
            attempted_identity=normalized_identity,
            ip_address=normalized_ip,
            token_prefix=_token_prefix(normalized_token),
            reason="invalid_token",
        )
        return False

    def _emit_auth_success(
        self,
        *,
        user_identity: str | None,
        ip_address: str | None,
        token_prefix: str,
    ) -> None:
        """Emit AUTH_SUCCESS if an event bus is configured."""
        if self._event_bus is None:
            return
        details: dict[str, str] = {
            "user_identity": user_identity or "unknown",
            "token_prefix": token_prefix,
        }
        if ip_address is not None:
            details["ip_address"] = ip_address
        self._emit_event(EventType.AUTH_SUCCESS, details)

    def _emit_auth_failure(
        self,
        *,
        attempted_identity: str | None,
        ip_address: str | None,
        token_prefix: str,
        reason: str,
    ) -> None:
        """Emit AUTH_FAILURE if an event bus is configured."""
        if self._event_bus is None:
            return
        details: dict[str, str] = {
            "attempted_identity": attempted_identity or "unknown",
            "user_identity": "unauthenticated",
            "failure_reason": reason,
            "token_prefix": token_prefix,
        }
        if ip_address is not None:
            details["ip_address"] = ip_address
        self._emit_event(EventType.AUTH_FAILURE, details)

    def _emit_event(self, event_type: EventType, details: dict[str, str]) -> None:
        """Emit one auth event and swallow publish-side failures."""
        bus = self._event_bus
        if bus is None:
            return
        try:
            bus.emit(
                DomainEvent(
                    event_type=event_type,
                    details=details,
                )
            )
        except Exception as exc:  # pragma: no cover - defensive logging path
            _logger.warning(
                "auth_event_emit_failed",
                event_type=event_type.value,
                error_type=type(exc).__name__,
                error_message=redact_value(str(exc)),
            )


__all__ = ["TokenAuthMiddleware"]
