#!/usr/bin/env python3
"""Normalize category background images for Poiana Fermecată.

For product-on-white-background images: detects the product bounding box,
scales it to fill ~75 % of the target height, and centres it on a light-grey
canvas — matching the consistent look used across the official site.

For natural/scene photos: falls back to a smart centre-crop at 4:5 ratio.

Target output: 800 × 1000 px, JPEG quality 88.

Usage:
    python3 scripts/normalize_category_images.py          # process public/images/
    python3 scripts/normalize_category_images.py --dry-run
    python3 scripts/normalize_category_images.py --input /tmp --output public/images/
"""

from __future__ import annotations

import argparse
import numpy as np
from pathlib import Path

from PIL import Image, ImageEnhance, ImageFilter

try:
    RESAMPLE = Image.Resampling.LANCZOS
except AttributeError:
    RESAMPLE = Image.LANCZOS  # type: ignore[attr-defined]

TARGET_W, TARGET_H = 800, 1000
PRODUCT_FILL = 0.75          # product occupies this fraction of target height
BG_THRESHOLD = 218           # pixels brighter than this on all channels = background
CORNER_SAMPLE = 60           # sample this many px from each corner to detect bg

CATEGORY_IMAGE_PREFIXES = (
    "cat-dulceata", "cat-sirop", "cat-serbet", "cat-ceai",
    "cat-zacusca",  "cat-gourmet", "cat-atelier", "cat-gusturi",
    "cat-gatim",
)


# ── helpers ──────────────────────────────────────────────────────────────────

def is_white_bg_image(img: Image.Image) -> bool:
    """Return True when the image has uniform bright corners (product-on-white style)."""
    rgb = img.convert("RGB")
    w, h = rgb.size
    s = min(CORNER_SAMPLE, w // 6, h // 6)
    arr = np.array(rgb, dtype=np.float32)
    corners = [arr[:s, :s], arr[:s, w-s:], arr[h-s:, :s], arr[h-s:, w-s:]]
    for patch in corners:
        if patch.mean() < BG_THRESHOLD or patch.std() > 20:
            return False
    return True


def product_bbox(img: Image.Image) -> tuple[int, int, int, int]:
    """Return bounding box (left, top, right, bottom) of the non-background content."""
    rgb = img.convert("RGB")
    arr = np.array(rgb, dtype=np.float32)
    bg_mask = np.all(arr > BG_THRESHOLD, axis=2)
    content_mask = ~bg_mask

    rows = np.any(content_mask, axis=1)
    cols = np.any(content_mask, axis=0)

    if not rows.any():
        w, h = img.size
        return 0, 0, w, h

    top    = int(np.argmax(rows))
    bottom = int(len(rows) - np.argmax(rows[::-1]))
    left   = int(np.argmax(cols))
    right  = int(len(cols) - np.argmax(cols[::-1]))
    return left, top, right, bottom


def normalize_white_bg(img: Image.Image) -> Image.Image:
    """Center the product on a uniform light-grey canvas at TARGET_W × TARGET_H."""
    img = img.convert("RGB")
    left, top, right, bottom = product_bbox(img)

    # Add a small safety margin
    margin = max(8, int((right - left) * 0.04))
    left   = max(0, left   - margin)
    top    = max(0, top    - margin)
    right  = min(img.width,  right  + margin)
    bottom = min(img.height, bottom + margin)

    product = img.crop((left, top, right, bottom))
    pw, ph = product.size

    # Scale so product height = PRODUCT_FILL × TARGET_H
    scale = (TARGET_H * PRODUCT_FILL) / ph
    new_pw = max(1, int(pw * scale))
    new_ph = max(1, int(ph * scale))

    # Never exceed canvas width
    if new_pw > TARGET_W - 20:
        scale2 = (TARGET_W - 20) / new_pw
        new_pw = max(1, int(new_pw * scale2))
        new_ph = max(1, int(new_ph * scale2))

    product = product.resize((new_pw, new_ph), RESAMPLE)

    # Build canvas with a neutral grey matching the WP site style
    canvas_color = (242, 242, 242)
    canvas = Image.new("RGB", (TARGET_W, TARGET_H), canvas_color)

    # Centre the product both horizontally and vertically (slightly above centre)
    paste_x = (TARGET_W - new_pw) // 2
    paste_y = max(0, (TARGET_H - new_ph) // 2 - int(TARGET_H * 0.03))
    canvas.paste(product, (paste_x, paste_y))
    return canvas


def center_crop_scene(img: Image.Image) -> Image.Image:
    """Centre-crop a natural/scene photo to TARGET_W × TARGET_H."""
    img = img.convert("RGB")
    sw, sh = img.size
    tr = TARGET_W / TARGET_H
    sr = sw / sh

    if sr > tr:
        nw = int(sh * tr)
        left = (sw - nw) // 2
        img = img.crop((left, 0, left + nw, sh))
    elif sr < tr:
        nh = int(sw / tr)
        top = (sh - nh) // 2
        img = img.crop((0, top, sw, top + nh))

    return img.resize((TARGET_W, TARGET_H), RESAMPLE)


def enhance(img: Image.Image) -> Image.Image:
    img = ImageEnhance.Contrast(img).enhance(1.06)
    img = ImageEnhance.Sharpness(img).enhance(1.10)
    return img


def process(src: Path, dst: Path, dry_run: bool, quality: int) -> None:
    img = Image.open(src)

    # Flatten transparency
    if img.mode in ("RGBA", "LA", "P"):
        bg = Image.new("RGB", img.size, (242, 242, 242))
        if img.mode == "P":
            img = img.convert("RGBA")
        mask = img.split()[-1] if img.mode in ("RGBA", "LA") else None
        bg.paste(img.convert("RGB"), mask=mask)
        img = bg
    elif img.mode != "RGB":
        img = img.convert("RGB")

    if is_white_bg_image(img):
        result = normalize_white_bg(img)
        mode = "white-bg→centred"
    else:
        result = center_crop_scene(img)
        mode = "scene→crop"

    result = enhance(result)

    if dry_run:
        print(f"[dry-run] {src.name:35s}  mode={mode}  → {dst.name}")
        return

    dst.parent.mkdir(parents=True, exist_ok=True)
    result.save(dst, quality=quality, optimize=True, progressive=True)
    print(f"  ✓  {src.name:35s}  mode={mode}")


def iter_category_images(root: Path):
    for path in sorted(root.iterdir()):
        if not path.is_file():
            continue
        if path.suffix.lower() not in (".jpg", ".jpeg", ".png", ".webp"):
            continue
        if any(path.name.startswith(p) for p in CATEGORY_IMAGE_PREFIXES):
            yield path


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--input",   default="public/images")
    parser.add_argument("--output",  default="public/images")
    parser.add_argument("--quality", type=int, default=88)
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()

    in_dir  = Path(args.input)
    out_dir = Path(args.output)

    images = list(iter_category_images(in_dir))
    if not images:
        print(f"No category images found in {in_dir}")
        return

    print(f"Processing {len(images)} image(s)  →  {TARGET_W}×{TARGET_H}\n")
    for src in images:
        out_name = src.stem + ".jpg"
        dst = out_dir / out_name
        try:
            process(src, dst, args.dry_run, args.quality)
            if not args.dry_run and src.suffix.lower() != ".jpg" and dst != src:
                src.unlink()
                print(f"       removed: {src.name}")
        except Exception as exc:
            print(f"  ✗  {src.name}: {exc}")


if __name__ == "__main__":
    main()

