"""Bounded, operator-invoked Phase 5B apply execution.

This module deliberately accepts the authoritative plan and Cloud operations as
callables.  It owns admission, fingerprint guards, readback, convergence, and
failure semantics; Core/Cloud adapters remain responsible for transport and
identity metadata construction.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Awaitable, Callable, Mapping

from .reconciliation import ReconciliationOperation, ReconciliationPlan, fingerprint, is_stale_plan


@dataclass(frozen=True)
class ApplyResult:
    project_id: str
    status: str
    attempted: int
    succeeded: int
    failed: int
    before_fingerprint: str
    planned_after_fingerprint: str
    after_fingerprint: str | None
    second_run: str
    operations: tuple[dict[str, Any], ...]


class ApplyRefused(RuntimeError):
    pass


def _inherited_users(users: list[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
    return [user for user in users if user.get("user_metadata", {}).get("source") in {
        "workspace_owner", "workspace_member", "workspace_inherited",
    } or user.get("user_metadata", {}).get("p2hs_authority", {}).get("source") == "workspace"]


def _matches_after(users: list[Mapping[str, Any]], operation: ReconciliationOperation) -> bool:
    from .reconciliation import _email_key

    for user in _inherited_users(users):
        if _email_key(str(user.get("email") or "")) != operation.stable_identity:
            continue
        if operation.field == "role":
            return user.get("role") == operation.after
        if operation.field == "identity":
            return user.get("role") == operation.after.get("role") and user.get("status", "active") == operation.after.get("status", "active")
    return False


async def execute_bounded_plan(
    *,
    plan: ReconciliationPlan,
    approved_project_ids: set[str],
    current_cloud_users: list[Mapping[str, Any]],
    mutate: Callable[[ReconciliationOperation], Awaitable[None]],
    read_cloud_users: Callable[[], Awaitable[list[Mapping[str, Any]]]],
    replan: Callable[[list[Mapping[str, Any]]], ReconciliationPlan],
) -> ApplyResult:
    """Apply only an already-approved, freshly-derived plan.

    The caller must derive Core state and the plan immediately before calling
    this function.  No persistence or queue/Redis access occurs here.
    """
    if plan.project_id not in approved_project_ids:
        raise ApplyRefused("project_not_allowlisted")
    if plan.blocked:
        raise ApplyRefused("plan_blocked")
    if any(operation.field not in {"identity", "role"} for operation in plan.operations):
        raise ApplyRefused("unexpected_operation_type")
    if any(not operation.endpoint.endswith("/auth/users") for operation in plan.operations):
        raise ApplyRefused("non_canonical_auth_endpoint")
    if is_stale_plan(plan, current_cloud_users=current_cloud_users):
        raise ApplyRefused("before_fingerprint_changed")

    results: list[dict[str, Any]] = []
    succeeded = 0
    for operation in plan.operations:
        try:
            await mutate(operation)
            after_users = await read_cloud_users()
            if not _matches_after(after_users, operation):
                results.append({"stable_identity": operation.stable_identity, "status": "readback_mismatch"})
                return ApplyResult(plan.project_id, "RECONCILIATION_FAILED", len(results), succeeded, 1,
                                   plan.before_fingerprint, plan.expected_fingerprint,
                                   fingerprint(after_users), "NOT_RUN", tuple(results))
            succeeded += 1
            results.append({"stable_identity": operation.stable_identity, "status": "succeeded"})
        except Exception as exc:
            results.append({"stable_identity": operation.stable_identity, "status": "failed", "error": type(exc).__name__})
            after_users = await read_cloud_users()
            return ApplyResult(plan.project_id, "PARTIAL_CONVERGENCE" if succeeded else "FAILED",
                               len(results), succeeded, 1, plan.before_fingerprint,
                               plan.expected_fingerprint, fingerprint(after_users), "NOT_RUN", tuple(results))

    after_users = await read_cloud_users()
    after_plan = replan(after_users)
    if after_plan.before_fingerprint != plan.expected_fingerprint or after_plan.operations or after_plan.findings:
        return ApplyResult(plan.project_id, "RECONCILIATION_FAILED", len(results), succeeded, 1,
                           plan.before_fingerprint, plan.expected_fingerprint,
                           after_plan.before_fingerprint, "NOT_RUN", tuple(results))
    second_plan = replan(after_users)
    second_run = "NO_OP" if not second_plan.operations and not second_plan.findings else "NOT_CONVERGED"
    return ApplyResult(plan.project_id, "CONVERGED" if second_run == "NO_OP" else "RECONCILIATION_FAILED",
                       len(results), succeeded, 0, plan.before_fingerprint,
                       plan.expected_fingerprint, after_plan.before_fingerprint,
                       second_run, tuple(results))
