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

import argparse
import html
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

DEFAULT_TITLE = "Audit export"
DEFAULT_CSS = r'''
@page {
  size: A4 landscape;
  margin: 11mm;
}

html, body {
  margin: 0;
  padding: 0;
  color: #0f172a;
  background: #ffffff;
  font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
  font-size: 11pt;
  line-height: 1.45;
}

* {
  box-sizing: border-box;
}

body {
  padding: 0;
}

.pdf-shell {
  width: 100%;
}

.simple-chat-message__artifacts {
  display: block;
}

.simple-chat-message__artifacts-toolbar,
.audit-feed-block__actions,
button,
input[type="hidden"] {
  display: none !important;
}

.simple-chat-message__section,
.simple-audit-dashboard__section,
.simple-audit-table-block,
.simple-chat-chart-card,
.audit-feed-block,
.simple-chat-linked-post,
.simple-chat-linked-comment {
  break-inside: avoid;
  page-break-inside: avoid;
}

.simple-audit-dashboard {
  display: block;
}

.simple-audit-dashboard__head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 10px;
  margin: 0 0 14px;
  padding: 0 0 10px;
  border-bottom: 2px solid #dbe4f0;
}

.simple-audit-dashboard__head h3 {
  margin: 0;
  font-size: 20pt;
  line-height: 1.15;
}

.simple-audit-dashboard__head p,
.simple-chat-chart__summary span,
.simple-audit-dashboard__meta {
  margin: 6px 0 0;
  color: #475569;
}

.simple-chat-message__section,
.simple-audit-dashboard__section,
.audit-feed-block,
.simple-chat-linked-post,
.simple-chat-linked-comment {
  margin: 0 0 14px;
  padding: 12px 14px;
  border: 1px solid #dbe4f0;
  border-radius: 12px;
  background: #ffffff;
}

.simple-audit-dashboard__section-head {
  margin: 0 0 8px;
}

.simple-audit-dashboard__section-head strong {
  display: inline-block;
  font-size: 12pt;
}

.simple-audit-overview-grid,
.simple-audit-comparison-grid,
.simple-audit-comments-grid,
.simple-audit-dashboard__split,
.simple-audit-embedded-charts,
.simple-chat-chart__grid,
.simple-chat-audit-blocks,
.simple-audit-dashboard__cards,
.simple-audit-dashboard__groups,
.simple-audit-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: stretch;
}

.simple-audit-overview-grid__cards,
.simple-audit-overview-grid__charts,
.simple-audit-comparison-grid__table,
.simple-audit-comparison-grid__charts,
.simple-audit-comments-grid__charts,
.simple-audit-comments-grid__groups,
.simple-audit-dashboard__split-col {
  flex: 1 1 320px;
  min-width: 280px;
}

.simple-audit-card,
.simple-audit-pill,
.simple-chat-chart-card {
  flex: 1 1 220px;
  min-width: 200px;
  padding: 12px;
  border-radius: 12px;
  border: 1px solid #dbe4f0;
  background: #f8fafc;
}

.simple-audit-card__label,
.simple-audit-pill__label,
.simple-chat-chart-card__head-sub,
.simple-chat-chart-card__range,
.audit-feed-block__description,
.audit-feed-block__connector-summary,
.simple-chat-chart__note {
  display: block;
  color: #475569;
  font-size: 9pt;
}

.simple-audit-card__value,
.simple-audit-pill__value {
  display: block;
  margin-top: 5px;
  font-size: 17pt;
  line-height: 1.1;
}

.simple-audit-card__note {
  display: block;
  margin-top: 6px;
  color: #334155;
  font-size: 9pt;
}

.simple-audit-pill {
  min-width: 150px;
}

.simple-audit-pill--positive {
  background: #ecfdf5;
  border-color: #a7f3d0;
}

.simple-audit-pill--negative {
  background: #fef2f2;
  border-color: #fecaca;
}

.simple-audit-pill--warning {
  background: #fffbeb;
  border-color: #fde68a;
}

.simple-audit-pill--neutral {
  background: #eff6ff;
  border-color: #bfdbfe;
}

.simple-audit-narrative,
.simple-audit-stack__intro,
.simple-audit-stack__body,
.audit-feed-block__note--export,
.audit-export-field-value,
.audit-feed-block__title-text {
  color: #0f172a;
}

.simple-audit-narrative__p,
.simple-audit-stack__intro p,
.simple-audit-stack__body p,
.audit-feed-block__note--export {
  margin: 0 0 8px;
}

.simple-audit-narrative__list ul,
.simple-audit-notes,
.simple-chat-linked-post__panel ul,
.simple-chat-linked-comment__context ul {
  margin: 0;
  padding-left: 18px;
}

.simple-chat-chart__summary {
  margin: 0 0 10px;
}

.simple-chat-chart__summary strong {
  font-size: 13pt;
}

.simple-chat-chart__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 12px;
  margin-top: 10px;
}

.simple-chat-chart__legend-item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 9pt;
  color: #334155;
}

.simple-chat-chart__legend-swatch {
  width: 10px;
  height: 10px;
  border-radius: 999px;
  flex: 0 0 10px;
}

svg {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
}

.simple-chat-chart-card svg text {
  font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
}

.simple-audit-table-wrap {
  width: 100%;
  overflow: hidden;
}

.simple-audit-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  font-size: 9pt;
}

.simple-audit-table thead th {
  background: #eff6ff;
  color: #0f172a;
}

.simple-audit-table th,
.simple-audit-table td {
  border: 1px solid #dbe4f0;
  padding: 8px 9px;
  vertical-align: top;
  word-break: break-word;
}

.simple-audit-table td.is-right,
.simple-audit-table th.is-right {
  text-align: right;
}

.audit-feed-block__head {
  display: block;
  margin-bottom: 8px;
}

.audit-feed-block__title {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.audit-feed-block__title span {
  display: inline-block;
  padding: 3px 8px;
  border-radius: 999px;
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  color: #1d4ed8;
  font-size: 9pt;
}

.audit-feed-block__title-text {
  font-weight: 700;
  font-size: 12pt;
}

.audit-feed-block__connectors {
  margin-bottom: 8px;
}

.audit-feed-block__canvas {
  margin: 10px 0;
  padding: 10px;
  border-radius: 10px;
  border: 1px solid #dbe4f0;
  background: #f8fafc;
  min-height: 80px;
}

.audit-feed-block__canvas-bar,
.audit-feed-block__canvas-grid {
  min-height: 18px;
}

.audit-feed-block__note--export,
.audit-export-field-value {
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid #dbe4f0;
  background: #f8fafc;
  white-space: pre-wrap;
}

textarea,
select,
input {
  display: none !important;
}

.simple-chat-linked-post__details,
.simple-chat-linked-comment .simple-chat-linked-post__details {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.simple-chat-linked-post__detail {
  flex: 1 1 140px;
  min-width: 120px;
  padding: 8px 10px;
  border-radius: 10px;
  border: 1px solid #dbe4f0;
  background: #f8fafc;
}

.simple-chat-linked-post__detail strong {
  display: block;
  margin-bottom: 4px;
  font-size: 8pt;
  color: #475569;
  text-transform: uppercase;
  letter-spacing: .03em;
}

.simple-chat-linked-post__detail span,
.simple-chat-linked-post__detail a,
.simple-chat-linked-comment__context,
.simple-chat-linked-comment__context li {
  font-size: 9pt;
  color: #0f172a;
}

a {
  color: #1d4ed8;
  text-decoration: none;
}
'''

SCRIPT_TAG_RE = re.compile(r"<script\b[^>]*>.*?</script>", re.IGNORECASE | re.DOTALL)
META_CHARSET_RE = re.compile(r"<meta\s+charset=[^>]+>", re.IGNORECASE)


class PdfEngineError(RuntimeError):
    pass


def ensure_writable_directory(path: Path) -> Path:
    path.mkdir(parents=True, exist_ok=True)
    probe = path / ".write_test"
    probe.write_text("ok", encoding="utf-8")
    probe.unlink(missing_ok=True)
    return path


def configure_runtime_environment() -> Path:
    candidates: list[Path] = []
    for raw in [os.environ.get("TMPDIR"), tempfile.gettempdir(), "/tmp", os.getcwd()]:
        if raw:
            candidates.append(Path(raw).resolve())

    last_error: Exception | None = None
    runtime_root: Path | None = None
    for base in candidates:
        try:
            base.mkdir(parents=True, exist_ok=True)
            runtime_root = ensure_writable_directory(base / "simple_chat_pdf_runtime")
            break
        except Exception as exc:  # noqa: BLE001
            last_error = exc

    if runtime_root is None:
        raise RuntimeError(f"Unable to create writable runtime directory: {last_error}")

    home_dir = ensure_writable_directory(runtime_root / "home")
    cache_dir = ensure_writable_directory(runtime_root / "cache")
    config_dir = ensure_writable_directory(runtime_root / "config")
    ensure_writable_directory(home_dir / ".cache")
    ensure_writable_directory(home_dir / ".config")
    ensure_writable_directory(cache_dir / "fontconfig")
    ensure_writable_directory(config_dir / "fontconfig")

    os.environ["HOME"] = str(home_dir)
    os.environ["XDG_CACHE_HOME"] = str(cache_dir)
    os.environ["XDG_CONFIG_HOME"] = str(config_dir)
    os.environ.setdefault("TMPDIR", str(runtime_root))

    return runtime_root


RUNTIME_ROOT = configure_runtime_environment()


def read_fragment(input_path: str | None) -> str:
    if input_path:
        return Path(input_path).read_text(encoding="utf-8")
    return sys.stdin.read()


def normalize_fragment(fragment_html: str) -> str:
    body = SCRIPT_TAG_RE.sub("", fragment_html).strip()
    if not body:
        raise ValueError("Empty artifacts HTML.")
    return body


def build_document(fragment_html: str, title: str) -> str:
    safe_title = html.escape(title or DEFAULT_TITLE, quote=True)
    return f'''<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{safe_title}</title>
  <style>{DEFAULT_CSS}</style>
</head>
<body>
  <main class="pdf-shell">{fragment_html}</main>
</body>
</html>
'''


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Convert simple-chat-message__artifacts HTML to PDF.")
    parser.add_argument("--input", help="Path to the input HTML fragment file. Reads stdin when omitted.")
    parser.add_argument("--output", required=True, help="Output PDF path.")
    parser.add_argument("--title", default=DEFAULT_TITLE, help="Document title.")
    parser.add_argument("--base-url", default=None, help="Base URL for relative assets.")
    return parser.parse_args()


def find_binary(candidates: list[str]) -> str | None:
    for candidate in candidates:
        resolved = shutil.which(candidate)
        if resolved:
            return resolved
    return None


def run_command(command: list[str], *, env: dict[str, str], cwd: str | None = None) -> tuple[int, str, str]:
    process = subprocess.run(
        command,
        cwd=cwd,
        env=env,
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
    )
    return process.returncode, process.stdout.strip(), process.stderr.strip()


def render_with_chromium(document_path: Path, output_path: Path, env: dict[str, str]) -> str:
    binary = find_binary([
        "chromium",
        "chromium-browser",
        "google-chrome",
        "google-chrome-stable",
        "chrome",
    ])
    if not binary:
        raise PdfEngineError("Chromium/Chrome not found in PATH.")

    command = [
        binary,
        "--headless",
        "--disable-gpu",
        "--no-sandbox",
        "--allow-file-access-from-files",
        "--disable-dev-shm-usage",
        "--run-all-compositor-stages-before-draw",
        f"--print-to-pdf={str(output_path)}",
        "--print-to-pdf-no-header",
        document_path.as_uri(),
    ]
    code, stdout, stderr = run_command(command, env=env, cwd=str(document_path.parent))
    if code != 0 or not output_path.is_file() or output_path.stat().st_size == 0:
        raise PdfEngineError(
            f"Chromium failed with exit code {code}. stdout={stdout or '<empty>'} stderr={stderr or '<empty>'}"
        )
    return f"chromium:{binary}"


def render_with_wkhtmltopdf(document_path: Path, output_path: Path, env: dict[str, str]) -> str:
    binary = find_binary(["wkhtmltopdf"])
    if not binary:
        raise PdfEngineError("wkhtmltopdf not found in PATH.")

    command = [
        binary,
        "--quiet",
        "--enable-local-file-access",
        "--encoding",
        "utf-8",
        "--page-size",
        "A4",
        "--orientation",
        "Landscape",
        "--margin-top",
        "11mm",
        "--margin-right",
        "11mm",
        "--margin-bottom",
        "11mm",
        "--margin-left",
        "11mm",
        str(document_path),
        str(output_path),
    ]
    code, stdout, stderr = run_command(command, env=env, cwd=str(document_path.parent))
    if code != 0 or not output_path.is_file() or output_path.stat().st_size == 0:
        raise PdfEngineError(
            f"wkhtmltopdf failed with exit code {code}. stdout={stdout or '<empty>'} stderr={stderr or '<empty>'}"
        )
    return f"wkhtmltopdf:{binary}"


def render_with_weasyprint(document_html: str, output_path: Path, base_url: str, env: dict[str, str]) -> str:
    try:
        from weasyprint import HTML  # type: ignore
    except Exception as exc:  # noqa: BLE001
        raise PdfEngineError(f"WeasyPrint import failed: {exc}") from exc

    previous_env = {key: os.environ.get(key) for key in env.keys()}
    os.environ.update(env)
    try:
        HTML(string=document_html, base_url=base_url).write_pdf(str(output_path))
    except Exception as exc:  # noqa: BLE001
        raise PdfEngineError(f"WeasyPrint render failed: {exc}") from exc
    finally:
        for key, value in previous_env.items():
            if value is None:
                os.environ.pop(key, None)
            else:
                os.environ[key] = value

    if not output_path.is_file() or output_path.stat().st_size == 0:
        raise PdfEngineError("WeasyPrint did not create a valid PDF file.")
    return "weasyprint"


def write_temp_document(document_html: str, runtime_root: Path) -> Path:
    export_dir = ensure_writable_directory(runtime_root / "render_input")
    document_path = export_dir / "audit_export_source.html"
    document_path.write_text(document_html, encoding="utf-8")
    return document_path


def render_pdf(document_html: str, output_path: Path, base_url: str) -> str:
    runtime_env = {
        "HOME": os.environ.get("HOME", str(RUNTIME_ROOT / "home")),
        "XDG_CACHE_HOME": os.environ.get("XDG_CACHE_HOME", str(RUNTIME_ROOT / "cache")),
        "XDG_CONFIG_HOME": os.environ.get("XDG_CONFIG_HOME", str(RUNTIME_ROOT / "config")),
        "TMPDIR": os.environ.get("TMPDIR", str(RUNTIME_ROOT)),
    }
    env = os.environ.copy()
    env.update(runtime_env)

    document_path = write_temp_document(document_html, RUNTIME_ROOT)
    engine_errors: list[str] = []

    for renderer in (
        lambda: render_with_chromium(document_path, output_path, env),
        lambda: render_with_wkhtmltopdf(document_path, output_path, env),
        lambda: render_with_weasyprint(document_html, output_path, base_url, runtime_env),
    ):
        try:
            return renderer()
        except Exception as exc:  # noqa: BLE001
            engine_errors.append(str(exc))
            if output_path.exists():
                output_path.unlink(missing_ok=True)

    raise PdfEngineError(" | ".join(engine_errors))


def main() -> int:
    args = parse_args()
    raw_fragment = read_fragment(args.input).strip()
    if not raw_fragment:
        print("Input HTML fragment is empty.", file=sys.stderr)
        return 1

    try:
        normalized_fragment = normalize_fragment(raw_fragment)
        document_html = build_document(normalized_fragment, args.title.strip() or DEFAULT_TITLE)
        output_path = Path(args.output).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)
        base_url = args.base_url or (str(Path(args.input).resolve().parent) if args.input else os.getcwd())
        engine_used = render_pdf(document_html, output_path, base_url)
        print(f"OK:{engine_used}")
        return 0
    except Exception as exc:  # noqa: BLE001
        diagnostics = (
            f"HOME={os.environ.get('HOME')}"
            f" | XDG_CACHE_HOME={os.environ.get('XDG_CACHE_HOME')}"
            f" | XDG_CONFIG_HOME={os.environ.get('XDG_CONFIG_HOME')}"
            f" | TMPDIR={os.environ.get('TMPDIR')}"
            f" | RUNTIME_ROOT={RUNTIME_ROOT}"
            f" | chromium={find_binary(['chromium', 'chromium-browser', 'google-chrome', 'google-chrome-stable', 'chrome']) or 'missing'}"
            f" | wkhtmltopdf={find_binary(['wkhtmltopdf']) or 'missing'}"
        )
        print(f"PDF generation failed: {exc} | {diagnostics}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
