#!/usr/bin/env python3
"""
Render PlantUML diagrams (.puml) to high-resolution PNG files via Kroki.

Why this script:
- local `plantuml` may require X11 in some environments;
- this script uses Kroki HTTP API and can raise output quality via `skinparam dpi`.

Examples:
    python3 Report/render_uml_png.py
    python3 Report/render_uml_png.py --dpi 360 --glob "*.puml"
    python3 Report/render_uml_png.py --input-dir Report/uml --timeout 120
"""

from __future__ import annotations

import argparse
import io
from pathlib import Path
import re
from typing import Iterable

import requests
from PIL import Image


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Render .puml diagrams to PNG with configurable DPI."
    )
    parser.add_argument(
        "--input-dir",
        default="Report/uml",
        help="Directory with .puml files (default: Report/uml)",
    )
    parser.add_argument(
        "--glob",
        default="*.puml",
        help="Glob pattern inside input-dir (default: *.puml)",
    )
    parser.add_argument(
        "--dpi",
        type=int,
        default=300,
        help="Output DPI via PlantUML skinparam (default: 300)",
    )
    parser.add_argument(
        "--timeout",
        type=int,
        default=120,
        help="HTTP timeout in seconds (default: 120)",
    )
    parser.add_argument(
        "--kroki-url",
        default="https://kroki.io/plantuml/png",
        help="Kroki PlantUML PNG endpoint",
    )
    parser.add_argument(
        "--min-dpi",
        type=int,
        default=96,
        help="Lower bound for auto DPI fallback (default: 96)",
    )
    parser.add_argument(
        "--max-dim",
        type=int,
        default=3900,
        help=(
            "Target max PNG dimension during auto-fit. "
            "Keep below backend hard limits (default: 3900)"
        ),
    )
    return parser.parse_args()


def iter_puml_files(input_dir: Path, pattern: str) -> Iterable[Path]:
    return sorted(p for p in input_dir.glob(pattern) if p.is_file())


def inject_or_replace_skinparam(plantuml_text: str, key: str, value: int) -> str:
    """
    Ensure diagram has the requested `skinparam <key>`.
    - If skinparam exists, replace it.
    - Otherwise insert after @startuml line.
    """
    line_value = f"skinparam {key} {value}"
    pattern = rf"(?im)^\s*skinparam\s+{re.escape(key)}\s+\d+\s*$"

    if re.search(pattern, plantuml_text):
        return re.sub(pattern, line_value, plantuml_text)

    lines = plantuml_text.splitlines()
    for i, line in enumerate(lines):
        if line.strip().lower().startswith("@startuml"):
            lines.insert(i + 1, line_value)
            return "\n".join(lines) + ("\n" if plantuml_text.endswith("\n") else "")

    # Fallback: if @startuml is missing, prepend anyway.
    return line_value + "\n" + plantuml_text


def render_one(
    puml_file: Path,
    kroki_url: str,
    dpi: int,
    min_dpi: int,
    max_dim: int,
    timeout: int,
) -> Path:
    source = puml_file.read_text(encoding="utf-8")
    source = inject_or_replace_skinparam(source, "padding", 24)

    current_dpi = dpi
    png_bytes: bytes | None = None

    while True:
        payload = inject_or_replace_skinparam(source, "dpi", current_dpi)
        response = requests.post(
            kroki_url,
            data=payload.encode("utf-8"),
            timeout=timeout,
        )
        response.raise_for_status()
        candidate = response.content

        with Image.open(io.BytesIO(candidate)) as im:
            width, height = im.size

        # Kroki/PlantUML PNG responses may hit hard canvas limits.
        # If we are close to that ceiling, lower DPI and retry.
        if (width > max_dim or height > max_dim) and current_dpi > min_dpi:
            next_dpi = max(min_dpi, int(current_dpi * 0.82))
            if next_dpi == current_dpi:
                next_dpi = current_dpi - 1
            current_dpi = max(min_dpi, next_dpi)
            continue

        png_bytes = candidate
        break

    if png_bytes is None:
        raise RuntimeError(f"Failed to render diagram: {puml_file}")

    out_file = puml_file.with_suffix(".png")
    out_file.write_bytes(png_bytes)
    return out_file


def main() -> int:
    args = parse_args()

    input_dir = Path(args.input_dir).resolve()
    if not input_dir.exists():
        raise FileNotFoundError(f"Input directory not found: {input_dir}")

    files = list(iter_puml_files(input_dir, args.glob))
    if not files:
        print(f"No files matched in {input_dir} with pattern {args.glob}")
        return 0

    print(f"Rendering {len(files)} diagrams from: {input_dir}")
    print(f"DPI: {args.dpi}")

    success = 0
    for puml in files:
        out = render_one(
            puml_file=puml,
            kroki_url=args.kroki_url,
            dpi=args.dpi,
            min_dpi=args.min_dpi,
            max_dim=args.max_dim,
            timeout=args.timeout,
        )
        print(f"created {out}")
        success += 1

    print(f"Done: {success}/{len(files)} PNG files generated.")
    return 0


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