"""Versioned staff-policy delivery for CRM, product-catalog, order-management, and billing-management.

11E2 permits upgrading only the absence of a lead role policy. product-catalog
reuses the same live verification; it does not inherit the lead historical
upgrade. Cloud retains its global compatibility behavior. Explicit
custom/malformed policies block.
"""
from __future__ import annotations

from copy import deepcopy
import json
import re

from src.integrations.cloud_control_client import CloudControlClient

LEAD_STAFF_VERSION = "1.1.0"
CRM_AUTHORIZATION_CONFLICT = "CRM_AUTHORIZATION_CONFLICT"
CRM_AUTHORIZATION_UNVERIFIED = "CRM_AUTHORIZATION_UNVERIFIED"
_ACTIONS = ("create", "read", "update", "delete")


def staff_policy() -> dict:
    return {"version": 1, "roles": {
        role: {action: {"effect": "allow", "scope": "all"} for action in _ACTIONS}
        for role in ("admin", "super_admin")
    }}


class CrmAuthorizationError(ValueError):
    def __init__(self, entity_key: str, reason: str, *, conflict: bool = False):
        super().__init__(
            f"CRM {entity_key} authorization {reason}; canonical staff-managed policy required."
        )
        self.entity_key = entity_key
        self.reason = reason
        self.code = CRM_AUTHORIZATION_CONFLICT if conflict else CRM_AUTHORIZATION_UNVERIFIED

    def as_blocker(self) -> dict:
        return {"errors": [str(self)], "validation_code": self.code,
                "entity_key": self.entity_key, "reason": self.reason}


def classify_policy(schema: dict) -> str:
    """Compare effective grants, accepting explicit denials but no unknown shapes."""
    if "access_policy" not in schema:
        return "historical_no_policy"
    policy = schema["access_policy"]
    if (not isinstance(policy, dict) or set(policy) != {"version", "roles"}
            or type(policy["version"]) is not int or policy["version"] != 1
            or not isinstance(policy["roles"], dict)):
        return "malformed_policy"
    grants = set()
    for role, actions in policy["roles"].items():
        if not isinstance(role, str) or not role or not isinstance(actions, dict):
            return "malformed_policy"
        for action, entry in actions.items():
            if (action not in _ACTIONS or not isinstance(entry, dict)
                    or set(entry) - {"effect", "scope"}
                    or entry.get("effect") not in ("allow", "deny")
                    or ("scope" in entry and entry["scope"] not in ("all", "owner"))):
                return "malformed_policy"
            if entry["effect"] == "allow":
                grants.add((role, action, entry.get("scope", "owner")))
    canonical = {(role, action, "all") for role in ("admin", "super_admin") for action in _ACTIONS}
    return "canonical_policy" if grants == canonical else "conflicting_policy"


def requires_crm_authorization(slug: str, version: str) -> str | None:
    if slug == "customer-records":
        return "customer"
    if slug == "product-catalog":
        return "catalog_item"
    if slug == "order-management":
        return "order_record"
    if slug == "billing-management":
        return "invoice_record"
    if slug == "inventory-management":
        return "inventory_record"
    if slug == "membership-management":
        return "membership_record"
    if slug != "lead-lifecycle":
        return None
    match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", version or "")
    # Unknown version cannot safely opt out of a security requirement.
    if not match:
        raise CrmAuthorizationError("lead", "has an unreadable pack version")
    if tuple(map(int, match.groups())) < (1, 1, 0):
        raise CrmAuthorizationError("lead", "requires catalog upgrade to 1.1.0")
    return "lead"


def require_desired_policy(composition: dict, entity_key: str) -> None:
    matches = [e for e in composition.get("entities", [])
               if isinstance(e, dict) and e.get("entity_key") == entity_key]
    if (len(matches) != 1 or not isinstance(matches[0].get("schema"), dict)
            or classify_policy(matches[0]["schema"]) != "canonical_policy"):
        raise CrmAuthorizationError(entity_key, "missing from required pack composition")


def _read_schema(payload: dict, project_id: str, entity_key: str) -> tuple[dict, dict]:
    entity = payload.get("entity")
    if (not isinstance(entity, dict) or entity.get("project_id") != project_id
            or entity.get("entity_key") != entity_key or entity.get("status") != "active"):
        raise CrmAuthorizationError(entity_key, "live entity unavailable or mismatched")
    try:
        schema = json.loads(entity["schema_json"])
    except (KeyError, TypeError, ValueError) as exc:
        raise CrmAuthorizationError(entity_key, "live schema unreadable") from exc
    if not isinstance(schema, dict) or not isinstance(schema.get("fields"), list):
        raise CrmAuthorizationError(entity_key, "live schema malformed")
    return entity, schema


async def ensure_installed_policy(
    cloud: CloudControlClient, project_id: str, entity_key: str, *, allow_legacy_upgrade: bool,
) -> None:
    """Read, classify, conditional merge, then verify. Retry converges from live truth."""
    payload = await cloud.get_project_entity(project_id, entity_key)
    entity, schema = _read_schema(payload, project_id, entity_key)
    state = classify_policy(schema)
    if state == "canonical_policy":
        return
    if state in ("conflicting_policy", "malformed_policy"):
        raise CrmAuthorizationError(entity_key, state, conflict=True)
    if not allow_legacy_upgrade or entity_key != "lead":
        raise CrmAuthorizationError(entity_key, "installed policy absent")
    version = entity.get("version")
    if (payload.get("schema_update_precondition") != "entity_version"
            or type(version) is not int or version < 1):
        raise CrmAuthorizationError(entity_key, "conditional schema update unavailable")
    merged = deepcopy(schema)
    merged["access_policy"] = staff_policy()
    await cloud.patch_project_entity(project_id, entity_key, {"schema": merged}, expected_version=version)
    verified = await cloud.get_project_entity(project_id, entity_key)
    after_entity, after = _read_schema(verified, project_id, entity_key)
    if classify_policy(after) != "canonical_policy":
        raise CrmAuthorizationError(entity_key, "post-patch policy verification failed")
    # A successful acknowledgement alone is not evidence that the merge survived.
    unchanged = {k: v for k, v in after.items() if k != "access_policy"}
    if unchanged != schema or any(after_entity.get(k) != entity.get(k) for k in (
        "rls_enabled", "owner_managed", "owner_singleton", "settings_json",
    )):
        raise CrmAuthorizationError(entity_key, "post-patch schema preservation failed")
