from __future__ import annotations

import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, Iterable, List

import fullbleed

ROOT = Path(__file__).resolve().parent
TEMPLATES = ROOT / "templates"
OUTPUT = ROOT / "output"
VENDOR = ROOT / "vendor"

HTML_TEMPLATE_PATH = TEMPLATES / "iconography_smoke.html"
CSS_PATH = TEMPLATES / "iconography_smoke.css"
SPRITE_PATH = VENDOR / "icons" / "bootstrap-icons.svg"
JIT_LOG_PATH = OUTPUT / "iconography_smoke.jit.jsonl"
PERF_LOG_PATH = OUTPUT / "iconography_smoke.perf.jsonl"
PDF_PATH = OUTPUT / "iconography_smoke.pdf"
PNG_PATH = OUTPUT / "iconography_smoke_page1.png"
SUMMARY_PATH = OUTPUT / "iconography_smoke_summary.json"

ICON_IDS = [
    "alarm",
    "calendar-event",
    "check-circle",
    "cloud-download",
    "file-earmark-text",
    "house-door",
    "shield-check",
    "tools",
]


def _utc_stamp() -> str:
    return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")


def _ensure_writable(path: Path) -> Path:
    if not path.exists():
        return path
    try:
        path.unlink()
        return path
    except PermissionError:
        stamped = path.with_name(f"{path.stem}_{_utc_stamp()}{path.suffix}")
        return stamped


def _extract_symbols(svg_text: str, icon_ids: Iterable[str]) -> Dict[str, str]:
    symbols: Dict[str, str] = {}
    pattern = re.compile(r'<symbol\b[^>]*\bid="([^"]+)"[^>]*>.*?</symbol>', re.S)
    for match in pattern.finditer(svg_text):
        symbol_id = match.group(1)
        if symbol_id in icon_ids:
            symbols[symbol_id] = match.group(0)
    return symbols


def _render_expanded_icon(symbol_text: str, icon_id: str) -> str:
    open_tag = re.search(r"<symbol\b([^>]*)>", symbol_text, re.S)
    viewbox = "0 0 16 16"
    if open_tag:
        attrs = open_tag.group(1)
        m = re.search(r'viewBox="([^"]+)"', attrs)
        if m:
            viewbox = m.group(1)
    inner = re.sub(r"^<symbol\b[^>]*>|</symbol>$", "", symbol_text, flags=re.S).strip()
    return f'<svg class="fb-icon" viewBox="{viewbox}" aria-label="{icon_id}">{inner}</svg>'


def _render_icon_grid(icon_ids: Iterable[str], mode: str, extracted_symbols: Dict[str, str]) -> str:
    blocks: List[str] = []
    for icon_id in icon_ids:
        if mode == "inline":
            icon_markup = (
                f'<svg class="fb-icon" viewBox="0 0 16 16" aria-label="{icon_id}">'
                f'<use href="#{icon_id}"></use></svg>'
            )
        elif mode == "external":
            icon_markup = (
                f'<svg class="fb-icon" viewBox="0 0 16 16" aria-label="{icon_id}">'
                f'<use href="vendor/icons/bootstrap-icons.svg#{icon_id}"></use></svg>'
            )
        elif mode == "expanded":
            symbol_text = extracted_symbols.get(icon_id)
            if symbol_text:
                icon_markup = _render_expanded_icon(symbol_text, icon_id)
            else:
                icon_markup = '<div class="fb-icon-missing">?</div>'
        else:
            raise ValueError(f"Unknown icon grid mode: {mode}")
        blocks.append(
            "<div class=\"icon-tile\">"
            f"{icon_markup}"
            f"<div class=\"icon-label\">{icon_id}</div>"
            "</div>"
        )
    return "\n".join(blocks)


def _build_html(extracted_symbols: Dict[str, str], missing_symbols: List[str]) -> str:
    template = HTML_TEMPLATE_PATH.read_text(encoding="utf-8")
    symbol_block = "\n".join(extracted_symbols.values())
    icon_grid_inline = _render_icon_grid(ICON_IDS, mode="inline", extracted_symbols=extracted_symbols)
    icon_grid_external = _render_icon_grid(ICON_IDS, mode="external", extracted_symbols=extracted_symbols)
    icon_grid_expanded = _render_icon_grid(ICON_IDS, mode="expanded", extracted_symbols=extracted_symbols)
    missing_text = ", ".join(missing_symbols) if missing_symbols else "none"
    html = (
        template.replace("{{ICON_SYMBOLS}}", symbol_block)
        .replace("{{ICON_GRID_INLINE}}", icon_grid_inline)
        .replace("{{ICON_GRID_EXTERNAL}}", icon_grid_external)
        .replace("{{ICON_GRID_EXPANDED}}", icon_grid_expanded)
        .replace("{{MISSING_SYMBOLS}}", missing_text)
        .replace("{{RUN_TS}}", datetime.now(timezone.utc).isoformat())
    )
    return html


def _read_jsonl(path: Path) -> List[dict]:
    if not path.exists():
        return []
    events: List[dict] = []
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            events.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    return events


def _count_pages(path: Path) -> int:
    try:
        import fitz  # type: ignore
    except Exception:
        return 0
    doc = fitz.open(path)
    try:
        return int(doc.page_count)
    finally:
        doc.close()


def _pdf_font_names(path: Path) -> List[str]:
    try:
        import fitz  # type: ignore
    except Exception:
        return []
    doc = fitz.open(path)
    names: List[str] = []
    try:
        for page in doc:
            for row in page.get_fonts(full=True):
                if len(row) >= 5:
                    # PyMuPDF tuples commonly look like:
                    # (xref, ext, subtype, fontname, resource_name, encoding, embedded)
                    names.append(str(row[3]))
    finally:
        doc.close()
    # preserve order but dedupe
    out: List[str] = []
    seen = set()
    for name in names:
        if name not in seen:
            out.append(name)
            seen.add(name)
    return out


def _emit_preview_png(pdf_path: Path, png_path: Path) -> str:
    try:
        import fitz  # type: ignore
    except Exception:
        return "skipped: PyMuPDF not installed"
    doc = fitz.open(pdf_path)
    try:
        if doc.page_count == 0:
            return "skipped: PDF has no pages"
        pix = doc[0].get_pixmap(dpi=144, alpha=False)
        pix.save(png_path)
    finally:
        doc.close()
    return "ok"


def create_engine(debug_out: Path, perf_out: Path) -> fullbleed.PdfEngine:
    bundle = fullbleed.AssetBundle()
    bundle.add_file(str(VENDOR / "fonts" / "NotoSans-Regular.ttf"), "font", name="noto-sans")
    bundle.add_file(str(VENDOR / "css" / "bootstrap.min.css"), "css", name="bootstrap")
    bundle.add_file(str(VENDOR / "fonts" / "Inter-Variable.ttf"), "font", name="inter")
    bundle.add_file(str(SPRITE_PATH), "svg", name="bootstrap-icons")

    engine = fullbleed.PdfEngine(
        page_width="8.5in",
        page_height="11in",
        margin="0.4in",
        pdf_version="1.7",
        debug=True,
        debug_out=str(debug_out),
        perf=True,
        perf_out=str(perf_out),
    )
    engine.register_bundle(bundle)
    return engine


def main() -> None:
    OUTPUT.mkdir(parents=True, exist_ok=True)

    pdf_path = _ensure_writable(PDF_PATH)
    jit_log_path = _ensure_writable(JIT_LOG_PATH)
    perf_log_path = _ensure_writable(PERF_LOG_PATH)
    png_path = _ensure_writable(PNG_PATH)
    summary_path = _ensure_writable(SUMMARY_PATH)

    sprite_text = SPRITE_PATH.read_text(encoding="utf-8")
    extracted_symbols = _extract_symbols(sprite_text, ICON_IDS)
    missing_symbols = [icon_id for icon_id in ICON_IDS if icon_id not in extracted_symbols]

    html = _build_html(extracted_symbols=extracted_symbols, missing_symbols=missing_symbols)
    css = CSS_PATH.read_text(encoding="utf-8")

    engine = create_engine(debug_out=jit_log_path, perf_out=perf_log_path)
    bytes_written = engine.render_pdf_to_file(html, css, str(pdf_path))
    preview_status = _emit_preview_png(pdf_path, png_path)
    page_count = _count_pages(pdf_path)
    pdf_font_names = _pdf_font_names(pdf_path)
    lowered_font_names = [n.lower() for n in pdf_font_names]

    jit_events = _read_jsonl(jit_log_path)
    perf_events = _read_jsonl(perf_log_path)
    svg_compile_events = [
        e for e in perf_events if e.get("type") == "perf.span" and e.get("name") == "svg.compile"
    ]
    html_asset_warnings = [e for e in jit_events if e.get("type") == "jit.html_asset_warning"]

    checks = {
        "vendored_bootstrap_css": (VENDOR / "css" / "bootstrap.min.css").exists(),
        "vendored_noto_sans_font": (VENDOR / "fonts" / "NotoSans-Regular.ttf").exists(),
        "vendored_inter_font": (VENDOR / "fonts" / "Inter-Variable.ttf").exists(),
        "vendored_bootstrap_icons_sprite": SPRITE_PATH.exists(),
        "extracted_requested_symbols": len(missing_symbols) == 0,
        "rendered_pdf_bytes_gt_zero": bytes_written > 0,
        "pdf_embeds_noto_sans": any("noto" in n for n in lowered_font_names),
        "single_page_layout_expected": page_count == 1,
        "svg_compile_events_present": len(svg_compile_events) > 0,
        "html_asset_warnings_absent": len(html_asset_warnings) == 0,
    }

    summary = {
        "schema": "fullbleed.iconography_smoke.v1",
        "ok": all(checks.values()),
        "generated_at_utc": datetime.now(timezone.utc).isoformat(),
        "reference_image": str((ROOT / "src.png").resolve()),
        "artifacts": {
            "pdf": str(pdf_path.resolve()),
            "preview_png": str(png_path.resolve()) if png_path.exists() else None,
            "jit_log": str(jit_log_path.resolve()),
            "perf_log": str(perf_log_path.resolve()),
        },
        "checks": checks,
        "metrics": {
            "pdf_bytes": bytes_written,
            "pdf_page_count": page_count,
            "jit_event_count": len(jit_events),
            "perf_event_count": len(perf_events),
            "svg_compile_event_count": len(svg_compile_events),
            "html_asset_warning_count": len(html_asset_warnings),
            "requested_icon_count": len(ICON_IDS),
            "extracted_icon_count": len(extracted_symbols),
            "missing_icons": missing_symbols,
            "preview_png_status": preview_status,
            "pdf_font_names": pdf_font_names,
            "icon_modes_tested": ["inline_symbol_ref", "external_sprite_ref", "expanded_symbol_svg"],
        },
    }
    summary_path.write_text(json.dumps(summary, ensure_ascii=True, indent=2), encoding="utf-8")

    print(f"[ok] Wrote {pdf_path} ({bytes_written} bytes)")
    print(f"[ok] Wrote {summary_path}")
    if png_path.exists():
        print(f"[ok] Wrote {png_path}")
    if missing_symbols:
        print(f"[warn] Missing requested icons in sprite: {', '.join(missing_symbols)}")


if __name__ == "__main__":
    main()
