"""
Authentication Strategy Interface.

All concrete providers (Local, LDAP, SAML, OIDC) implement this Protocol.
The Orchestrator calls `authenticate()` on each enabled strategy in order
until one returns a successful `AuthResult` or all fail.

Design notes:
- `authenticate()` MUST NOT return None on failure; raise `AuthError` instead.
  This forces the orchestrator to handle generic failure messages centrally
  and prevents accidental success-on-None paths.
- Strategies are stateless w.r.t. the request; per-request state (e.g. SAML
  RelayState, OIDC nonce/PKCE verifier) is held in a signed session cookie
  outside the strategy.
- `ExternalIdentity` is the strategy's portable output. It does NOT touch
  the database. JIT provisioning is the orchestrator's job.
"""
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any

from app.models.user import AuthProvider, User, UserRole


class AuthError(Exception):
    """
    Raised by strategies when authentication fails.

    `safe_message` is the message we expose to the client. It MUST be generic
    ('Invalid credentials') for credential-based strategies to prevent
    account enumeration. `detail` is for the audit log only.
    """

    def __init__(self, detail: str, safe_message: str = "Invalid credentials"):
        super().__init__(detail)
        self.detail = detail
        self.safe_message = safe_message


@dataclass
class ExternalIdentity:
    """
    Portable identity returned by a non-local strategy.

    The orchestrator maps this to a local User row via JIT provisioning,
    auto-linking by `email` if an existing local user matches.
    """
    provider: AuthProvider
    external_id: str                       # IdP-stable subject (oid / objectGUID / nameID)
    username: str
    email: str
    display_name: Optional[str] = None
    groups: List[str] = field(default_factory=list)  # raw group strings from IdP
    extra: Dict[str, Any] = field(default_factory=dict)  # provider-specific extras


@dataclass
class AuthResult:
    """
    Returned by a successful authentication attempt. Wraps the local
    User row (existing or just JIT-provisioned) along with metadata
    used for audit logging and MFA orchestration.
    """
    user: User
    provider: AuthProvider
    mfa_required: bool = False             # True if local TOTP needed before issuing JWT
    mfa_setup_required: bool = False       # True if MFA-enforcement policy demands enrolment
                                           # before the user is allowed any session at all.
                                           # Mutually exclusive with mfa_required (router
                                           # checks setup first).
    audit_message: str = ""
    raw_identity: Optional[ExternalIdentity] = None


class AuthStrategy(ABC):
    """Abstract base. Concrete strategies subclass and implement `name` + auth methods."""

    name: AuthProvider

    @property
    @abstractmethod
    def is_enabled(self) -> bool:
        """Return True if this provider is configured and ready to serve auth requests."""
        ...

    @abstractmethod
    def authenticate_credentials(
        self, username: str, password: str
    ) -> ExternalIdentity:
        """
        Credential-based authentication (Local, LDAP).

        Raise `AuthError` on any failure (user not found, bad password,
        locked, expired etc.) — always with the same safe_message to defeat
        enumeration. SSO strategies (SAML/OIDC) should raise NotImplementedError.
        """
        ...

    def supports_credentials(self) -> bool:
        """True for strategies usable with username+password (Local, LDAP)."""
        try:
            # Probe: if the subclass overrode this AND didn't raise NotImplementedError
            # in authenticate_credentials, it supports credentials.
            return self.__class__.authenticate_credentials is not AuthStrategy.authenticate_credentials
        except Exception:
            return False
