"""Pure domain services for Identity — role policy + access-token claims.

These are decisions, not side effects: they take plain data and return plain data
or an action enum. Persisting the decision (DB writes) and signing the token live in
infrastructure. Mirrors ``auth_api._policy_admin_emails``, ``_reconcile_policy_admin``
and ``_issue_token``.
"""

from __future__ import annotations

import uuid
from datetime import datetime, timedelta
from enum import Enum, auto
from typing import Any

# Default policy admins when AUTH_ADMIN_EMAILS is unset
# (must stay in sync with migration 007 cleanup list + auth_api._DEFAULT_POLICY_ADMIN_EMAILS).
DEFAULT_POLICY_ADMIN_EMAILS: frozenset[str] = frozenset(
    {
        "thaontt@ump.edu.vn",
        "nltanh@ump.edu.vn",
        "ldbaochau@ump.edu.vn",
        "htchuong@ump.edu.vn",
    }
)


def policy_admin_emails(auth_admin_emails_env: str | None) -> frozenset[str]:
    """Emails that receive ``admin`` from institutional policy.

    If ``AUTH_ADMIN_EMAILS`` is set, ONLY that comma-separated list applies
    (lowercased). If unset, the built-in UMP allow-list applies. Pure — the env
    value is passed in by the caller (composition layer), not read here.
    """
    raw = (auth_admin_emails_env or "").strip()
    if raw:
        return frozenset(part.strip().lower() for part in raw.split(",") if part.strip())
    return DEFAULT_POLICY_ADMIN_EMAILS


class AdminReconcileAction(Enum):
    """What to do with a user's ``admin`` role row given email policy."""

    none = auto()
    add_admin = auto()     # policy admin, no row → INSERT (admin_from_email_policy=True)
    mark_policy = auto()   # policy admin, row exists → ensure admin_from_email_policy=True
    remove_admin = auto()  # not policy admin, policy-granted row exists → DELETE


def reconcile_admin_action(
    email: str,
    policy_admins: frozenset[str],
    has_admin_row: bool,
    admin_from_policy: bool,
) -> AdminReconcileAction:
    """Pure decision mirroring ``auth_api._reconcile_policy_admin``.

    Manual admin rows (``admin_from_email_policy=False``) are preserved when the
    email is not allow-listed.
    """
    email_norm = email.strip().lower()
    if email_norm in policy_admins:
        return (
            AdminReconcileAction.mark_policy
            if has_admin_row
            else AdminReconcileAction.add_admin
        )
    if has_admin_row and admin_from_policy:
        return AdminReconcileAction.remove_admin
    return AdminReconcileAction.none


def build_access_token_claims(
    user_id: uuid.UUID,
    email: str,
    roles: list[str],
    credential_version: int,
    now: datetime,
    expire_hours: int,
) -> dict[str, Any]:
    """Build HS256 JWT claims (mirror of ``auth_api._issue_token``); signing is infra.

    A session-scoped token carries exactly one identity + the active roles + ``cv``
    (credential version) so a password change invalidates it.
    """
    exp = now + timedelta(hours=int(expire_hours))
    return {
        "sub": str(user_id),
        "email": email,
        "roles": roles,
        "cv": int(credential_version),
        "iat": int(now.timestamp()),
        "exp": int(exp.timestamp()),
    }
