"""Phase 13A structural composition measurement on the frozen generated-product path.

Live Discovery observations stay in the 13A-01 receipt. This module does not call
providers, mutate projects, repair plans, or count 12H materialization as a
production Nuxt/React build (those remain 13C/13E).
"""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

from src.domain.app_blocks.generated_product_conformance_12h import (
    FORBIDDEN_MARKERS,
    STARTERS,
    _product_source_blob,
    compile_cert_plan,
    inspect_must_mount,
    materialize_generated_product,
    planned_surfaces,
    scan_forbidden,
)
from src.domain.app_blocks.schemas import AppBlockPlan
from src.domain.projects.discovery_schemas import DiscoverySpec, ProductDefinition

STARTER = "html"
RUN_01 = Path(__file__).resolve().parents[3] / "docs" / "audits" / "fixtures" / "13a_run_01"


def rate(numerator: int, denominator: int, reason: str | None = None) -> dict:
    return {
        "numerator": numerator,
        "denominator": denominator,
        "rate": (numerator / denominator) if denominator else None,
        "unmeasured_reason": reason if not denominator else None,
    }


def spec_from_prompt(prompt: str) -> DiscoverySpec:
    """Raw-text control: exact customer language, no invented matcher phrases."""
    return DiscoverySpec(product=ProductDefinition(purpose=prompt))


def spec_from_trial(trial: dict) -> DiscoverySpec | None:
    discovery = trial.get("discovery") or {}
    if discovery.get("status") != "success":
        return None
    payload = (discovery.get("result") or {}).get("spec")
    if not payload:
        return None
    return DiscoverySpec.model_validate(payload)


def host_map_gaps(plan: AppBlockPlan) -> dict[str, list[str]]:
    gaps: dict[str, list[str]] = {}
    for starter in STARTERS:
        surfaces = planned_surfaces(plan, starter)
        missing = [
            f"{item.block}#{item.slot}"
            for item in surfaces
            if not item.implementation
        ]
        gaps[starter] = missing
    return gaps


def evaluate_structural(spec: DiscoverySpec, dest: Path) -> dict:
    plan = compile_cert_plan(spec)
    satisfiable = not plan.unsatisfiable
    if satisfiable:
        gaps = host_map_gaps(plan)
    else:
        gaps = {starter: ["unsatisfiable"] for starter in STARTERS}
    host_ready = satisfiable and all(not gaps[starter] for starter in STARTERS)
    surfaces = planned_surfaces(plan, STARTER) if satisfiable else []
    mount = {
        "planned_must_surfaces": len(surfaces),
        "generated_must_surfaces": 0,
        "mounted_must_surfaces": 0,
        "reachable_must_surfaces": 0,
        "missing_must_surfaces": [] if host_ready else list(gaps[STARTER]),
    }
    forbidden_hits: list[str] = []
    extra_surfaces: list[str] = []
    materialized = False
    codegen_error: str | None = None
    if host_ready:
        try:
            dest.mkdir(parents=True, exist_ok=True)
            materialize_generated_product(dest, plan=plan, starter=STARTER)
            materialized = True
            mount = inspect_must_mount(dest, surfaces, STARTER)
            _forbidden_count, extra_surfaces = scan_forbidden(dest, plan)
            lower = _product_source_blob(dest).lower()
            forbidden_hits = [
                marker for marker in FORBIDDEN_MARKERS if marker.lower() in lower
            ]
        except Exception as exc:  # noqa: BLE001 — baseline records the failure class only
            codegen_error = type(exc).__name__
            mount["missing_must_surfaces"] = [codegen_error]
    must_ok = materialized and not mount["missing_must_surfaces"]
    unexpected = bool(forbidden_hits or extra_surfaces)
    structural_ok = must_ok and not unexpected
    repair_required = (not host_ready) or (not must_ok) or unexpected or bool(codegen_error)
    return {
        "plan_satisfiable": satisfiable,
        "adoption": plan.adoption,
        "planned_blocks": [block.id for block in plan.blocks],
        "host_map_gaps": gaps,
        "host_map_ready": host_ready,
        "materialized": materialized,
        "codegen_error": codegen_error,
        "must_mount": mount,
        "forbidden_markers": forbidden_hits,
        "unexpected_semantic_surfaces": extra_surfaces,
        "codegen_success": host_ready and codegen_error is None,
        "must_mount_conformance": must_ok,
        "unexpected_semantic_surface": unexpected,
        "structural_validation": structural_ok,
        "repair_required": repair_required,
        "repair_success": False,
        "terminal_generation_failure": repair_required,
    }


def score_structural(rows: list[dict]) -> dict:
    n = len(rows)
    if not n:
        reason = "No structural composition trials."
        return {
            "codegen_success_rate": rate(0, 0, reason),
            "must_mount_conformance_rate": rate(0, 0, reason),
            "unexpected_semantic_surface_rate": rate(0, 0, reason),
            "structural_validation_rate": rate(0, 0, reason),
            "repair_required_rate": rate(0, 0, reason),
            "repair_success_rate": rate(0, 0, "No repairs attempted in 13A."),
            "terminal_generation_failure_rate": rate(0, 0, reason),
        }
    return {
        "codegen_success_rate": rate(sum(r["codegen_success"] for r in rows), n),
        "must_mount_conformance_rate": rate(sum(r["must_mount_conformance"] for r in rows), n),
        "unexpected_semantic_surface_rate": rate(
            sum(r["unexpected_semantic_surface"] for r in rows), n
        ),
        "structural_validation_rate": rate(sum(r["structural_validation"] for r in rows), n),
        "repair_required_rate": rate(sum(r["repair_required"] for r in rows), n),
        "repair_success_rate": rate(
            0,
            sum(r["repair_required"] for r in rows),
            "No repairs attempted in 13A.",
        ),
        "terminal_generation_failure_rate": rate(
            sum(r["terminal_generation_failure"] for r in rows), n
        ),
    }


PRODUCT_BLOCKS = {
    "crm.customer",
    "crm.lead",
    "crm.pipeline",
    "forms.contact",
    "forms.quote_request",
    "case.management",
    "workflow.assignment",
    "task.management",
    "activity.timeline",
    "notifications.in_app",
    "files.attachments",
    "documents",
    "booking.appointment",
    "catalog.product",
    "orders.order",
    "billing.invoice",
    "inventory.stock",
    "membership.member",
}


def product_blocks_of(planned: list[str]) -> set[str]:
    return {item for item in planned if item in PRODUCT_BLOCKS}


def gold_join(structural: dict, trial: dict) -> dict:
    expected = set(trial.get("expected_product_blocks") or [])
    predicted = product_blocks_of(structural.get("planned_blocks") or [])
    return {
        "oracle_kind": trial.get("oracle_kind"),
        "expected_product_blocks": sorted(expected),
        "predicted_product_blocks": sorted(predicted),
        "exact_product_match": predicted == expected,
        "empty_plan": not structural.get("planned_blocks"),
        "intended_product_structurally_valid": (
            predicted == expected and bool(structural.get("structural_validation"))
        ),
    }


def score_intent_joined(rows: list[dict]) -> dict:
    admitted = [row for row in rows if row.get("oracle_kind") == "admitted"]
    nonempty = [row for row in rows if not row.get("empty_plan")]
    return {
        "empty_plan_rate": rate(sum(bool(r.get("empty_plan")) for r in rows), len(rows)),
        "admitted_exact_product_and_structural_rate": rate(
            sum(bool(r.get("intended_product_structurally_valid")) for r in admitted),
            len(admitted),
        ),
        "nonempty_plan_structural_rate": rate(
            sum(r["structural_validation"] for r in nonempty),
            len(nonempty),
        ),
    }


def load_run_01_trials(root: Path | None = None) -> list[dict]:
    base = root or RUN_01
    trials = []
    for path in sorted((base / "trials").glob("13A-*.json")):
        trials.append(json.loads(path.read_text(encoding="utf-8")))
    return trials


def measure_run_01(out_dir: Path, *, trials: list[dict] | None = None) -> dict:
    """HTML structural composition for raw-text controls and live ready specs."""
    rows = trials if trials is not None else load_run_01_trials()
    out_dir.mkdir(parents=True, exist_ok=True)
    raw_results: list[dict] = []
    ready_results: list[dict] = []
    with tempfile.TemporaryDirectory(prefix="p2hs-13a-struct-") as tmp:
        tmp_path = Path(tmp)
        for row in rows:
            raw_spec = spec_from_prompt(row["prompt"])
            raw = evaluate_structural(raw_spec, tmp_path / "raw" / row["id"])
            raw_results.append(
                {
                    "id": row["id"],
                    "stratum": row["stratum"],
                    "cohort": "raw_text",
                    **raw,
                    **gold_join(raw, row),
                }
            )
            discovery = row.get("discovery") or {}
            ready = (
                discovery.get("status") == "success"
                and (discovery.get("result") or {}).get("decision") == "ready"
            )
            if ready:
                live_spec = spec_from_trial(row)
                if live_spec is not None:
                    live = evaluate_structural(live_spec, tmp_path / "ready" / row["id"])
                    ready_results.append(
                        {
                            "id": row["id"],
                            "stratum": row["stratum"],
                            "cohort": "live_ready",
                            **live,
                            **gold_join(live, row),
                        }
                    )
    payload = {
        "status": "structural_composition_measured",
        "method": (
            "Frozen 12H plan-derived HTML assembler + host-map inspection. "
            "Not live LLM Codegen, not Nuxt/React production builds, not hosted browser."
        ),
        "phase_13b_authorized": False,
        "corpus_size": len(rows),
        "raw_text_structural": score_structural(raw_results),
        "live_ready_structural": score_structural(ready_results),
        "raw_text_intent_joined": score_intent_joined(raw_results),
        "live_ready_intent_joined": score_intent_joined(ready_results),
        "empty_plan_ids": [item["id"] for item in raw_results if item.get("empty_plan")],
        "admitted_mismatch_ids": [
            item["id"]
            for item in raw_results
            if item.get("oracle_kind") == "admitted" and not item.get("exact_product_match")
        ],
        "actual_build_rate": rate(
            0,
            0,
            "Deferred to 13E: no corpus Nuxt/React/HTML production builds.",
        ),
        "runtime_smoke_rate": rate(
            0,
            0,
            "Deferred to 13E: no hosted BFF/Cloud browser journeys.",
        ),
        "raw_text_failures": [
            {
                "id": item["id"],
                "stratum": item["stratum"],
                "missing_must_surfaces": item["must_mount"]["missing_must_surfaces"],
                "host_map_gaps": item["host_map_gaps"],
                "unexpected_semantic_surfaces": item["unexpected_semantic_surfaces"],
                "codegen_error": item["codegen_error"],
            }
            for item in raw_results
            if item["repair_required"]
        ],
        "live_ready_failures": [
            {
                "id": item["id"],
                "stratum": item["stratum"],
                "missing_must_surfaces": item["must_mount"]["missing_must_surfaces"],
                "host_map_gaps": item["host_map_gaps"],
                "unexpected_semantic_surfaces": item["unexpected_semantic_surfaces"],
                "codegen_error": item["codegen_error"],
            }
            for item in ready_results
            if item["repair_required"]
        ],
        "raw_text_by_stratum": {
            group: score_structural([item for item in raw_results if item["stratum"] == group])
            for group in sorted({item["stratum"] for item in raw_results})
        },
    }
    (out_dir / "structural_scorecard.json").write_text(
        json.dumps(payload, indent=2) + "\n", encoding="utf-8"
    )
    (out_dir / "structural_raw_trials.json").write_text(
        json.dumps(raw_results, indent=2) + "\n", encoding="utf-8"
    )
    (out_dir / "structural_ready_trials.json").write_text(
        json.dumps(ready_results, indent=2) + "\n", encoding="utf-8"
    )
    return payload
