"""
Stage 08: Frame Extraction

Extract frames from ALL grid iterations and support per-scene refinement
using NanoBanana chat mode.

This stage is architecturally different from others - it operates at the
scene level (9 selections) rather than take level (1 selection).
"""
import json
import sys
from pathlib import Path
from datetime import datetime, timezone
from typing import Optional, List

# Add scripts to path for nanobanana_text2img
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "scripts"))

from lib.frame_extraction import extract_all_frames_from_grids
from lib.path_utils_v3 import ensure_dir
from lib.generation_log import setup_generation_log
from lib.reference_tracking import build_reference_record
from nanobanana_text2img import generate_image, generate_image_chat


def extract_frames(take_path: Path, story_name: str, force: bool = False) -> dict:
    """
    Extract frames from ALL grid iterations (incremental by default).

    On first run: Extracts all grids
    On subsequent runs: Only extracts NEW grids (not in extraction_metadata.json)
    With --force: Re-extracts ALL grids from scratch

    Args:
        take_path: Path to take directory
        story_name: Story name (slug)
        force: Re-extract ALL grids from scratch

    Returns:
        Result dict with status, message
    """
    logger = setup_generation_log(take_path)
    logger.section("Stage 8: Frame Extraction")

    frames_dir = take_path / "08-frames"
    extraction_meta_file = frames_dir / "extraction_metadata.json"

    # Load existing extraction metadata (if any)
    previously_extracted = []
    if extraction_meta_file.exists() and not force:
        with open(extraction_meta_file) as f:
            prev_meta = json.load(f)
            previously_extracted = prev_meta.get("grid_iterations_extracted", [])
        logger.info(f"Previous extraction found: {len(previously_extracted)} grids")
        logger.info("Running INCREMENTAL extraction (only new grids)")
    else:
        if force:
            logger.info("FORCE mode: Re-extracting ALL grids from scratch")
        else:
            logger.info("First extraction: Extracting ALL grids")

    try:
        # Extract frames (incremental if previously extracted)
        result = extract_all_frames_from_grids(take_path, skip_grids=previously_extracted)

        grid_iterations = result["grid_iterations"]  # ALL grids (old + new)
        scene_count = result["scene_count"]
        layout = result["layout"]
        skipped_count = result.get("skipped_count", 0)
        extracted_count = result.get("extracted_count", 0)

        if extracted_count == 0:
            logger.info("No new grids to extract - all grids already processed")
            return {
                "status": "success",
                "grid_count": len(grid_iterations),
                "scene_count": scene_count,
                "total_frames": len(grid_iterations) * scene_count,
                "new_frames": 0,
                "message": "No new grids found"
            }

        logger.info(f"Extracted {extracted_count} NEW grids × {scene_count} scenes")
        logger.info(f"Total grids: {len(grid_iterations)} ({skipped_count} already extracted, {extracted_count} new)")

        # Create extraction metadata
        extraction_metadata = {
            "stage": "08-frames",
            "created_at": datetime.now(timezone.utc).isoformat(),
            "grid_iterations_extracted": grid_iterations,
            "layout": layout,
            "scene_count": scene_count,
            "extraction_method": "PIL_divider_detection",
            "source_grids": {
                grid_iter: f"07-grids/{grid_iter}/grid.jpg"
                for grid_iter in grid_iterations
            },
            "h_boundaries": result["h_boundaries"],
            "v_boundaries": result["v_boundaries"],
            "dividers_detected": True
        }

        with open(extraction_meta_file, 'w') as f:
            json.dump(extraction_metadata, f, indent=2)
        logger.info(f"Created extraction_metadata.json")

        # Create empty pipeline.json for per-scene selections
        pipeline_file = frames_dir / "pipeline.json"
        frame_pipeline = {
            f"{i:02d}-scene": None
            for i in range(1, scene_count + 1)
        }
        with open(pipeline_file, 'w') as f:
            json.dump(frame_pipeline, f, indent=2)
        logger.info(f"Created empty pipeline.json")

        # Update or create metadata.json for each scene
        for scene_num in range(1, scene_count + 1):
            scene_dir = frames_dir / f"{scene_num:02d}-scene"
            meta_file = scene_dir / "metadata.json"

            # Load existing metadata if present
            if meta_file.exists():
                with open(meta_file) as f:
                    scene_meta = json.load(f)
            else:
                scene_meta = {
                    "scene": f"{scene_num:02d}-scene",
                    "scene_number": scene_num,
                    "frames": {},
                    "selected": None,
                    "selected_at": None
                }

            # Add NEW frame entries (don't overwrite existing)
            new_grids = [g for g in grid_iterations if g not in scene_meta["frames"]]
            for grid_iter in new_grids:
                scene_meta["frames"][grid_iter] = {
                    "type": "extracted",
                    "source_grid": f"07-grids/{grid_iter}/grid.jpg",
                    "created_at": datetime.now(timezone.utc).isoformat(),
                    "rating": None
                }

            # Save updated metadata
            with open(meta_file, 'w') as f:
                json.dump(scene_meta, f, indent=2)

        logger.info(f"Frame extraction completed!")
        logger.info(f"Extracted {len(grid_iterations)} grids × {scene_count} scenes")
        logger.info(f"Next: Use 'status-frames' to see available frames")
        logger.info(f"Then: Use 'select-frame SCENE FRAME' to choose frames")

        return {
            "status": "success",
            "grid_count": len(grid_iterations),
            "scene_count": scene_count,
            "total_frames": len(grid_iterations) * scene_count,
            "new_grids": extracted_count,
            "new_frames": extracted_count * scene_count
        }

    except Exception as e:
        logger.error(f"Frame extraction failed: {e}")
        import traceback
        traceback.print_exc()
        return {
            "status": "error",
            "error": str(e)
        }


def refine_scene_frame(
    take_path: Path,
    story_name: str,
    scene: str,
    base_frame: str,
    feedback: str,
    reference_images: Optional[List[str]] = None,
    scale_refs: bool = True
) -> dict:
    """
    Refine a scene frame using NanoBanana chat mode.

    Args:
        take_path: Path to take directory
        story_name: Story name
        scene: Scene name (e.g., "01-scene")
        base_frame: Base frame number (e.g., "001")
        feedback: User feedback for refinement
        reference_images: Optional list of additional reference image paths

    Returns:
        Result dict with status, new frame name
    """
    logger = setup_generation_log(take_path)
    logger.section(f"Stage 8: Refine Frame - {scene}/{base_frame}")

    frames_dir = take_path / "08-frames"
    scene_dir = frames_dir / scene

    if not scene_dir.exists():
        logger.error(f"Scene directory not found: {scene}")
        return {"status": "error", "error": f"Scene not found: {scene}"}

    base_frame_path = scene_dir / f"{base_frame}.jpg"
    if not base_frame_path.exists():
        logger.error(f"Base frame not found: {base_frame}.jpg")
        return {"status": "error", "error": f"Frame not found: {base_frame}.jpg"}

    # Chat history path
    chat_history_path = scene_dir / f"nb-chat-history-{base_frame}.json"
    feedback_log_path = scene_dir / f"feedback-{base_frame}.txt"

    # Check if first refinement or continuation
    is_first_refinement = not chat_history_path.exists()

    # Count existing refinements (any image extension — OpenRouter may return PNG)
    existing_refinements = []
    for item in scene_dir.iterdir():
        if item.name.startswith(f"{base_frame}-refined-") and item.suffix in (".jpg", ".png", ".webp"):
            try:
                iter_num = int(item.stem.split('-')[-1])
                existing_refinements.append(iter_num)
            except ValueError:
                pass

    next_iter = max(existing_refinements) + 1 if existing_refinements else 1
    output_frame_name = f"{base_frame}-refined-{next_iter:03d}"
    output_frame_path = scene_dir / f"{output_frame_name}.jpg"

    logger.info(f"Refining {base_frame}.jpg → {output_frame_name}.jpg")
    logger.info(f"Mode: {'FIRST refinement' if is_first_refinement else f'CONTINUE chat (turn {next_iter + 1})'}")
    logger.info(f"Feedback: {feedback}")

    try:
        # Build reference image paths list
        ref_images = []
        ref_labels = []  # describes each image's role
        ref_roles = []   # for metadata tracking

        # On first refinement, include base frame + project references
        if is_first_refinement:
            ref_images.append(str(base_frame_path))
            ref_labels.append("frame to refine/edit (keep composition, characters and objects)")
            ref_roles.append({"role": "base_frame", "source": "iteration"})

            # Add character sheets, characteristic shot, locations, objects
            scene_num_int = int(scene.split('-')[0])
            project_refs = _collect_scene_references(take_path, scene_num_int)
            for ref in project_refs:
                ref_images.append(ref)
                ref_labels.append(_describe_reference(ref))
                ref_roles.append(_classify_reference(ref))
            if project_refs:
                logger.info(f"Including {len(project_refs)} project reference(s)")
                for ref in project_refs:
                    logger.info(f"  {Path(ref).name}")

        # Add any additional reference images provided
        if reference_images:
            for ref in reference_images:
                ref_images.append(ref)
                ref_labels.append(_describe_reference(ref))
                ref_roles.append(_classify_reference(ref))
            logger.info(f"Including {len(reference_images)} additional reference image(s)")

    # Build prompt: full prompt on first refinement, just user feedback on continuations
        if is_first_refinement:
            prompt = build_refinement_prompt(
                take_path, scene, base_frame, feedback, ref_labels
            )
        else:
            # Chat continuation — NanoBanana already has all context from history.
            # Just send the user's exact feedback.
            prompt = feedback

        # Save prompt
        prompt_file = scene_dir / f"prompt-{output_frame_name}.txt"
        with open(prompt_file, 'w') as f:
            f.write(prompt)

        # Scale down references if requested
        if scale_refs and ref_images:
            from lib.image_utils import scale_down_references
            scaled_dir = scene_dir / "scaled_refs"
            ref_images = scale_down_references(
                ref_images, scaled_dir, max_pixels=2048
            )
            logger.info(f"Scaled references to 2K in {scaled_dir}")

        # Call NanoBanana chat API
        result_path = generate_image_chat(
            prompt=prompt,
            output_path=str(output_frame_path),
            history_path=str(chat_history_path),
            aspect_ratio="9:16",
            image_size="4K",
            reference_image_paths=ref_images if ref_images else None,
            is_refinement=(not is_first_refinement)
        )

        if not result_path or not Path(result_path).exists():
            logger.error("NanoBanana refinement failed")
            return {"status": "error", "error": "Image generation failed"}

        logger.info(f"Created {output_frame_name}.jpg")

        # Save feedback to log
        with open(feedback_log_path, 'a') as f:
            f.write(f"[{next_iter:03d}] {feedback}\n")

        # Update scene metadata
        scene_meta_file = scene_dir / "metadata.json"
        with open(scene_meta_file) as f:
            scene_meta = json.load(f)

        scene_meta["frames"][output_frame_name] = {
            "type": "refined",
            "base_frame": f"{base_frame}.jpg",
            "refined_from": f"{base_frame}-refined-{next_iter - 1:03d}" if next_iter > 1 else None,
            "chat_turn": next_iter + 1,  # +1 because first turn is base frame generation
            "feedback": feedback,
            "created_at": datetime.now(timezone.utc).isoformat(),
            "chat_history": f"nb-chat-history-{base_frame}.json",
            "model": "nanobanana-gemini3pro",
            "reference_images_sent": build_reference_record(
                ref_images, take_path.parent, ref_roles
            ),
            "rating": None
        }

        with open(scene_meta_file, 'w') as f:
            json.dump(scene_meta, f, indent=2)

        logger.info("Updated scene metadata")
        logger.info(f"Next: Review {output_frame_name}.jpg")
        logger.info(f"Then: select-frame {scene} {output_frame_name} (if satisfied)")

        return {
            "status": "success",
            "frame": output_frame_name,
            "chat_turn": next_iter + 1
        }

    except Exception as e:
        logger.error(f"Refinement failed: {e}")
        import traceback
        traceback.print_exc()
        return {"status": "error", "error": str(e)}


def build_refinement_prompt(
    take_path: Path,
    scene: str,
    base_frame: str,
    feedback: str,
    ref_labels: List[str] = None
) -> str:
    """Build comprehensive refinement prompt for NanoBanana."""
    # Parse scene number
    scene_num = int(scene.split('-')[0])

    # Read script for style info
    script_data = None
    pipeline_file = take_path / "pipeline.json"
    if pipeline_file.exists():
        with open(pipeline_file) as f:
            pipeline = json.load(f)
        # Try adapted script first, fall back to original script
        for stage_key in ("09-script-adapted", "03-script"):
            script_iter = pipeline.get(stage_key)
            if script_iter:
                script_file = take_path / stage_key / script_iter / "script.json"
                if script_file.exists():
                    with open(script_file) as f:
                        script_data = json.load(f)
                    break

    # Extract visual style from script
    style_desc = ""
    if script_data:
        style = script_data.get("style", {})
        if isinstance(style, dict):
            visual = style.get("visual", "")
            reference = style.get("reference", "")
            if visual and reference:
                style_desc = f"{visual} — {reference}"
            elif visual:
                style_desc = visual

    # Build [Analyze...] prefix describing each reference image's role
    prompt_lines = []
    if ref_labels:
        image_desc = ". ".join(
            f"Image {i}: {label}" for i, label in enumerate(ref_labels, 1)
        )
        prompt_lines.extend([
            f"[REFERENCE IMAGES: {image_desc}. "
            f"Image 1 is the BASE FRAME to edit and improve — keep its composition, "
            f"camera angle, characters, and objects. All other images are STYLE REFERENCES "
            f"ONLY — use them to match character faces, clothing, objects, and locations "
            f"but do NOT copy their composition. Output must be tack-sharp with crisp focus.]",
            f"",
        ])

    if style_desc:
        prompt_lines.extend([
            f"High-quality frame in 9:16 vertical format (1080x1920 resolution).",
            f"Art style: {style_desc}.",
            f"",
            f"This is Scene {scene_num}.",
            f"",
        ])
    else:
        prompt_lines.extend([
            f"High-quality cinematic frame in 9:16 vertical format (1080x1920 resolution).",
            f"",
            f"This is Scene {scene_num}.",
            f"",
        ])

    # NOTE: We intentionally do NOT include the adapted script's scene description here.
    # The adapted script (stage 09) describes the SELECTED GRID's frames, but the user
    # may be refining frames from DIFFERENT grid iterations. Including a description of
    # a different composition confuses NanoBanana. The user's feedback is the instruction.

    # Add user feedback
    prompt_lines.extend([
        f"USER REFINEMENT REQUEST:",
        f"{feedback}",
        f"",
        f"CRITICAL - NO TEXT/CAPTIONS:",
        f"- DO NOT add any text, captions, subtitles, or speech bubbles to the image",
        f"- DO NOT render spoken words as text overlays",
        f"- This is a pure visual frame - dialogue will be added separately in video editing",
        f"- If scene mentions speech, show only the CHARACTER SPEAKING, not the words",
        f"",
        f"TECHNICAL REQUIREMENTS:",
        f"- Maintain 1080x1920 resolution (9:16 aspect ratio)",
        f"- High sharpness and detail",
        f"- Remove any divider artifacts or borders",
        f"- {'Keep the ' + style_desc.split(' — ')[0] + ' art style consistently' if style_desc else 'Photorealistic quality, cinematic finish'}",
        f"",
        f"NEGATIVE PROMPT: low resolution, blurry, white borders, black borders, divider lines, grid lines, distorted, text overlay, captions, subtitles, speech bubbles, words, letters, typography",
    ])

    return "\n".join(prompt_lines)


def select_scene_frame(take_path: Path, scene: str, frame: str) -> dict:
    """
    Select a frame for a scene.

    Args:
        take_path: Path to take directory
        scene: Scene name (e.g., "01-scene")
        frame: Frame name (e.g., "001" or "001-refined-002")

    Returns:
        Result dict with status
    """
    logger = setup_generation_log(take_path)
    logger.section(f"Stage 8: Select Frame - {scene}")

    frames_dir = take_path / "08-frames"
    scene_dir = frames_dir / scene

    if not scene_dir.exists():
        logger.error(f"Scene directory not found: {scene}")
        return {"status": "error", "error": f"Scene not found: {scene}"}

    # Validate frame exists
    frame_path = scene_dir / f"{frame}.jpg"
    if not frame_path.exists():
        logger.error(f"Frame not found: {frame}.jpg")
        return {"status": "error", "error": f"Frame not found: {frame}.jpg"}

    # Update main pipeline with frame selection
    from lib.pipeline import load_pipeline, save_pipeline

    pipeline = load_pipeline(take_path)

    # Initialize 08-frames as dict if not already
    if not isinstance(pipeline.get("08-frames"), dict):
        pipeline["08-frames"] = {}

    pipeline["08-frames"][scene] = frame

    save_pipeline(take_path, pipeline)

    # Update scene metadata
    scene_meta_file = scene_dir / "metadata.json"
    with open(scene_meta_file) as f:
        scene_meta = json.load(f)

    scene_meta["selected"] = frame
    scene_meta["selected_at"] = datetime.now(timezone.utc).isoformat()

    with open(scene_meta_file, 'w') as f:
        json.dump(scene_meta, f, indent=2)

    logger.info(f"Selected {frame}.jpg for {scene}")
    logger.info("Use 'status-frames' to see overall progress")

    return {"status": "success", "scene": scene, "frame": frame}


def rate_scene_frame(take_path: Path, scene: str, frame: str, rating: int) -> dict:
    """
    Rate a scene frame (1-5).

    Args:
        take_path: Path to take directory
        scene: Scene name
        frame: Frame name
        rating: Rating (1-5)

    Returns:
        Result dict with status
    """
    if rating < 1 or rating > 5:
        return {"status": "error", "error": "Rating must be 1-5"}

    frames_dir = take_path / "08-frames"
    scene_dir = frames_dir / scene
    scene_meta_file = scene_dir / "metadata.json"

    if not scene_meta_file.exists():
        return {"status": "error", "error": f"Scene not found: {scene}"}

    with open(scene_meta_file) as f:
        scene_meta = json.load(f)

    if frame not in scene_meta["frames"]:
        return {"status": "error", "error": f"Frame not found: {frame}"}

    scene_meta["frames"][frame]["rating"] = rating

    with open(scene_meta_file, 'w') as f:
        json.dump(scene_meta, f, indent=2)

    return {"status": "success", "scene": scene, "frame": frame, "rating": rating}


def get_frames_status(take_path: Path) -> dict:
    """
    Get status of all scenes and frame selections.

    Returns:
        {
            "status": "success",
            "scenes": [
                {
                    "scene": "01-scene",
                    "selected": "001-refined-002",
                    "available_base": ["001", "002", "057"],
                    "available_refined": ["001-refined-001", "001-refined-002"]
                },
                ...
            ]
        }
    """
    frames_dir = take_path / "08-frames"

    if not frames_dir.exists():
        return {"status": "error", "error": "Frames not extracted. Run: extract-frames"}

    extraction_meta_file = frames_dir / "extraction_metadata.json"
    if not extraction_meta_file.exists():
        return {"status": "error", "error": "Extraction metadata not found"}

    with open(extraction_meta_file) as f:
        extraction_meta = json.load(f)

    scene_count = extraction_meta["scene_count"]
    grid_iterations = extraction_meta["grid_iterations_extracted"]

    # Load frame selections from main pipeline
    from lib.pipeline import load_pipeline

    pipeline = load_pipeline(take_path)
    frame_selections = pipeline.get("08-frames")

    # If not dict, no selections yet
    if not isinstance(frame_selections, dict):
        frame_selections = {}

    # Collect status for each scene
    scenes_status = []
    for scene_num in range(1, scene_count + 1):
        scene_name = f"{scene_num:02d}-scene"
        scene_dir = frames_dir / scene_name

        # Get available frames
        available_base = []
        available_refined = []

        for item in scene_dir.iterdir():
            if item.suffix == ".jpg":
                name = item.stem
                if "-refined-" in name:
                    available_refined.append(name)
                elif name.isdigit() and len(name) == 3:
                    available_base.append(name)

        available_base.sort()
        available_refined.sort()

        scenes_status.append({
            "scene": scene_name,
            "selected": frame_selections.get(scene_name),
            "available_base": available_base,
            "available_refined": available_refined
        })

    return {
        "status": "success",
        "scenes": scenes_status,
        "grid_count": len(grid_iterations),
        "scene_count": scene_count,
    }




def _classify_reference(ref_path: str) -> dict:
    """Classify a reference image path into role/asset_id/source for tracking."""
    p = Path(ref_path)
    name = p.stem
    parent = p.parent.name

    if "characteristic_shot" in name:
        return {"role": "characteristic_shot", "source": "iteration"}
    elif parent == "people" or p.parent.parent.name == "people":
        char_id = parent if p.parent.parent.name == "people" else name
        return {"role": "character_ref", "asset_id": char_id, "source": "reference"}
    elif parent == "locations" or p.parent.parent.name == "locations":
        loc_id = parent if p.parent.parent.name == "locations" else name
        return {"role": "location_ref", "asset_id": loc_id, "source": "reference"}
    elif parent == "objects" or p.parent.parent.name == "objects":
        obj_id = parent if p.parent.parent.name == "objects" else name
        return {"role": "object_ref", "asset_id": obj_id, "source": "reference"}
    else:
        return {"role": "unknown", "source": "unknown"}


def _describe_reference(ref_path: str) -> str:
    """Generate a human-readable label for a reference image path."""
    p = Path(ref_path)
    name = p.stem
    parent = p.parent.name

    if "characteristic_shot" in name:
        return "characteristic shot (overall visual style reference)"
    elif parent == "people" or p.parent.parent.name == "people":
        char_id = parent if p.parent.parent.name == "people" else name
        char_label = char_id.replace("_", " ").title()
        if "sheet" in name:
            return f"{char_label} character sheet (face/body/clothing reference)"
        elif "portrait" in name:
            return f"{char_label} portrait (face reference)"
        return f"{char_label} reference"
    elif parent == "locations" or p.parent.parent.name == "locations":
        loc_id = parent if p.parent.parent.name == "locations" else name
        loc_label = loc_id.replace("_", " ").title()
        return f"{loc_label} location reference"
    elif parent == "objects" or p.parent.parent.name == "objects":
        obj_id = parent if p.parent.parent.name == "objects" else name
        obj_label = obj_id.replace("_", " ").title()
        return f"{obj_label} object reference"
    else:
        return f"{name} reference"


def _collect_scene_references(take_path: Path, scene_num: int = None) -> List[str]:
    """Collect project reference images for scene frame refinement.

    Scene-aware: uses assets.json to find which characters, locations,
    and objects appear in this specific scene.

    Includes: character sheets + location refs + object refs (no characteristic shot).
    """
    refs = []
    project_path = take_path.parent

    # NOTE: We intentionally do NOT include the characteristic shot here.
    # It's a 9.6MB composite image that dominates smaller base frames (~200KB)
    # and NanoBanana tends to copy its composition. The individual character sheets,
    # location refs, and object refs provide the same style information without
    # the composition bias.

    # Load assets to find scene-specific references
    from lib.pipeline import load_pipeline
    pipeline = load_pipeline(take_path)
    scene_chars = None
    scene_locations = None
    scene_objects = None
    assets_iter = pipeline.get("04-assets")
    if assets_iter and scene_num:
        assets_file = take_path / "04-assets" / assets_iter / "assets.json"
        if assets_file.exists():
            assets = json.loads(assets_file.read_text())
            scene_chars, scene_locations, scene_objects = \
                _get_scene_assets(assets, scene_num)

    refs_path = project_path / "references"

    # Character reference sheets
    people_path = refs_path / "people"
    if people_path.exists():
        for char_dir in sorted(people_path.iterdir()):
            if not char_dir.is_dir():
                continue
            # If we know scene characters, only include those in this scene
            if scene_chars is not None and char_dir.name not in scene_chars:
                continue
            for img_name in ["sheet.jpg", "portrait.jpg"]:
                img_path = char_dir / img_name
                if img_path.exists():
                    refs.append(str(img_path))
                    break

    # Location references
    locations_path = refs_path / "locations"
    if locations_path.exists():
        for loc_item in sorted(locations_path.iterdir()):
            if loc_item.is_dir():
                if scene_locations is not None and loc_item.name not in scene_locations:
                    continue
                for img_name in ["photo.jpg", "reference.jpg"]:
                    img_path = loc_item / img_name
                    if img_path.exists():
                        refs.append(str(img_path))
                        break
            elif loc_item.suffix in (".jpg", ".png"):
                loc_id = loc_item.stem
                if scene_locations is not None and loc_id not in scene_locations:
                    continue
                refs.append(str(loc_item))

    # Object references
    objects_path = refs_path / "objects"
    if objects_path.exists():
        for obj_item in sorted(objects_path.iterdir()):
            if obj_item.is_dir():
                if scene_objects is not None and obj_item.name not in scene_objects:
                    continue
                for img_name in ["photo.jpg", "reference.jpg"]:
                    img_path = obj_item / img_name
                    if img_path.exists():
                        refs.append(str(img_path))
                        break
            elif obj_item.suffix in (".jpg", ".png"):
                obj_id = obj_item.stem
                if scene_objects is not None and obj_id not in scene_objects:
                    continue
                refs.append(str(obj_item))

    return refs


def _get_scene_assets(assets: dict, scene_num: int) -> tuple:
    """Extract character/location/object IDs that appear in a specific scene.

    Returns (char_ids, location_ids, object_ids) — each a set or None if not found.
    """
    chars = set()
    locations = set()
    objects = set()

    # Characters: check appears_in
    for char in assets.get("characters", []):
        appears_in = char.get("appears_in", [])
        if scene_num in appears_in:
            char_id = char.get("id", "")
            if char_id:
                chars.add(char_id)

    # Locations: check appears_in
    for loc in assets.get("locations", []):
        appears_in = loc.get("appears_in", [])
        if scene_num in appears_in:
            loc_id = loc.get("id", "")
            if loc_id:
                locations.add(loc_id)

    # Objects: check appears_in
    for obj in assets.get("objects", []):
        appears_in = obj.get("appears_in", [])
        if scene_num in appears_in:
            obj_id = obj.get("id", "")
            if obj_id:
                objects.add(obj_id)

    # Return empty sets (not None) — None means "unknown, include all",
    # empty set means "known, none in this scene"
    return (chars, locations, objects)


