#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import requests
from PIL import Image, ImageChops, ImageStat


DEFAULT_COMPARE_URL = "http://192.168.200.184/newpec/navsea-compare-karatsu-20nm.html"
DEFAULT_BACKEND_AUDIT_JSON = Path(
    "/root/sourceserver/pbf/NavSea_Original_vs_Delivery_Render_Audit_Karatsu_20nm_2026-03-31.r7.json"
)
DEFAULT_OUTPUT_DIR = Path("/root/sourceserver/pbf/report/strict_audit")
DEFAULT_HOTSPOTS_PATH = Path("/root/sourceserver/pbf/strict_audit_hotspots_20nm.json")


@dataclass
class CropBox:
    left: int
    top: int
    right: int
    bottom: int

    def as_tuple(self) -> tuple[int, int, int, int]:
        return (self.left, self.top, self.right, self.bottom)


@dataclass(frozen=True)
class HotspotSpec:
    id: str
    label: str
    notes: str
    pane_box_norm: tuple[float, float, float, float]
    backend_focus: tuple[str, ...]


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Run strict audit by combining browser-rendered visual diff with backend render audit summary."
    )
    parser.add_argument("--compare-url", default=DEFAULT_COMPARE_URL)
    parser.add_argument("--backend-audit-json", type=Path, default=DEFAULT_BACKEND_AUDIT_JSON)
    parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
    parser.add_argument("--width", type=int, default=1280)
    parser.add_argument("--height", type=int, default=720)
    parser.add_argument("--toolbar-height", type=int, default=150)
    parser.add_argument("--bottom-trim", type=int, default=170)
    parser.add_argument("--timeout-sec", type=int, default=120)
    parser.add_argument("--hotspots-json", type=Path, default=DEFAULT_HOTSPOTS_PATH)
    return parser.parse_args()


def fetch_compare_version(compare_url: str) -> str | None:
    try:
        text = requests.get(compare_url, timeout=20).text
    except Exception:
        return None
    match = re.search(r'const\s+COMPARE_VERSION\s*=\s*"([^"]+)"', text)
    return match.group(1) if match else None


def run_browser_screenshot(compare_url: str, output_png: Path, width: int, height: int, timeout_sec: int) -> None:
    cmd = [
        "timeout",
        f"{timeout_sec}s",
        "google-chrome",
        "--headless=new",
        "--disable-gpu",
        "--enable-unsafe-swiftshader",
        "--no-sandbox",
        "--virtual-time-budget=15000",
        f"--window-size={width},{height}",
        f"--screenshot={output_png}",
        compare_url,
    ]
    subprocess.run(cmd, check=True, capture_output=True, text=True)


def build_crop_boxes(image_width: int, image_height: int, toolbar_height: int, bottom_trim: int) -> tuple[CropBox, CropBox]:
    usable_top = toolbar_height
    usable_bottom = max(usable_top + 1, image_height - bottom_trim)
    mid_x = image_width // 2
    left_box = CropBox(0, usable_top, mid_x, usable_bottom)
    right_box = CropBox(mid_x, usable_top, image_width, usable_bottom)
    return left_box, right_box


def load_hotspots(hotspots_json: Path) -> list[HotspotSpec]:
    raw = json.loads(hotspots_json.read_text())
    hotspots: list[HotspotSpec] = []
    for item in raw:
        hotspots.append(
            HotspotSpec(
                id=item["id"],
                label=item["label"],
                notes=item.get("notes", ""),
                pane_box_norm=tuple(item["pane_box_norm"]),
                backend_focus=tuple(item.get("backend_focus", [])),
            )
        )
    return hotspots


def normalized_box_to_pixels(box_norm: tuple[float, float, float, float], pane_width: int, pane_height: int) -> CropBox:
    left = max(0, min(pane_width, round(box_norm[0] * pane_width)))
    top = max(0, min(pane_height, round(box_norm[1] * pane_height)))
    right = max(left + 1, min(pane_width, round(box_norm[2] * pane_width)))
    bottom = max(top + 1, min(pane_height, round(box_norm[3] * pane_height)))
    return CropBox(left, top, right, bottom)


def compute_visual_metrics(left_img: Image.Image, right_img: Image.Image) -> dict[str, Any]:
    if left_img.size != right_img.size:
        raise ValueError(f"image sizes differ: {left_img.size} vs {right_img.size}")

    diff = ImageChops.difference(left_img, right_img)
    diff_rgb = diff.convert("RGB")
    diff_gray = diff.convert("L")
    stat = ImageStat.Stat(diff_rgb)
    mean_abs = [round(v, 4) for v in stat.mean]
    rms = [round(v, 4) for v in stat.rms]

    bbox = diff_gray.getbbox()
    nonzero_pixels = 0
    if bbox:
        mask = diff_gray.point(lambda value: 255 if value else 0)
        histogram = mask.histogram()
        nonzero_pixels = histogram[255] if len(histogram) > 255 else 0

    total_pixels = left_img.size[0] * left_img.size[1]
    changed_ratio = round(nonzero_pixels / total_pixels, 6) if total_pixels else 0.0

    return {
        "image_width": left_img.size[0],
        "image_height": left_img.size[1],
        "total_pixels": total_pixels,
        "changed_pixels": nonzero_pixels,
        "changed_ratio": changed_ratio,
        "mean_abs_rgb": mean_abs,
        "rms_rgb": rms,
        "diff_bbox": list(bbox) if bbox else None,
        "diff_image": diff,
    }


def save_visual_outputs(
    full_png: Path,
    left_png: Path,
    right_png: Path,
    diff_png: Path,
    toolbar_height: int,
    bottom_trim: int,
    hotspots: list[HotspotSpec],
    output_dir: Path,
) -> dict[str, Any]:
    image = Image.open(full_png).convert("RGBA")
    left_box, right_box = build_crop_boxes(image.width, image.height, toolbar_height, bottom_trim)
    left = image.crop(left_box.as_tuple())
    right = image.crop(right_box.as_tuple())
    left.save(left_png)
    right.save(right_png)

    metrics = compute_visual_metrics(left, right)
    diff_img = metrics.pop("diff_image")
    diff_boost = diff_img.convert("RGB").point(lambda value: min(255, value * 4))
    diff_boost.save(diff_png)

    metrics["full_crop_left"] = list(left_box.as_tuple())
    metrics["full_crop_right"] = list(right_box.as_tuple())
    metrics["hotspots"] = save_hotspot_outputs(left, right, hotspots, output_dir)
    return metrics


def save_hotspot_outputs(
    left_img: Image.Image,
    right_img: Image.Image,
    hotspots: list[HotspotSpec],
    output_dir: Path,
) -> list[dict[str, Any]]:
    hotspot_dir = output_dir / "hotspots"
    hotspot_dir.mkdir(parents=True, exist_ok=True)

    results: list[dict[str, Any]] = []
    for hotspot in hotspots:
        box = normalized_box_to_pixels(hotspot.pane_box_norm, left_img.width, left_img.height)
        left_crop = left_img.crop(box.as_tuple())
        right_crop = right_img.crop(box.as_tuple())
        metrics = compute_visual_metrics(left_crop, right_crop)
        diff_img = metrics.pop("diff_image")
        diff_boost = diff_img.convert("RGB").point(lambda value: min(255, value * 4))

        left_path = hotspot_dir / f"{hotspot.id}_left.png"
        right_path = hotspot_dir / f"{hotspot.id}_right.png"
        diff_path = hotspot_dir / f"{hotspot.id}_diff.png"
        left_crop.save(left_path)
        right_crop.save(right_path)
        diff_boost.save(diff_path)

        metrics["id"] = hotspot.id
        metrics["label"] = hotspot.label
        metrics["notes"] = hotspot.notes
        metrics["backend_focus"] = list(hotspot.backend_focus)
        metrics["pane_box_norm"] = list(hotspot.pane_box_norm)
        metrics["pane_box_pixels"] = list(box.as_tuple())
        metrics["left_image"] = str(left_path)
        metrics["right_image"] = str(right_path)
        metrics["diff_image"] = str(diff_path)
        results.append(metrics)

    return sorted(results, key=lambda item: item["changed_ratio"], reverse=True)


def load_backend_summary(report_json_path: Path) -> dict[str, Any]:
    data = json.loads(report_json_path.read_text())

    def top_list(items: list[dict[str, Any]], limit: int = 10) -> list[dict[str, Any]]:
        return sorted(items, key=lambda item: item["count"], reverse=True)[:limit]

    return {
        "original_feature_instances": data["original_feature_instances"],
        "engineering_feature_instances": data["engineering_feature_instances"],
        "result_count": data["result_count"],
        "status_counts": data["status_counts"],
        "top_annotation_issues": top_list(data.get("annotation_issue_counts", [])),
        "top_style_semantic_issues": top_list(data.get("style_semantic_issue_counts", [])),
        "top_source_layer_issues": top_list(data.get("source_layer_issue_counts", [])),
    }


def write_reports(
    output_dir: Path,
    compare_url: str,
    compare_version: str | None,
    backend_audit_json: Path,
    visual_metrics: dict[str, Any],
    backend_summary: dict[str, Any],
) -> tuple[Path, Path]:
    report_json = output_dir / "strict_audit_summary.json"
    report_md = output_dir / "strict_audit_summary.md"

    summary = {
        "compare_url": compare_url,
        "compare_version": compare_version,
        "backend_audit_json": str(backend_audit_json),
        "visual_metrics": visual_metrics,
        "backend_summary": backend_summary,
        "notes": [
            "Visual metrics are browser-rendered screenshot diff metrics from the fixed compare page.",
            "Backend metrics are current render-audit summary metrics and may remain blind to sprite-load failures.",
        ],
    }
    report_json.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")

    lines = [
        "# NavSea Strict Audit Summary",
        "",
        "## Scope",
        "",
        f"- Compare page: `{compare_url}`",
        f"- Compare version: `{compare_version or 'unknown'}`",
        f"- Backend render audit: `{backend_audit_json}`",
        "",
        "## Visual Audit",
        "",
        f"- changed pixels: `{visual_metrics['changed_pixels']}` / `{visual_metrics['total_pixels']}`",
        f"- changed ratio: `{visual_metrics['changed_ratio']}`",
        f"- mean abs rgb: `{visual_metrics['mean_abs_rgb']}`",
        f"- rms rgb: `{visual_metrics['rms_rgb']}`",
        f"- left crop: `{visual_metrics['full_crop_left']}`",
        f"- right crop: `{visual_metrics['full_crop_right']}`",
        "",
        "Artifacts:",
        "",
        f"- `{output_dir / 'compare_full.png'}`",
        f"- `{output_dir / 'compare_left.png'}`",
        f"- `{output_dir / 'compare_right.png'}`",
        f"- `{output_dir / 'compare_diff.png'}`",
        "",
        "## Hotspot AOI Audit",
        "",
    ]

    for hotspot in visual_metrics.get("hotspots", []):
        lines.extend(
            [
                f"### {hotspot['label']}",
                "",
                f"- id: `{hotspot['id']}`",
                f"- notes: `{hotspot['notes']}`",
                f"- backend focus: `{hotspot['backend_focus']}`",
                f"- changed pixels: `{hotspot['changed_pixels']}` / `{hotspot['total_pixels']}`",
                f"- changed ratio: `{hotspot['changed_ratio']}`",
                f"- mean abs rgb: `{hotspot['mean_abs_rgb']}`",
                f"- pane box norm: `{hotspot['pane_box_norm']}`",
                f"- pane box pixels: `{hotspot['pane_box_pixels']}`",
                f"- left image: `{hotspot['left_image']}`",
                f"- right image: `{hotspot['right_image']}`",
                f"- diff image: `{hotspot['diff_image']}`",
                "",
            ]
        )

    lines.extend(
        [
        "## Backend Render Audit",
        "",
        f"- original feature instances: `{backend_summary['original_feature_instances']}`",
        f"- delivery feature instances: `{backend_summary['engineering_feature_instances']}`",
        f"- result count: `{backend_summary['result_count']}`",
        "",
        "Status counts:",
        "",
        ]
    )
    for key, value in backend_summary["status_counts"].items():
        lines.append(f"- `{key}`: `{value}`")

    lines.extend(["", "Top annotation issues:", ""])
    for item in backend_summary["top_annotation_issues"][:10]:
        lines.append(f"- `{item['issue']}` | `{item['source_layer']}` | `{item['count']}`")

    lines.extend(["", "Top style semantic issues:", ""])
    for item in backend_summary["top_style_semantic_issues"][:10]:
        lines.append(f"- `{item['issue']}` | `{item['source_layer']}` | `{item['count']}`")

    lines.extend(
        [
            "",
            "## Interpretation",
            "",
            "- This report intentionally puts browser-rendered visual output and backend render-audit summary in one place.",
            "- If the browser screenshot improves but backend counts do not, the current backend audit is likely blind to a browser/runtime issue such as sprite resolution.",
        ]
    )
    report_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
    return report_md, report_json


def main() -> None:
    args = parse_args()
    args.output_dir.mkdir(parents=True, exist_ok=True)

    compare_version = fetch_compare_version(args.compare_url)
    hotspots = load_hotspots(args.hotspots_json)
    full_png = args.output_dir / "compare_full.png"
    left_png = args.output_dir / "compare_left.png"
    right_png = args.output_dir / "compare_right.png"
    diff_png = args.output_dir / "compare_diff.png"

    run_browser_screenshot(args.compare_url, full_png, args.width, args.height, args.timeout_sec)
    visual_metrics = save_visual_outputs(
        full_png=full_png,
        left_png=left_png,
        right_png=right_png,
        diff_png=diff_png,
        toolbar_height=args.toolbar_height,
        bottom_trim=args.bottom_trim,
        hotspots=hotspots,
        output_dir=args.output_dir,
    )
    backend_summary = load_backend_summary(args.backend_audit_json)
    report_md, report_json = write_reports(
        output_dir=args.output_dir,
        compare_url=args.compare_url,
        compare_version=compare_version,
        backend_audit_json=args.backend_audit_json,
        visual_metrics=visual_metrics,
        backend_summary=backend_summary,
    )

    print(
        json.dumps(
            {
                "compare_version": compare_version,
                "report_md": str(report_md),
                "report_json": str(report_json),
                "changed_ratio": visual_metrics["changed_ratio"],
                "changed_pixels": visual_metrics["changed_pixels"],
                "hotspots": [
                    {
                        "id": item["id"],
                        "changed_ratio": item["changed_ratio"],
                    }
                    for item in visual_metrics.get("hotspots", [])
                ],
            },
            ensure_ascii=False,
            indent=2,
        )
    )


if __name__ == "__main__":
    main()
