"""Provider-neutral authentication vocabulary shared by Core projections.

``providers`` is retained at the HTTP/database boundary for compatibility, but
it is not an executable authentication policy.  A generated-app principal has
an identity channel, an authentication method, an optional configured
connection, an audience, and an authorization role.
"""
from __future__ import annotations

import hashlib
import json
from typing import Any


AUTH_METHODS = {"password", "passwordless", "oidc", "ldap"}
IDENTITY_CHANNELS = {"email", "oidc", "ldap"}
AUDIENCES = {"staff", "customer"}


def authz_policy_fingerprint(policy: dict[str, Any]) -> str:
    """Return a stable non-secret digest for the projected authorization policy."""
    canonical = json.dumps(policy, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def legacy_auth_projection(providers: list[str] | None) -> dict[str, Any]:
    """Project the pre-explicit-policy provider vocabulary into the v130 rule.

    This ordering is a compatibility contract, not a heuristic: ``email`` and
    ``magic_link`` mean passwordless authentication, while password auth is
    enabled only by an explicit ``password`` provider.  It mirrors the
    historical v130/v131 migrations so existing generated projects can be
    classified without inferring credential state from provider names.
    """
    values = {str(value).strip().lower() for value in (providers or [])}
    methods: list[str] = []
    channels: list[str] = []
    connections: list[dict[str, Any]] = []
    if "password" in values:
        methods.append("password")
        channels.append("email")
    if "email" in values or "magic_link" in values:
        methods.append("passwordless")
        if "email" not in channels:
            channels.append("email")
        connections.append({"id": "local_magic_link", "method": "passwordless", "channel": "email"})
    if "google" in values:
        methods.append("oidc")
        channels.append("oidc")
        connections.append({"id": "google", "method": "oidc", "channel": "oidc"})
    return {
        "identity_channels": channels,
        "auth_methods": methods,
        "connections": connections,
        "audiences": ["staff", "customer"],
    }


def normalize_auth_contract(
    *,
    providers: list[str] | None = None,
    identity_channels: list[str] | None = None,
    auth_methods: list[str] | None = None,
    connections: list[dict[str, Any]] | None = None,
    audiences: list[str] | None = None,
) -> dict[str, Any]:
    legacy = legacy_auth_projection(providers)
    methods = [str(v).strip().lower() for v in (auth_methods or legacy["auth_methods"])]
    channels = [str(v).strip().lower() for v in (identity_channels or legacy["identity_channels"])]
    return {
        "identity_channels": list(dict.fromkeys(v for v in channels if v in IDENTITY_CHANNELS)),
        "auth_methods": list(dict.fromkeys(v for v in methods if v in AUTH_METHODS)),
        "connections": connections if connections is not None else legacy["connections"],
        "audiences": list(dict.fromkeys(v for v in (audiences or legacy["audiences"]) if v in AUDIENCES)),
    }


def canonical_auth_target(
    *,
    providers: list[str] | None,
    auth_methods: list[str] | None,
    audiences: list[str] | None,
    allow_signup: bool,
    admin_route: str,
    customer_route: str,
) -> dict[str, Any]:
    """Build the executable migration target from one canonical auth contract.

    Legacy provider values are resolved through ``normalize_auth_contract``;
    callers must not infer password support from credential storage or events.
    Those are conformance/drift signals only.
    """
    contract = normalize_auth_contract(
        providers=providers,
        auth_methods=auth_methods,
        audiences=audiences,
    )
    methods = contract["auth_methods"]
    return {
        "staff": {
            "enabled": "staff" in contract["audiences"],
            "methods": list(methods),
            "signup": False,
            "password_reset": "password" in methods,
        },
        "customer": {
            "enabled": "customer" in contract["audiences"],
            "methods": list(methods),
            "signup": bool(allow_signup),
            "password_reset": "password" in methods,
        },
        "admin_route": admin_route,
        "customer_route": customer_route,
        "identity_channels": contract["identity_channels"],
        "connections": contract["connections"],
        "inherited_role_policy": "workspace owner/admin only",
        "session_target": "httponly_cookie",
    }
