"""W20A2: review HTML overlay emitter.

Pure rendering function. Takes a base_location_dossier (W20A) + a
geometry-candidates dict (W20A2 ``floor_plan_geometry_readback``) and
returns a self-contained HTML string.

The HTML is a **review / result-visibility surface**, not a production
UI. It is served via the user's mandated external HTTP server pattern
(``python -m http.server <port> --bind 0.0.0.0``); never opened via
``open <path>``.

Contract:
  - static template / code carries no scenario-specific literal tokens.
  - runtime payload may display source-provided labels / shot text
    verbatim because that content is data, not contract.
  - read-only: no fetch / POST / mutation against any pipeline state.
  - deterministic: identical input produces byte-identical output.
"""
from __future__ import annotations

import html as _html
from typing import Any, Dict, List, Optional, Tuple


# Grid pixel size: 10×10 cells of 60 px = 600 px square area. Markers
# rendered as 14 px dots; arrows / cones drawn over the grid.
DEFAULT_CELL_PX: int = 60
DEFAULT_GRID_PADDING_PX: int = 20


def _esc(value: Any) -> str:
    return _html.escape(str(value), quote=True)


def _cell_center(
    *, row: int, col: int, cell_px: int, pad: int
) -> Tuple[int, int]:
    """Convert (row, col) → SVG (x, y) center pixel coordinates."""
    x = pad + col * cell_px + cell_px // 2
    y = pad + row * cell_px + cell_px // 2
    return x, y


def _svg_open(
    *, grid_rows: int, grid_cols: int, cell_px: int, pad: int
) -> str:
    width = pad * 2 + grid_cols * cell_px
    height = pad * 2 + grid_rows * cell_px
    return (
        f'<svg viewBox="0 0 {width} {height}" '
        f'xmlns="http://www.w3.org/2000/svg" '
        f'class="fp-review-svg" '
        f'role="img" aria-label="floor plan review overlay">'
    )


def _svg_defs() -> str:
    """Arrow-head marker definition. Must sit INSIDE the <svg> root so
    SVG references like ``marker-end="url(#fp-arrow-head)"`` resolve.
    """
    return (
        '<defs><marker id="fp-arrow-head" viewBox="0 0 10 10" '
        'refX="9" refY="5" markerWidth="6" markerHeight="6" '
        'orient="auto"><path d="M0,0 L0,10 L10,5 z" fill="#b91c1c"/>'
        '</marker></defs>'
    )


def _svg_grid_body(
    *,
    grid_rows: int,
    grid_cols: int,
    cell_px: int,
    pad: int,
) -> str:
    """Grid lines + row/col labels (rendered INSIDE the open <svg>)."""
    lines: List[str] = []
    # Background rect (grid area).
    lines.append(
        f'<rect x="{pad}" y="{pad}" width="{grid_cols * cell_px}" '
        f'height="{grid_rows * cell_px}" '
        f'fill="#fafafa" stroke="#999" stroke-width="1.5"/>'
    )
    # Grid lines.
    for r in range(grid_rows + 1):
        y = pad + r * cell_px
        lines.append(
            f'<line x1="{pad}" y1="{y}" '
            f'x2="{pad + grid_cols * cell_px}" y2="{y}" '
            f'stroke="#d0d0d0" stroke-width="0.75"/>'
        )
    for c in range(grid_cols + 1):
        x = pad + c * cell_px
        lines.append(
            f'<line x1="{x}" y1="{pad}" '
            f'x2="{x}" y2="{pad + grid_rows * cell_px}" '
            f'stroke="#d0d0d0" stroke-width="0.75"/>'
        )
    # Row labels (left margin).
    for r in range(grid_rows):
        y = pad + r * cell_px + cell_px // 2 + 4
        lines.append(
            f'<text x="{pad - 4}" y="{y}" '
            f'text-anchor="end" font-size="10" fill="#666">{r}</text>'
        )
    # Col labels (top margin).
    for c in range(grid_cols):
        x = pad + c * cell_px + cell_px // 2
        lines.append(
            f'<text x="{x}" y="{pad - 6}" '
            f'text-anchor="middle" font-size="10" fill="#666">{c}</text>'
        )
    return "\n".join(lines)


def _svg_marker_dot(
    *,
    cell: List[int],
    number: int,
    kind: str,
    cell_px: int,
    pad: int,
) -> str:
    """Render a base marker dot keyed on its kind."""
    row, col = cell[0], cell[1]
    x, y = _cell_center(row=row, col=col, cell_px=cell_px, pad=pad)
    fill = {
        "base_structural_unit": "#1e6aa1",
        "base_opening": "#d97706",
        "base_persistent_fixture": "#0b8060",
        "base_persistent_furniture": "#7c3aed",
    }.get(kind, "#444")
    return (
        f'<g class="fp-marker fp-marker-{_esc(kind)}">'
        f'<circle cx="{x}" cy="{y}" r="11" fill="{fill}" '
        f'stroke="#fff" stroke-width="1.5" opacity="0.92"/>'
        f'<text x="{x}" y="{y + 4}" text-anchor="middle" '
        f'font-size="11" fill="#fff" font-weight="600">'
        f'{_esc(number)}</text>'
        f'</g>'
    )


def _svg_camera_point(
    *,
    cell: List[int],
    unit_marker: int,
    cell_px: int,
    pad: int,
) -> str:
    row, col = cell[0], cell[1]
    x, y = _cell_center(row=row, col=col, cell_px=cell_px, pad=pad)
    # Larger ring around the unit anchor to mark candidate camera point.
    return (
        f'<g class="fp-camera-point">'
        f'<circle cx="{x}" cy="{y}" r="18" fill="none" '
        f'stroke="#1e6aa1" stroke-width="2" stroke-dasharray="3 2"/>'
        f'<text x="{x}" y="{y - 22}" text-anchor="middle" '
        f'font-size="9" fill="#1e6aa1">cam · u{_esc(unit_marker)}</text>'
        f'</g>'
    )


def _svg_direction_arrow(
    *,
    record: Dict[str, Any],
    cell_px: int,
    pad: int,
) -> str:
    cam = record["camera_cell"]
    tgt = record["look_at_cell"]
    x1, y1 = _cell_center(row=cam[0], col=cam[1], cell_px=cell_px, pad=pad)
    x2, y2 = _cell_center(row=tgt[0], col=tgt[1], cell_px=cell_px, pad=pad)
    return (
        f'<g class="fp-direction-arrow">'
        f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
        f'stroke="#b91c1c" stroke-width="1.4" opacity="0.55" '
        f'marker-end="url(#fp-arrow-head)"/>'
        f'</g>'
    )


def _svg_view_cone_marker(
    *,
    record: Dict[str, Any],
    cell_px: int,
    pad: int,
) -> str:
    """Render a view-cone footprint at the camera cell, **rotated to
    the candidate direction** (``dx``, ``dy``) recorded on the cone.

    The cone is a symmetric isoceles polygon with apex at the camera
    cell and axis pointing toward the look-at cell. Lens enum drives
    the fov°. Code never picks a final camera or look-at — every
    cone is a candidate, one per (direction × lens). The HTML layer
    only visualizes the candidate; LLM-owned decisions remain
    downstream.
    """
    import math

    cam = record["camera_cell"]
    x, y = _cell_center(row=cam[0], col=cam[1], cell_px=cell_px, pad=pad)
    fov = int(record["fov_deg"])
    # ``dx`` = col diff (SVG x axis), ``dy`` = row diff (SVG y axis,
    # positive points DOWN). atan2(dy, dx) gives the cone's heading in
    # SVG coordinate space.
    dx_grid = float(record.get("dx", 0))
    dy_grid = float(record.get("dy", 0))
    if dx_grid == 0 and dy_grid == 0:
        # Degenerate (same cell). Should not happen because direction
        # records skip self-pairs, but be defensive.
        return ""
    theta = math.atan2(dy_grid, dx_grid)
    half = math.radians(fov / 2.0)
    length = cell_px * 2
    e1x = x + length * math.cos(theta - half)
    e1y = y + length * math.sin(theta - half)
    e2x = x + length * math.cos(theta + half)
    e2y = y + length * math.sin(theta + half)
    color = {
        "wide": "#10b981",
        "normal": "#3b82f6",
        "telephoto": "#a855f7",
    }.get(record["lens_enum"], "#888")
    look_at_label = ""
    if "look_at_unit" in record:
        look_at_label = (
            f' data-look-at-unit="{_esc(record["look_at_unit"])}"'
        )
    return (
        f'<g class="fp-view-cone fp-view-cone-{_esc(record["lens_enum"])}"'
        f'{look_at_label}>'
        f'<polygon points="{x},{y} {e1x:.1f},{e1y:.1f} '
        f'{e2x:.1f},{e2y:.1f}" fill="{color}" opacity="0.08" '
        f'stroke="{color}" stroke-width="0.8" stroke-opacity="0.6"/>'
        f'</g>'
    )


def _diagnostics_block(
    *, title: str, diagnostics: List[str]
) -> str:
    if not diagnostics:
        return ""
    items = "".join(f"<li>{_esc(d)}</li>" for d in diagnostics)
    return (
        f'<details class="fp-diag" open><summary>{_esc(title)}'
        f' ({len(diagnostics)})</summary><ul>{items}</ul></details>'
    )


def _per_bg_facts_table(
    *, per_bg: Dict[str, Dict[str, Any]]
) -> str:
    if not per_bg:
        return '<p class="fp-empty">no per-bg render facts.</p>'
    headers = [
        "bg_id",
        "target_units",
        "dominant_unit",
        "use",
        "base_refs",
        "transient",
        "clean",
        "applies_to_shots",
        "depends_on_bg",
    ]
    rows = []
    for bg_id in sorted(per_bg):
        facts = per_bg[bg_id]
        rows.append(
            "<tr>"
            f"<td>{_esc(bg_id)}</td>"
            f"<td>{_esc(facts.get('target_unit_marker_numbers'))}</td>"
            f"<td>{_esc(facts.get('dominant_target_unit_marker_number'))}</td>"
            f"<td>{_esc(facts.get('use_numbered_elements'))}</td>"
            f"<td>{_esc(facts.get('base_marker_numbers_to_reference'))}</td>"
            f"<td>{_esc(facts.get('transient_marker_numbers_to_describe'))}</td>"
            f"<td>{_esc(facts.get('clean_background_expected'))}</td>"
            f"<td>{_esc(facts.get('applies_to_shots'))}</td>"
            f"<td>{_esc(facts.get('depends_on_bg'))}</td>"
            "</tr>"
        )
    head_html = "".join(f"<th>{_esc(h)}</th>" for h in headers)
    return (
        '<table class="fp-table">'
        f"<thead><tr>{head_html}</tr></thead>"
        f"<tbody>{''.join(rows)}</tbody>"
        "</table>"
    )


def render_review_html(
    *,
    dossier: Dict[str, Any],
    geometry: Dict[str, Any],
    cell_px: int = DEFAULT_CELL_PX,
    pad: int = DEFAULT_GRID_PADDING_PX,
    page_title_prefix: str = "W20A2 review",
) -> str:
    """Return a self-contained HTML page string."""
    fp_id = dossier.get("fp_id", "?")
    grid = geometry.get("grid_size") or [10, 10]
    rows, cols = int(grid[0]), int(grid[1])
    fp_image_path = dossier.get("fp_image_path")
    standard_of_living = (
        dossier.get("dwelling_identity", {}).get("standard_of_living_band")
    )

    # Build SVG. <defs> sits INSIDE <svg> so marker-end="url(#…)"
    # references resolve in the same scope.
    parts: List[str] = []
    parts.append(_svg_open(
        grid_rows=rows, grid_cols=cols, cell_px=cell_px, pad=pad
    ))
    parts.append(_svg_defs())
    parts.append(_svg_grid_body(
        grid_rows=rows, grid_cols=cols, cell_px=cell_px, pad=pad
    ))

    # View cones first (under markers).
    for record in geometry.get("view_cone_records", []):
        parts.append(_svg_view_cone_marker(
            record=record, cell_px=cell_px, pad=pad
        ))

    # Direction arrows.
    for record in geometry.get("direction_vector_records", []):
        parts.append(_svg_direction_arrow(
            record=record, cell_px=cell_px, pad=pad
        ))

    # Camera points (rings) around unit anchors.
    for unit_str, cell_list in geometry.get(
        "camera_cell_candidates_per_unit", {}
    ).items():
        try:
            unit_marker = int(unit_str)
        except (TypeError, ValueError):
            continue
        for cell in cell_list:
            parts.append(_svg_camera_point(
                cell=cell,
                unit_marker=unit_marker,
                cell_px=cell_px,
                pad=pad,
            ))

    # Marker dots on top.
    marker_cells = geometry.get("marker_cells", {})
    # Reconstruct kind via base_marker_inventory.
    kind_by_number: Dict[int, str] = {}
    for entry in dossier.get("base_marker_inventory") or []:
        try:
            kind_by_number[int(entry["number"])] = entry.get(
                "base_layer_decision", ""
            )
        except (TypeError, ValueError):
            continue
    for num_str, cell in marker_cells.items():
        try:
            number = int(num_str)
        except (TypeError, ValueError):
            continue
        kind = kind_by_number.get(number, "")
        parts.append(_svg_marker_dot(
            cell=cell,
            number=number,
            kind=kind,
            cell_px=cell_px,
            pad=pad,
        ))
    parts.append("</svg>")

    svg_str = "\n".join(parts)

    # Diagnostics blocks.
    diag_geom = _diagnostics_block(
        title="geometry diagnostics",
        diagnostics=geometry.get("diagnostics") or [],
    )
    diag_wall = _diagnostics_block(
        title="wall/door invalidation diagnostics",
        diagnostics=geometry.get("wall_door_invalidation_diagnostics") or [],
    )
    diag_dossier = _diagnostics_block(
        title="dossier diagnostics",
        diagnostics=dossier.get("diagnostics") or [],
    )

    # Per-bg facts table.
    per_bg_table = _per_bg_facts_table(
        per_bg=dossier.get("per_bg_render_facts_by_bg_id") or {}
    )

    # Fp image preview block (read-only, displays runtime path verbatim).
    fp_image_html = ""
    if fp_image_path:
        fp_image_html = (
            f'<p class="fp-image-path">fp_image_path: '
            f'<code>{_esc(fp_image_path)}</code></p>'
        )

    title = f"{page_title_prefix} · fp_id={fp_id}"

    css = """
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
       Roboto, Helvetica, Arial, sans-serif; margin: 24px;
       color: #222; background: #f7f7f9; }
h1 { font-size: 18px; margin: 0 0 6px; }
h2 { font-size: 13px; margin: 16px 0 6px; color: #555; }
.fp-meta { font-size: 12px; color: #666; margin-bottom: 10px; }
.fp-meta code { background: #ececef; padding: 1px 4px; border-radius: 3px; }
.fp-pills { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 12px; }
.fp-pill { background: #1e6aa1; color: #fff; font-size: 11px;
           padding: 2px 8px; border-radius: 10px; }
.fp-pill.note { background: #d97706; }
.fp-pill.warn { background: #b91c1c; }
.fp-pill.muted { background: #6b7280; }
.fp-review-svg { background: #fff; border: 1px solid #ddd;
                 box-shadow: 0 1px 3px rgba(0,0,0,0.06);
                 max-width: 100%; height: auto; }
.fp-diag { margin: 10px 0; background: #fff;
           padding: 8px 12px; border: 1px solid #e5e5e5;
           border-radius: 4px; font-size: 12px; }
.fp-diag summary { cursor: pointer; font-weight: 600; }
.fp-diag ul { margin: 6px 0 4px 18px; padding: 0; }
.fp-table { border-collapse: collapse; font-size: 11px;
            background: #fff; margin-top: 6px; }
.fp-table th, .fp-table td { border: 1px solid #ddd;
                             padding: 3px 6px; text-align: left;
                             vertical-align: top; }
.fp-table th { background: #ececef; }
.fp-image-path { font-size: 12px; color: #555; }
.fp-empty { font-size: 12px; color: #777; font-style: italic; }
.legend { font-size: 11px; color: #555; margin-top: 4px; }
.legend span { display: inline-block; width: 10px; height: 10px;
               border-radius: 50%; vertical-align: middle;
               margin-right: 4px; }
"""

    body_parts: List[str] = []
    body_parts.append(f'<h1>{_esc(title)}</h1>')
    body_parts.append('<div class="fp-pills">')
    body_parts.append(
        f'<span class="fp-pill">readback_status: '
        f'{_esc(geometry.get("readback_status"))}</span>'
    )
    body_parts.append(
        f'<span class="fp-pill note">grid: {rows}×{cols}</span>'
    )
    if standard_of_living:
        body_parts.append(
            f'<span class="fp-pill muted">band: '
            f'{_esc(standard_of_living)}</span>'
        )
    body_parts.append(
        f'<span class="fp-pill warn">NOT a final BG — review surface only'
        f'</span>'
    )
    body_parts.append('</div>')
    body_parts.append(fp_image_html)

    body_parts.append('<h2>geometry overlay (10×10 grid)</h2>')
    body_parts.append(svg_str)
    body_parts.append(
        '<p class="legend">'
        '<span style="background:#1e6aa1"></span> unit · '
        '<span style="background:#d97706"></span> opening · '
        '<span style="background:#0b8060"></span> fixture · '
        '<span style="background:#7c3aed"></span> furniture · '
        'dashed ring = candidate camera cell · '
        'red arrow = candidate direction vector · '
        'green/blue/purple cone = wide/normal/telephoto view cone.'
        '</p>'
    )

    body_parts.append('<h2>per-bg render facts</h2>')
    body_parts.append(per_bg_table)

    body_parts.append('<h2>diagnostics</h2>')
    body_parts.append(diag_geom)
    body_parts.append(diag_wall)
    body_parts.append(diag_dossier)

    body_html = "\n".join(body_parts)

    return (
        "<!doctype html>\n"
        '<html lang="en"><head>'
        f'<meta charset="utf-8"><title>{_esc(title)}</title>'
        f"<style>{css}</style>"
        "</head><body>"
        f"{body_html}"
        "</body></html>\n"
    )
