"""
ref_image_ops.py — PIL-based operations on character ref images.

Extracted from the former `recoil/pipeline/_lib/ref_resolver.py` shim on
2026-05-24 as part of the ref_resolution capability convergence (SSOT
manifest). The shim was tombstoned because resolution logic itself lives
canonically at `recoil/core/ref_resolver.py`. These four functions remained
in the shim because they're UI-specific PIL operations with no canonical
home; ref_image_ops gives them one.

Under the v3 layout, character assets live at ``assets/char/<slug>/``
(routed through ``ProjectPaths.asset_subject_dir``). Callers that still pass
a v1 ``refs_root`` (i.e. an old ``output/refs/`` path) are honored as a
legacy shim — paths are derived against the parent project root.

Public surface:
- character_ref_status(refs_root, char_id) -> dict — used by Console API
  to report a character's ref status (locked/exploring/missing) plus
  hero/turnaround/wardrobe metadata + dimensions.
- split_grid_image(grid_path, panel_count, divider_threshold) -> list[(role, PIL.Image)]
- promote_grid(refs_root, char_id, grid_path, panel_count) -> dict[role -> Path]
- generate_thumbnail(source_path, thumb_dir, size) -> Path

These all depend on PIL (and split_grid_image on numpy). PIL/numpy are
imported lazily inside the functions to keep import-time cost low for
callers that only need character_ref_status's metadata branch.
"""

import sys
from pathlib import Path

# Defensive sys.path setup — pipeline tools may run with pipeline/ as cwd,
# but core.ref_resolver lives at recoil/core/. Mirror the pattern the
# former ref_resolver.py shim used.
_RECOIL_ROOT = Path(__file__).resolve().parent.parent.parent
if str(_RECOIL_ROOT) not in sys.path:
    sys.path.insert(0, str(_RECOIL_ROOT))

from recoil.core.paths import ProjectPaths  # noqa: E402
from recoil.core.ref_resolver import (  # noqa: E402
    IMAGE_EXTS,
    TURNAROUND_ANGLES,
    slugify_asset_id,
    resolve_character_refs,
)


PANEL_ROLE_MAP_3 = ("front", "profile", "back")
PANEL_ROLE_MAP_4 = ("front", "profile", "back", "three_quarter")


def character_ref_status(refs_root: Path, char_id: str) -> dict:
    """Return ref status for a character (used by console API).

    The ``refs_root`` argument is the legacy ``output/refs/`` directory.
    The project root is recovered as ``refs_root.parent.parent`` (i.e.
    drop ``output/refs``) so we can route through ``ProjectPaths`` for
    the v3 ``assets/char/<slug>/`` location.
    """
    char_slug = slugify_asset_id(char_id)
    # Recover project root from the legacy refs_root (output/refs).
    project_root = refs_root.parent.parent
    paths = ProjectPaths.from_root(project_root)
    char_dir = paths.asset_subject_dir("char", char_slug)
    if not char_dir.is_dir():
        return {
            "status": "missing",
            "hero": None,
            "turnaround": {},
            "exploration_count": 0,
        }

    refs = resolve_character_refs(paths, char_id)
    exploration_dir = char_dir / "_exploration" if char_dir.is_dir() else None
    exploration_count = (
        len(list(exploration_dir.glob("*")))
        if exploration_dir and exploration_dir.is_dir()
        else 0
    )

    from recoil.core.ref_resolver import get_dimensions

    def _ref_info(path):
        if path is None or not path.is_file():
            return None
        dims = get_dimensions(path)
        return {"path": str(path), "dimensions": list(dims) if dims else None}

    hero_info = _ref_info(refs.get("hero"))
    turnaround = {angle: _ref_info(refs.get(angle)) for angle in TURNAROUND_ANGLES}

    wardrobe_turnaround = {}
    if char_dir.is_dir():
        for angle in TURNAROUND_ANGLES:
            for ext in IMAGE_EXTS:
                wp = char_dir / f"{char_slug}_wardrobe_{angle}{ext}"
                if wp.is_file():
                    wardrobe_turnaround[angle] = _ref_info(wp)
                    break

    if hero_info:
        status = "locked"
    elif exploration_count > 0:
        status = "exploring"
    else:
        status = "missing"

    return {
        "status": status,
        "hero": hero_info,
        "turnaround": turnaround,
        "wardrobe_turnaround": wardrobe_turnaround,
        "exploration_count": exploration_count,
    }


def split_grid_image(grid_path, panel_count=4, divider_threshold=30):
    """Split a grid image into individual panels by detecting black dividers."""
    from PIL import Image
    import numpy as np

    img = Image.open(grid_path)
    w, h = img.size
    arr = np.array(img)

    col_means = arr.mean(axis=(0, 2))
    dark_cols = np.where(col_means < divider_threshold)[0]

    panels_bounds = []
    if len(dark_cols) > 0:
        gaps = np.diff(dark_cols)
        splits = np.where(gaps > 5)[0]
        dividers = []
        start = int(dark_cols[0])
        for s in splits:
            end = int(dark_cols[s])
            dividers.append((start, end))
            start = int(dark_cols[s + 1])
        dividers.append((start, int(dark_cols[-1])))

        prev_end = 0
        for div_start, div_end in dividers:
            if div_start > prev_end + 10:
                panels_bounds.append((prev_end, div_start))
            prev_end = div_end + 1
        if prev_end < w - 10:
            panels_bounds.append((prev_end, w))
    else:
        panel_w = w // panel_count
        panels_bounds = [(i * panel_w, (i + 1) * panel_w) for i in range(panel_count)]

    detected = len(panels_bounds)
    if detected != panel_count:
        raise ValueError(f"Expected {panel_count} panels but detected {detected}.")

    role_map = PANEL_ROLE_MAP_4 if panel_count == 4 else PANEL_ROLE_MAP_3
    if panel_count not in (3, 4):
        role_map = tuple(f"panel_{i}" for i in range(panel_count))

    result = []
    for i, (left, right) in enumerate(panels_bounds):
        panel = img.crop((left, 0, right, h))
        role = role_map[i] if i < len(role_map) else f"panel_{i}"
        result.append((role, panel))

    return result


def promote_grid(refs_root, char_id, grid_path, panel_count=4):
    """Split a grid image and copy panels to canonical ref names.

    Under the v3 layout, both ``legacy_dest`` (the slug-prefixed
    backwards-compat name) and ``canonical_out`` (the
    ``assets/char/<slug>/<role>.png`` canonical name) land in the
    same v3 asset_subject_dir. The two filenames coexist there; ref_resolver
    prefers the canonical-named file.
    """
    from PIL import Image

    char_slug = slugify_asset_id(char_id)
    # Recover project root from the legacy refs_root (output/refs).
    project_root = Path(refs_root).parent.parent
    paths = ProjectPaths.from_root(project_root)
    char_dir = paths.asset_subject_dir("char", char_slug)
    char_dir.mkdir(parents=True, exist_ok=True)
    thumbs_dir = char_dir / "_thumbs"
    thumbs_dir.mkdir(exist_ok=True)

    panels = split_grid_image(grid_path, panel_count=panel_count)
    written = {}

    for role, panel_img in panels:
        legacy_dest = char_dir / f"{char_slug}_{role}.png"
        panel_img.save(legacy_dest, "PNG")

        thumb = panel_img.copy()
        thumb.thumbnail((256, 256), Image.LANCZOS)
        thumb.save(thumbs_dir / f"{char_slug}_{role}_thumb.jpg", "JPEG", quality=85)

        canonical_out = char_dir / f"{role}.png"
        canonical_out.parent.mkdir(parents=True, exist_ok=True)
        for old_file in canonical_out.parent.glob(f"{role}.*"):
            old_file.unlink()
        panel_img.save(canonical_out, "PNG")

        written[role] = legacy_dest

    return written


def generate_thumbnail(source_path, thumb_dir=None, size=256):
    """Generate a JPEG thumbnail for a canonical ref image."""
    from PIL import Image

    source_path = Path(source_path)
    if thumb_dir is None:
        thumb_dir = source_path.parent / "_thumbs"
    thumb_dir = Path(thumb_dir)
    thumb_dir.mkdir(exist_ok=True)

    thumb_name = f"{source_path.stem}_thumb.jpg"
    thumb_path = thumb_dir / thumb_name

    with Image.open(source_path) as img:
        img.thumbnail((size, size), Image.LANCZOS)
        img.save(thumb_path, "JPEG", quality=85)

    return thumb_path
