"""Read-only planning primitives for Core -> Cloud staff reconciliation.

This module deliberately has no HTTP client or database dependency.  It builds
an explainable plan from authoritative Core identities and a Cloud user
readback.  Mutation remains behind the existing project-auth synchronization
path until an operator approves a plan.
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from typing import Any, Mapping

from .staff_directory import WorkspaceAppIdentity

_INHERITED_SOURCES = {"workspace_owner", "workspace_member", "workspace_inherited"}


@dataclass(frozen=True)
class ReconciliationOperation:
    resource: str
    stable_identity: str
    field: str
    before: Any
    after: Any
    authority: str
    endpoint: str
    idempotency_key: str
    session_consequence: str = "UNKNOWN"


@dataclass(frozen=True)
class ReconciliationPlan:
    project_id: str
    workspace_id: str
    before_fingerprint: str
    expected_fingerprint: str
    operations: tuple[ReconciliationOperation, ...]
    findings: tuple[str, ...]
    blocked: tuple[str, ...] = ()


def _canonical(value: Any) -> str:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)


def fingerprint(value: Any) -> str:
    return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()


def _email_key(email: str) -> str:
    return hashlib.sha256(email.strip().lower().encode("utf-8")).hexdigest()[:16]


def _metadata(user: Mapping[str, Any]) -> Mapping[str, Any]:
    value = user.get("user_metadata")
    return value if isinstance(value, Mapping) else {}


def _is_inherited(user: Mapping[str, Any]) -> bool:
    metadata = _metadata(user)
    source = str(metadata.get("source") or "").strip().lower()
    authority = metadata.get("p2hs_authority")
    return source in _INHERITED_SOURCES or (
        isinstance(authority, Mapping) and authority.get("source") == "workspace"
    )


def _state(users: list[Mapping[str, Any]]) -> list[dict[str, Any]]:
    """Return only workspace-derived identities, excluding app-local users."""

    result = []
    for user in users:
        email = str(user.get("email") or "").strip().lower()
        if not email or not _is_inherited(user):
            continue
        result.append(
            {
                "identity": _email_key(email),
                "role": str(user.get("role") or ""),
                "status": str(user.get("status") or "active"),
                "source": str(_metadata(user).get("source") or ""),
                "workspace_role": str(_metadata(user).get("workspace_role") or ""),
            }
        )
    return sorted(result, key=lambda item: item["identity"])


def _expected(identities: list[WorkspaceAppIdentity]) -> list[dict[str, Any]]:
    return sorted(
        [
            {
                "identity": _email_key(identity.email),
                "role": identity.app_role,
                "status": "active",
                "source": "workspace_owner" if identity.is_owner else "workspace_member",
                "workspace_role": identity.workspace_role,
            }
            for identity in identities
        ],
        key=lambda item: item["identity"],
    )


def build_reconciliation_plan(
    *,
    project_id: str,
    workspace_id: str,
    identities: list[WorkspaceAppIdentity],
    cloud_users: list[Mapping[str, Any]],
) -> ReconciliationPlan:
    """Build a deterministic plan without performing any mutation.

    A same-email Cloud user without workspace provenance is treated as a
    collision and blocks the operation.  It is never converted implicitly.
    """

    expected = _expected(identities)
    actual = _state(cloud_users)
    before = {item["identity"]: item for item in actual}
    desired = {item["identity"]: item for item in expected}
    all_cloud_by_identity = {
        _email_key(str(user.get("email") or "")): user
        for user in cloud_users
        if str(user.get("email") or "").strip()
    }
    operations: list[ReconciliationOperation] = []
    blocked: list[str] = []

    for identity, target in desired.items():
        current = before.get(identity)
        if current is None:
            collision = all_cloud_by_identity.get(identity)
            if collision is not None and not _is_inherited(collision):
                blocked.append(f"identity_collision:{identity}")
                continue
            operations.append(
                ReconciliationOperation(
                    resource="cloud_inherited_identity",
                    stable_identity=identity,
                    field="identity",
                    before="absent",
                    after={"role": target["role"], "status": target["status"]},
                    authority="Core workspace membership + canonical workspace role mapping",
                    endpoint=f"POST /v1/projects/{project_id}/auth/users",
                    idempotency_key=f"project-auth-reconcile:{project_id}:{identity}:upsert",
                )
            )
            continue
        if current["role"] != target["role"]:
            operations.append(
                ReconciliationOperation(
                    resource="cloud_inherited_identity",
                    stable_identity=identity,
                    field="role",
                    before=current["role"],
                    after=target["role"],
                    authority="Core workspace membership + canonical workspace role mapping",
                    # Cloud deliberately rejects the public role endpoint for
                    # workspace-managed identities.  The Core-authorized
                    # upsert path carries the provenance marker that permits
                    # this correction; the apply layer resolves the email
                    # from Core and supplies the full canonical metadata.
                    endpoint=f"POST /v1/projects/{project_id}/auth/users",
                    idempotency_key=f"project-auth-reconcile:{project_id}:{identity}:role",
                )
            )
        if current["status"] != target["status"]:
            operations.append(
                ReconciliationOperation(
                    resource="cloud_inherited_identity",
                    stable_identity=identity,
                    field="status",
                    before=current["status"],
                    after=target["status"],
                    authority="Core workspace membership status",
                    endpoint=(
                        f"PATCH /v1/projects/{project_id}/auth/users/"
                        f"{current['cloud_user_id']}"
                    ),
                    idempotency_key=f"project-auth-reconcile:{project_id}:{identity}:status",
                )
            )

    expected_fp = fingerprint(expected)
    before_fp = fingerprint(actual)
    findings = tuple(
        sorted(
            {
                *(f"missing_inherited:{item['identity']}" for item in expected if item["identity"] not in before),
                *(f"role_mismatch:{item['identity']}" for item in expected if item["identity"] in before and before[item["identity"]]["role"] != item["role"]),
                *(f"status_mismatch:{item['identity']}" for item in expected if item["identity"] in before and before[item["identity"]]["status"] != item["status"]),
                *(f"stale_inherited:{item['identity']}" for item in actual if item["identity"] not in desired),
            }
        )
    )
    return ReconciliationPlan(
        project_id=project_id,
        workspace_id=workspace_id,
        before_fingerprint=before_fp,
        expected_fingerprint=expected_fp,
        operations=tuple(operations),
        findings=findings,
        blocked=tuple(sorted(blocked)),
    )


def is_stale_plan(plan: ReconciliationPlan, *, current_cloud_users: list[Mapping[str, Any]]) -> bool:
    """Return true when Cloud changed since planning; callers must refuse apply."""

    return fingerprint(_state(current_cloud_users)) != plan.before_fingerprint


def plan_as_dict(plan: ReconciliationPlan) -> dict[str, Any]:
    """Serialize sanitized evidence; no email, tokens, or customer content."""

    return {
        "project_id": plan.project_id,
        "workspace_id": plan.workspace_id,
        "before_fingerprint": plan.before_fingerprint,
        "expected_fingerprint": plan.expected_fingerprint,
        "findings": list(plan.findings),
        "blocked": list(plan.blocked),
        "operations": [
            {
                "resource": operation.resource,
                "stable_identity": operation.stable_identity,
                "field": operation.field,
                "before": operation.before,
                "after": operation.after,
                "authority": operation.authority,
                "endpoint": operation.endpoint,
                "idempotency_key": operation.idempotency_key,
                "session_consequence": operation.session_consequence,
            }
            for operation in plan.operations
        ],
    }
