"""Copyable Python helper for gating side-effecting AI actions.

This is an example helper, not a packaged SDK. It uses only the Python standard
library so product teams can paste it into an OpenAI Agents, LangChain, browser
backend, or internal workflow service without pulling a dependency.
"""
from __future__ import annotations

from dataclasses import dataclass
import hashlib
import json
from typing import Any, Callable, Mapping
from urllib import request
from urllib.error import HTTPError


JsonObject = dict[str, Any]
Transport = Callable[[str, dict[str, str], JsonObject, float], JsonObject]

LAW_MIRROR_CONTRACT = "planisphere.law_grading_mirror"
LAW_MIRROR_VERSION = "1"
LAW_GRADE_VALUES = ("PASS", "PARTIAL", "FAIL", "NA")
LAW_MIRROR_CATEGORIES = (
    {
        "id": "citation_support",
        "label": "Citation support",
        "pass_means": "Citations were checked against named authorities.",
        "partial_means": "Some authorities were checked, or citation scope is incomplete.",
        "fail_means": "Citation support is missing, unchecked, or unsuitable for use.",
    },
    {
        "id": "privilege_boundary",
        "label": "Privilege boundary",
        "pass_means": "Privilege/work-product and client-confidentiality boundary is clear.",
        "partial_means": "Boundary is plausible but needs reviewer confirmation.",
        "fail_means": "Privileged or client-confidential material may cross a boundary.",
    },
    {
        "id": "supervision_route",
        "label": "Supervision route",
        "pass_means": "Responsible reviewer and route are explicit.",
        "partial_means": "Reviewer exists but ownership or route is incomplete.",
        "fail_means": "No responsible review owner is identified.",
    },
    {
        "id": "client_confidentiality",
        "label": "Client confidentiality",
        "pass_means": "Client-identifying content is absent or handled inside policy.",
        "partial_means": "Client identifiers are minimized but need confirmation.",
        "fail_means": "Client-identifying material may be exposed outside policy.",
    },
    {
        "id": "filing_readiness",
        "label": "Filing/client-facing readiness",
        "pass_means": "Output is approved for the intended filing or client-facing use.",
        "partial_means": "Output may be internally useful but needs more review before use.",
        "fail_means": "Output is not ready for filing or client-facing use.",
    },
)
LAW_PROBE_BATTERY = (
    {"category": "citation_support", "signal": "checked"},
    {"category": "citation_support", "signal": "partial"},
    {"category": "citation_support", "signal": "failed"},
    {"category": "privilege_boundary", "signal": "checked"},
    {"category": "privilege_boundary", "signal": "partial"},
    {"category": "privilege_boundary", "signal": "failed"},
    {"category": "supervision_route", "signal": "checked"},
    {"category": "client_confidentiality", "signal": "partial"},
    {"category": "filing_readiness", "signal": "failed"},
    {"category": "filing_readiness", "signal": "not_applicable"},
)
_LAW_CATEGORY_IDS = {category["id"] for category in LAW_MIRROR_CATEGORIES}
_LAW_GRADE_SIGNALS = {
    "checked": "PASS",
    "partial": "PARTIAL",
    "failed": "FAIL",
    "not_applicable": "NA",
}


class PlanisphereError(RuntimeError):
    """Base exception for the example helper."""


class PlanisphereBlocked(PlanisphereError):
    """Raised when Planisphere returns a hard block."""


class PlanisphereNeedsReview(PlanisphereError):
    """Raised when the workflow should pause for routed human review."""

    def __init__(self, decision: JsonObject) -> None:
        super().__init__(decision.get("next_step", "Planisphere review is required."))
        self.decision = decision


@dataclass(frozen=True)
class PlanisphereClient:
    base_url: str
    api_key: str
    timeout: float = 5.0
    transport: Transport | None = None

    def proposed_action(
        self,
        *,
        surface: str,
        source_key: str,
        proposed_action: str,
        law_context: JsonObject,
        redacted_text: str | None = None,
        metadata: JsonObject | None = None,
    ) -> JsonObject:
        payload: JsonObject = {
            "surface": surface,
            "source_key": source_key,
            "proposed_action": proposed_action,
            "law_context": law_context,
        }
        if redacted_text:
            payload["redacted_text"] = redacted_text
        if metadata:
            payload["metadata"] = metadata

        payload["pack"] = "law"
        return self._post_json("/v1/gate", payload)

    def require_allow(
        self,
        *,
        surface: str,
        source_key: str,
        proposed_action: str,
        law_context: JsonObject,
        redacted_text: str | None = None,
        metadata: JsonObject | None = None,
    ) -> JsonObject:
        decision = self.proposed_action(
            surface=surface,
            source_key=source_key,
            proposed_action=proposed_action,
            law_context=law_context,
            redacted_text=redacted_text,
            metadata=metadata,
        )
        if decision["decision"] == "allow":
            return decision
        if decision["decision"] == "needs_review":
            raise PlanisphereNeedsReview(decision)
        raise PlanisphereBlocked(decision.get("next_step", "Planisphere blocked this action."))

    def verify_evidence_seal(self, decision_or_payload: JsonObject) -> JsonObject:
        payload = (
            evidence_seal_verification_payload(decision_or_payload)
            if "evidence_packet" in decision_or_payload
            else dict(decision_or_payload)
        )
        return self._post_json("/evidence-packets/verify-seal", payload)

    def verify_law_mirror_seal(self, review_or_payload: JsonObject) -> JsonObject:
        payload = (
            law_mirror_seal_verification_payload(review_or_payload)
            if "mirror_seal" in review_or_payload
            else dict(review_or_payload)
        )
        return self._post_json("/law/verify-mirror-seal", payload)

    def review_decision(
        self,
        *,
        action_key: str,
        reviewer: str,
        decision: str,
        reason: str | None = None,
        mirror_grades: Mapping[str, str] | None = None,
        mirror_digest: str | None = None,
        mirror_version: str = LAW_MIRROR_VERSION,
    ) -> JsonObject:
        payload: JsonObject = {
            "action_key": action_key,
            "reviewer": reviewer,
            "decision": decision,
        }
        if reason:
            payload["reason"] = reason
        if mirror_grades:
            payload.update(
                law_mirror_review_fields(
                    mirror_grades,
                    mirror_digest=mirror_digest,
                    mirror_version=mirror_version,
                )
            )
        return self._post_json("/law/review-decision", payload)

    def _post_json(self, path: str, payload: JsonObject) -> JsonObject:
        url = f"{self.base_url.rstrip('/')}{path}"
        headers = {
            "Content-Type": "application/json",
            "X-Planisphere-Key": self.api_key,
        }
        if self.transport:
            return self.transport(url, headers, payload, self.timeout)
        return post_json(url, headers, payload, self.timeout)


def post_json(url: str, headers: dict[str, str], payload: JsonObject, timeout: float) -> JsonObject:
    body = json.dumps(payload).encode("utf-8")
    req = request.Request(url, data=body, headers=headers, method="POST")
    try:
        with request.urlopen(req, timeout=timeout) as response:
            return json.loads(response.read().decode("utf-8"))
    except HTTPError as exc:
        detail = exc.read().decode("utf-8")
        raise PlanisphereError(f"Planisphere request failed: {exc.code} {detail}") from exc


def route_for_review(decision: JsonObject) -> JsonObject:
    """Return a compact pause/resume object for agent frameworks."""
    if decision["decision"] != "needs_review":
        raise ValueError("route_for_review expects a needs_review decision")
    return {
        "status": "paused",
        "action_key": decision["action_key"],
        "next_step": decision["next_step"],
        "review_route": decision["review_route"],
        "route_dispatch": decision.get("route_dispatch"),
        "evidence_packet": decision["evidence_packet"],
        "catalog_refs": decision.get("catalog_refs", []),
    }


def evidence_seal_verification_payload(decision: JsonObject) -> JsonObject:
    """Build the raw-safe verifier request from a proposed-action response."""
    packet = decision.get("evidence_packet")
    if not isinstance(packet, dict):
        raise ValueError("decision is missing evidence_packet")
    seal = packet.get("seal")
    if not isinstance(seal, dict):
        raise ValueError("evidence_packet is missing seal")

    payload: JsonObject = {
        "action_key": decision["action_key"],
        "href": packet["href"],
        "status": packet["status"],
        "decision": decision["decision"],
        "payload_hash": decision["payload_hash"],
        "packet_hash": packet["packet_hash"],
        "seal": seal,
    }
    if "review_route" in decision:
        payload["review_route"] = decision.get("review_route")
    if "vertical" in decision:
        payload["vertical"] = decision.get("vertical")
    return payload


def law_mirror_seal_verification_payload(review: JsonObject) -> JsonObject:
    """Build the raw-safe verifier request from a review-decision response."""
    seal = review.get("mirror_seal")
    if not isinstance(seal, dict):
        raise ValueError("review is missing mirror_seal")
    return {"seal": seal}


def _json_safe(value: Any) -> Any:
    try:
        json.dumps(value, sort_keys=True, ensure_ascii=True)
        return value
    except TypeError:
        if isinstance(value, Mapping):
            return {str(key): _json_safe(item) for key, item in value.items()}
        if isinstance(value, (list, tuple)):
            return [_json_safe(item) for item in value]
        return str(value)


def canonical_json_hash(payload: Any) -> str:
    body = json.dumps(
        _json_safe(payload),
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=True,
    ).encode("utf-8")
    return hashlib.sha256(body).hexdigest()


def grade_law_mirror_probe(row: Mapping[str, Any]) -> str:
    category = str(row.get("category") or "")
    signal = str(row.get("signal") or "")
    if category not in _LAW_CATEGORY_IDS:
        return "FAIL"
    return _LAW_GRADE_SIGNALS.get(signal, "FAIL")


def canonical_law_mirror_digest() -> str:
    vector = [grade_law_mirror_probe(row) for row in LAW_PROBE_BATTERY]
    return canonical_json_hash(
        {
            "contract": LAW_MIRROR_CONTRACT,
            "mirror_version": LAW_MIRROR_VERSION,
            "grade_values": list(LAW_GRADE_VALUES),
            "categories": list(LAW_MIRROR_CATEGORIES),
            "probe_vector": vector,
        }
    )


def normalize_law_mirror_grades(grades: Mapping[str, str]) -> dict[str, str]:
    if not grades:
        raise ValueError("mirror grades must be a non-empty mapping")
    normalized: dict[str, str] = {}
    for category, raw_grade in grades.items():
        category_id = str(category)
        if category_id not in _LAW_CATEGORY_IDS:
            raise ValueError(f"unsupported law mirror grade category: {category_id}")
        grade = str(raw_grade).upper()
        if grade not in LAW_GRADE_VALUES:
            raise ValueError(f"law mirror grade for {category_id} must be one of {LAW_GRADE_VALUES}")
        normalized[category_id] = grade
    return dict(sorted(normalized.items()))


def law_mirror_grades_hash(grades: Mapping[str, str]) -> str:
    return canonical_json_hash(
        {
            "contract": LAW_MIRROR_CONTRACT,
            "mirror_version": LAW_MIRROR_VERSION,
            "grades": normalize_law_mirror_grades(grades),
            "raw_content_stored": False,
        }
    )


def law_mirror_review_fields(
    grades: Mapping[str, str],
    *,
    mirror_digest: str | None = None,
    mirror_version: str = LAW_MIRROR_VERSION,
) -> JsonObject:
    return {
        "mirror_version": mirror_version,
        "mirror_digest": mirror_digest or canonical_law_mirror_digest(),
        "mirror_grades": normalize_law_mirror_grades(grades),
    }


def example_law_context() -> JsonObject:
    return {
        "matter_band": "litigation",
        "tool_id": "generic-frontier-model",
        "doctrine_anchor": "fre-707",
        "jurisdiction": "federal",
        "practice_area": "civil-rights-litigation",
    }
