#!/usr/bin/env python3
"""Contact sheet and crop tool for visual QA flow captures.

Two modes:

  Grid mode (default):
    tooling/visual-thumbnail DIR [--config PATH]
    Reads {flow}_{NNN}.png frames + {flow}_manifest.txt sidecar from DIR,
    generates a contact sheet grid with timecodes and labels.
    Output: DIR/{flow}_sheet.png

  Crop mode:
    tooling/visual-thumbnail --crop REGION IMAGE [--config PATH]
    Extracts a named region from IMAGE at 1:1 scale.
    Output: IMAGE_crop_{REGION}.png

Config: reads thumbnail dimensions, columns, and crop regions from
tests/visual.json (auto-detected from script location, or --config).
"""

import argparse
import json
import sys
from pathlib import Path

try:
    from PIL import Image, ImageDraw
except ImportError:
    print("visual-thumbnail requires Pillow: pip install Pillow", file=sys.stderr)
    sys.exit(1)

ROOT = Path(__file__).resolve().parent.parent
DEFAULT_CONFIG = ROOT / "tests" / "visual.json"

# Fallbacks when config keys are missing
DEFAULT_THUMB_WIDTH = 320
DEFAULT_THUMB_HEIGHT = 180
DEFAULT_COLUMNS = 4
LABEL_HEIGHT = 24  # pixels reserved below each thumbnail for text


def load_config(config_path: Path) -> dict:
    """Load configuration from JSON file."""
    if not config_path.exists():
        print(f"Config not found: {config_path}", file=sys.stderr)
        print("Continuing with built-in defaults.", file=sys.stderr)
        return {}
    with open(config_path) as f:
        return json.load(f)


def parse_manifest(manifest_path: Path) -> list[dict]:
    """Parse a flow manifest file.

    Each line: NNN TIMECODE LABEL
    Example:   001 0:03 dialogue opens
    """
    entries = []
    with open(manifest_path) as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            parts = line.split(None, 2)
            if len(parts) < 2:
                continue
            entry = {
                "frame": parts[0],
                "timecode": parts[1],
                "label": parts[2] if len(parts) > 2 else "",
            }
            entries.append(entry)
    return entries


def detect_flow(directory: Path) -> str | None:
    """Detect flow name from manifest sidecar in directory."""
    manifests = list(directory.glob("*_manifest.txt"))
    if len(manifests) == 1:
        # {flow}_manifest.txt -> flow
        stem = manifests[0].stem
        return stem.removesuffix("_manifest")
    if len(manifests) > 1:
        print(f"Multiple manifests found in {directory}:", file=sys.stderr)
        for m in manifests:
            print(f"  {m.name}", file=sys.stderr)
        return None
    return None


def grid_mode(directory: Path, config: dict) -> int:
    """Generate a contact sheet from flow captures."""
    directory = directory.resolve()
    if not directory.is_dir():
        print(f"Not a directory: {directory}", file=sys.stderr)
        return 1

    flow = detect_flow(directory)
    if flow is None:
        print(f"No manifest found in {directory}. Expected {{flow}}_manifest.txt", file=sys.stderr)
        return 1

    manifest_path = directory / f"{flow}_manifest.txt"
    entries = parse_manifest(manifest_path)
    if not entries:
        print(f"Empty manifest: {manifest_path}", file=sys.stderr)
        return 1

    # Read thumbnail config
    thumb_cfg = config.get("thumbnail", {})
    tw = thumb_cfg.get("width", DEFAULT_THUMB_WIDTH)
    th = thumb_cfg.get("height", DEFAULT_THUMB_HEIGHT)
    cols = thumb_cfg.get("columns", DEFAULT_COLUMNS)

    rows = (len(entries) + cols - 1) // cols
    cell_h = th + LABEL_HEIGHT

    sheet_w = tw * cols
    sheet_h = cell_h * rows
    sheet = Image.new("RGB", (sheet_w, sheet_h), color=(30, 30, 30))
    draw = ImageDraw.Draw(sheet)

    for idx, entry in enumerate(entries):
        frame_file = directory / f"{flow}_{entry['frame']}.png"
        if not frame_file.exists():
            print(f"  Missing frame: {frame_file.name}", file=sys.stderr)
            continue

        img = Image.open(frame_file)
        img.thumbnail((tw, th), Image.LANCZOS)

        col = idx % cols
        row = idx // cols
        x = col * tw
        y = row * cell_h

        # Center thumbnail within its cell if it's smaller than tw x th
        offset_x = x + (tw - img.width) // 2
        offset_y = y + (th - img.height) // 2
        sheet.paste(img, (offset_x, offset_y))

        # Draw timecode + label below thumbnail
        text = entry["timecode"]
        if entry["label"]:
            text += f" {entry['label']}"
        text_y = y + th + 2
        draw.text((x + 4, text_y), text, fill=(200, 200, 200))

    output_path = directory / f"{flow}_sheet.png"
    sheet.save(output_path)
    print(f"Sheet: {output_path}")
    return 0


def crop_mode(region_name: str, image_path: Path, config: dict) -> int:
    """Extract a named crop region from an image at 1:1 scale."""
    image_path = image_path.resolve()
    if not image_path.exists():
        print(f"Image not found: {image_path}", file=sys.stderr)
        return 1

    crops = config.get("crops", {})
    if region_name not in crops:
        available = ", ".join(sorted(crops.keys())) if crops else "(none)"
        print(f"Unknown crop region: {region_name}", file=sys.stderr)
        print(f"Available regions: {available}", file=sys.stderr)
        return 1

    coords = crops[region_name]
    if not isinstance(coords, list) or len(coords) != 4:
        print(f"Invalid crop coords for '{region_name}': expected [x, y, w, h]", file=sys.stderr)
        return 1

    x, y, w, h = coords
    img = Image.open(image_path)
    cropped = img.crop((x, y, x + w, y + h))

    stem = image_path.stem
    suffix = image_path.suffix
    output_path = image_path.parent / f"{stem}_crop_{region_name}{suffix}"
    cropped.save(output_path)
    print(output_path)
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Contact sheet and crop tool for visual QA flow captures.",
    )
    parser.add_argument(
        "--config", type=Path, default=DEFAULT_CONFIG,
        help=f"Config JSON path (default: {DEFAULT_CONFIG.relative_to(ROOT)})",
    )

    # Crop mode
    parser.add_argument(
        "--crop", metavar="REGION",
        help="Crop mode: extract named region from IMAGE",
    )

    # Positional: DIR (grid mode) or IMAGE (crop mode)
    parser.add_argument(
        "target", type=Path,
        help="Directory of flow captures (grid mode) or image file (crop mode)",
    )

    args = parser.parse_args()
    config = load_config(args.config)

    if args.crop:
        return crop_mode(args.crop, args.target, config)
    else:
        return grid_mode(args.target, config)


if __name__ == "__main__":
    sys.exit(main())
