"""W21B-wave-4 v8 Phase A: floor_plan_layout_plan — Path-S coordinate scaffold.

Turns FP metadata (numbered_elements inventory) into a *validated coarse
geometry* contract: the LLM (Path-S layout planner) emits an
``element_layout[]`` of axis-aligned rects on a normalized grid; code
validates and (Phase B) renders a deterministic SVG box scaffold. This is
a DATA contract, not a rendered-image contract — FP drawing drift becomes
structurally impossible because code draws from the validated coordinates.

This module is the pure core (schema + validator + synthetic fixture);
it performs NO LLM / image / VLM / DB I/O. The LLM provider (A1) and the
checkpoint step (A3) live in separate modules and inject their output
through ``validate_layout``.

Boundaries (brief §2.1 + scenario-leakage guard):
  - Code checks structure / exact-ID join / geometry only. It never
    lexically inspects labels or prose. The only authority for a marker's
    layer is the dossier ``base_layer_decision`` (the planner must not
    re-classify).
  - Transient / state-overlay markers are NEVER part of the base layout
    scaffold (they belong to the per-bg overlay payload).
"""
from __future__ import annotations

from typing import Any, Dict, List


BASE_STRUCTURAL_LAYERS: frozenset = frozenset({
    "base_structural_unit", "base_opening",
    "base_persistent_fixture", "base_persistent_furniture",
})

DEFAULT_GRID = 100
DEFAULT_RENDER_CAP = 18

# Overlap fraction above which two non-area rendered rects are treated as
# an impossible (near-full) overlap.
_OVERLAP_FAIL_FRACTION = 0.9


_OPENAI_UNSUPPORTED_SCHEMA_KEYS: frozenset = frozenset({
    "anyOf", "oneOf", "allOf", "not", "minItems", "maxItems", "uniqueItems",
    "minLength", "maxLength", "pattern", "format", "minimum", "maximum",
    "exclusiveMinimum", "exclusiveMaximum", "multipleOf", "patternProperties",
    "contains", "minContains", "maxContains",
})


# Strict OpenAI-compatible JSON schema for the layout planner output.
LAYOUT_SCHEMA: Dict[str, Any] = {
    "type": "object", "additionalProperties": False,
    "required": ["fp_id", "place_semantic_tags", "expected_visual_density",
                 "key_fixture_groups", "element_layout",
                 "metadata_only_elements", "validation_notes"],
    "properties": {
        "fp_id": {"type": "string"},
        "place_semantic_tags": {"type": "array", "items": {"type": "string"}},
        "expected_visual_density": {"type": "string",
                                    "enum": ["low", "medium", "high"]},
        "key_fixture_groups": {"type": "array", "items": {
            "type": "object", "additionalProperties": False,
            "required": ["group_id", "label"],
            "properties": {"group_id": {"type": "string"},
                           "label": {"type": "string"}}}},
        "element_layout": {"type": "array", "items": {
            "type": "object", "additionalProperties": False,
            "required": ["number", "label", "base_layer_decision",
                         "importance", "render_on_plan", "shape_kind",
                         "rect", "adjacent_to", "camera_visibility_hint"],
            "properties": {
                "number": {"type": "integer"},
                "label": {"type": "string"},
                "base_layer_decision": {"type": "string"},
                "importance": {"type": "number"},
                "render_on_plan": {"type": "boolean"},
                "shape_kind": {"type": "string", "enum": [
                    "rect", "line", "door_arc", "window_line",
                    "fixture_icon", "area"]},
                "rect": {"type": "object", "additionalProperties": False,
                         "required": ["x", "y", "w", "h"],
                         "properties": {"x": {"type": "integer"},
                                        "y": {"type": "integer"},
                                        "w": {"type": "integer"},
                                        "h": {"type": "integer"}}},
                "adjacent_to": {"type": "array", "items": {"type": "integer"}},
                "camera_visibility_hint": {"type": "string"}}}},
        "metadata_only_elements": {"type": "array", "items": {
            "type": "object", "additionalProperties": False,
            "required": ["number", "label"],
            "properties": {"number": {"type": "integer"},
                           "label": {"type": "string"}}}},
        "validation_notes": {"type": "array", "items": {"type": "string"}},
    },
}


class LayoutPlanError(Exception):
    """Fail-closed signal for the layout-plan core."""


def _is_int(v: Any) -> bool:
    return isinstance(v, int) and not isinstance(v, bool)


def _inventory_map(dossier_inventory: List[Dict[str, Any]]) -> Dict[int, str]:
    """``{number: base_layer_decision}`` for ALL inventory markers (base +
    overlay), so the validator can both exact-ID-join and detect a
    transient marker wrongly placed in the base layout."""
    out: Dict[int, str] = {}
    for e in dossier_inventory or []:
        if not isinstance(e, dict):
            continue
        try:
            n = int(e["number"])
        except (KeyError, TypeError, ValueError):
            continue
        d = e.get("base_layer_decision")
        if isinstance(d, str):
            out[n] = d
    return out


def validate_layout(
    *,
    output: Any,
    dossier_inventory: List[Dict[str, Any]],
    fp_id: str,
    grid: int = DEFAULT_GRID,
    render_cap: int = DEFAULT_RENDER_CAP,
) -> Dict[str, Any]:
    """Pure validator. Returns ``{"ok", "blockers": [str], "layout": dict|None}``.

    Checks: fp_id match; required shape; exact-ID join (every layout/
    metadata number ∈ dossier); no re-classification of base_layer_decision;
    transient (state_overlay_*) markers excluded from the layout; rect
    bounds; duplicate numbers; impossible near-full overlap between two
    non-area rendered rects; render cap (rendered non-required ≤ cap).
    """
    blockers: List[str] = []
    if not isinstance(output, dict):
        return {"ok": False, "blockers": [f"output not a dict ({type(output).__name__})"], "layout": None}
    if output.get("fp_id") != fp_id:
        blockers.append(f"fp_id mismatch: {output.get('fp_id')!r} != {fp_id!r}")

    inv = _inventory_map(dossier_inventory)
    if not inv:
        blockers.append("dossier_inventory empty")
    base_nums = {n for n, d in inv.items() if d in BASE_STRUCTURAL_LAYERS}
    required_struct = {n for n, d in inv.items()
                       if d in ("base_structural_unit", "base_opening")}

    el = output.get("element_layout")
    if not isinstance(el, list):
        blockers.append("element_layout must be a list")
        el = []

    seen: set = set()
    rendered_rects: List[tuple] = []   # (number, rect) non-area rendered
    rendered_count = 0
    for idx, e in enumerate(el):
        pfx = f"element_layout[{idx}]"
        if not isinstance(e, dict):
            blockers.append(f"{pfx} not a dict")
            continue
        n = e.get("number")
        if not _is_int(n):
            blockers.append(f"{pfx}.number must be int")
            continue
        if n in seen:
            blockers.append(f"{pfx}.number={n} duplicated")
        seen.add(n)
        layer = e.get("base_layer_decision")
        if n not in inv:
            blockers.append(f"{pfx}.number={n} not in dossier inventory")
        else:
            if layer != inv[n]:
                blockers.append(
                    f"{pfx}.base_layer_decision={layer!r} disagrees with "
                    f"dossier {inv[n]!r} for marker #{n}")
            if inv[n] not in BASE_STRUCTURAL_LAYERS:
                blockers.append(
                    f"{pfx}.number={n} is a transient/state_overlay marker "
                    f"({inv[n]!r}); it must NOT appear in the base layout")
        r = e.get("rect") or {}
        try:
            x, y, w, h = int(r["x"]), int(r["y"]), int(r["w"]), int(r["h"])
        except (KeyError, TypeError, ValueError):
            blockers.append(f"{pfx}.rect malformed")
            continue
        if not (0 <= x <= grid and 0 <= y <= grid and 0 < w <= grid
                and 0 < h <= grid and x + w <= grid and y + h <= grid):
            blockers.append(f"{pfx} #{n} rect out-of-bound {r}")
        if e.get("render_on_plan"):
            rendered_count += 1
            if e.get("shape_kind") != "area":
                rendered_rects.append((n, (x, y, w, h)))

    # impossible near-full overlap between two non-area rendered rects
    for i in range(len(rendered_rects)):
        for j in range(i + 1, len(rendered_rects)):
            (n1, a), (n2, b) = rendered_rects[i], rendered_rects[j]
            ox = max(0, min(a[0]+a[2], b[0]+b[2]) - max(a[0], b[0]))
            oy = max(0, min(a[1]+a[3], b[1]+b[3]) - max(a[1], b[1]))
            inter = ox * oy
            amin = min(a[2]*a[3], b[2]*b[3])
            if amin > 0 and inter >= _OVERLAP_FAIL_FRACTION * amin:
                blockers.append(f"#{n1} and #{n2} near-full overlap")

    # render cap: rendered (excluding required structural/openings) ≤ cap
    if rendered_count > render_cap + len(required_struct):
        blockers.append(
            f"rendered {rendered_count} exceeds cap {render_cap} "
            f"(+{len(required_struct)} required structural exempt)")

    # metadata_only numbers must also be known base markers
    for idx, m in enumerate(output.get("metadata_only_elements") or []):
        n = m.get("number") if isinstance(m, dict) else None
        if not _is_int(n):
            blockers.append(f"metadata_only_elements[{idx}].number must be int")
        elif n not in base_nums:
            blockers.append(
                f"metadata_only_elements[{idx}].number={n} not a base marker")

    for f in ("place_semantic_tags", "key_fixture_groups",
              "metadata_only_elements", "validation_notes"):
        if not isinstance(output.get(f), list):
            blockers.append(f"{f} must be a list")
    if output.get("expected_visual_density") not in ("low", "medium", "high"):
        blockers.append("expected_visual_density invalid")

    if blockers:
        return {"ok": False, "blockers": blockers, "layout": None}
    return {"ok": True, "blockers": [], "layout": output}


def compute_synthetic_layout_fixture(
    *,
    dossier_inventory: List[Dict[str, Any]],
    fp_id: str,
    grid: int = DEFAULT_GRID,
) -> Dict[str, Any]:
    """Deterministic placeholder layout (base markers spread on the grid).

    Mirrors the W20A2 synthetic-fixture pattern: lets the step / renderer /
    tests run before a real LLM provider is wired. Cap-respecting and
    self-validating. Transient markers are excluded.
    """
    base = [e for e in (dossier_inventory or [])
            if isinstance(e, dict)
            and e.get("base_layer_decision") in BASE_STRUCTURAL_LAYERS]
    if not base:
        raise LayoutPlanError(
            f"fp_id={fp_id!r}: dossier_inventory has zero base_* markers")
    # cap the rendered set deterministically (structural units first)
    order = sorted(base, key=lambda e: (
        0 if e["base_layer_decision"] in ("base_structural_unit", "base_opening") else 1,
        int(e["number"])))
    cap = DEFAULT_RENDER_CAP + sum(
        1 for e in base if e["base_layer_decision"] in
        ("base_structural_unit", "base_opening"))
    rendered = order[:cap]
    deferred = order[cap:]
    cell = max(8, grid // 11)
    els: List[Dict[str, Any]] = []
    for i, e in enumerate(rendered):
        col, row = i % 9, i // 9
        x, y = col * (cell + 1), row * (cell + 1)
        is_area = e["base_layer_decision"] == "base_structural_unit"
        els.append({
            "number": int(e["number"]),
            "label": str(e.get("label", "")),
            "base_layer_decision": e["base_layer_decision"],
            "importance": 0.5, "render_on_plan": True,
            "shape_kind": "area" if is_area else "rect",
            "rect": {"x": min(x, grid - cell), "y": min(y, grid - cell),
                     "w": cell, "h": cell},
            "adjacent_to": [], "camera_visibility_hint": "midground",
        })
    return {
        "fp_id": fp_id,
        "place_semantic_tags": [],
        "expected_visual_density": "low",
        "key_fixture_groups": [],
        "element_layout": els,
        "metadata_only_elements": [
            {"number": int(e["number"]), "label": str(e.get("label", ""))}
            for e in deferred],
        "validation_notes": [
            "synthetic_fixture: deterministic placeholder layout, not a real "
            "LLM plan; coordinates are grid-spread, not a real dwelling."],
    }
