"""Command-line interface for video-thumbnail-creator."""

import argparse
import json
import os
import shutil
import sys
import tempfile
from typing import List, Optional, Tuple

from .ai_selector import DEFAULT_CLAUDE_MODEL, select_frame_with_ai
from .config import get_effective_config, get_value, list_config, set_value
from .crop_selector import CROP_POSITIONS, apply_crop_position, select_crop_with_ai
from .embedded_image import detect_embedded_image, extract_embedded_image
from .sidecar_image import detect_sidecar_image
from .extractor import extract_frames, extract_single_frame_highres
from .fanart_composer import FANART_SUFFIX, FANART_WIDTH_4K, FANART_HEIGHT_4K, compose_fanart
from .metadata import build_metadata, embed_metadata
from .mosaic import create_mosaic
from .poster_composer import compose_landscape_with_text, compose_poster
from .poster_template import load_template
from .image_converter import is_image_file, prepare_image_source
from .utils import check_dependencies, get_video_properties, open_image
from .badges import detect_badges

# Exit codes
EXIT_SUCCESS = 0
EXIT_ERROR = 1
EXIT_NO_SELECTION = 2
EXIT_AI_FAILED = 3


def _resolve_output_path(
    input_path: str,
    output_dir: str | None,
    output_name_suffix: str,
) -> str:
    """Resolve the final output path for the poster image."""
    base_dir = output_dir or os.path.dirname(os.path.abspath(input_path))
    os.makedirs(base_dir, exist_ok=True)
    input_stem = os.path.splitext(os.path.basename(input_path))[0]
    return os.path.join(base_dir, f"{input_stem}{output_name_suffix}.jpg")


def _emit_result(
    poster_path: str,
    frame_index: int,
    mode: str,
    reasoning: str,
    input_path: str,
    use_json: bool,
    fmt: str = "poster",
    crop_position: str = "",
    overlay_title: str = "",
    source: str = "frame",
    overlay_category: str = "",
    overlay_note: str = "",
    badges: Optional[List[str]] = None,
    fanart_path: Optional[str] = None,
) -> None:
    """Write result to stdout."""
    if use_json:
        payload: dict = {
            "poster_path": os.path.abspath(poster_path),
            "frame_index": frame_index,
            "mode": mode,
            "format": fmt,
            "source": source,
            "reasoning": reasoning,
            "input_path": os.path.abspath(input_path),
        }
        if crop_position:
            payload["crop_position"] = crop_position
        if overlay_title:
            payload["overlay_title"] = overlay_title
        if overlay_category:
            payload["overlay_category"] = overlay_category
        if overlay_note:
            payload["overlay_note"] = overlay_note
        if badges:
            payload["badges"] = badges
        if fanart_path:
            payload["fanart_path"] = os.path.abspath(fanart_path)
        print(json.dumps(payload, ensure_ascii=False))
    else:
        print(os.path.abspath(poster_path))


def _resolve_overlay_title(args: argparse.Namespace) -> str:
    """Return the overlay title to use, or an empty string if none."""
    if getattr(args, "overlay_title", None):
        return args.overlay_title
    if getattr(args, "overlay_title_from_filename", False):
        return os.path.splitext(os.path.basename(args.input_path))[0]
    return ""


def _resolve_category(args: argparse.Namespace) -> Tuple[Optional[str], Optional[str]]:
    """Return (overlay_category, overlay_category_logo_path) based on CLI args.

    If both ``--overlay-category`` and ``--overlay-category-logo`` are provided,
    the logo takes precedence and a warning is emitted to stderr.
    """
    category: Optional[str] = getattr(args, "overlay_category", None)
    category_logo: Optional[str] = getattr(args, "overlay_category_logo", None)

    if category and category_logo:
        print(
            "Warning: both --overlay-category and --overlay-category-logo provided; logo takes precedence.",
            file=sys.stderr,
        )
        return None, category_logo

    return category or None, category_logo or None


def _ask_crop_position_manual(highres_path: str) -> str:
    """Show the frame and prompt the user for a crop position."""
    open_image(highres_path)
    positions_str = " / ".join(CROP_POSITIONS)
    try:
        raw = input(f"\n   Crop-Position wählen [{positions_str}]: ").strip().lower()
    except (EOFError, KeyboardInterrupt):
        return "center"
    return raw if raw in CROP_POSITIONS else "center"


def _apply_postprocess(
    highres_path: str,
    poster_path: str,
    fmt: str,
    crop_position: str,
    overlay_title: str,
    overlay_category: Optional[str] = None,
    overlay_category_logo_path: Optional[str] = None,
    overlay_note: Optional[str] = None,
    template: Optional[dict] = None,
    badges: Optional[List[str]] = None,
) -> None:
    """Apply format-specific post-processing to a high-res frame."""
    if fmt == "poster":
        compose_poster(
            highres_path, crop_position or "center", overlay_title or None, poster_path,
            overlay_category=overlay_category,
            overlay_category_logo_path=overlay_category_logo_path,
            overlay_note=overlay_note,
            template=template,
            badges=badges,
        )
    elif overlay_title:
        compose_landscape_with_text(highres_path, overlay_title, poster_path, template=template)
    else:
        shutil.copy2(highres_path, poster_path)


def _generate_fanart_if_requested(
    args: argparse.Namespace,
    highres_path: str,
    video_properties: Optional[dict] = None,
) -> Optional[str]:
    """Generate a fanart image if ``--fanart`` was requested.

    Parameters
    ----------
    args:
        Parsed CLI arguments.
    highres_path:
        Path to the already-converted sRGB JPEG source frame.
    video_properties:
        Video properties dict (from :func:`get_video_properties`), or ``None``
        for image input.  When ``None`` the source image dimensions are used to
        determine 4K vs HD.

    Returns
    -------
    The absolute path to the created fanart file, or ``None`` when fanart was
    not requested or failed.
    """
    if not getattr(args, "fanart", False):
        return None

    if video_properties is not None:
        is_4k = bool(video_properties.get("is_4k", False))
    else:
        # Image input: check source dimensions directly
        try:
            from PIL import Image as _Image
            with _Image.open(highres_path) as _img:
                _w, _h = _img.size
            is_4k = (_w >= FANART_WIDTH_4K or _h >= FANART_HEIGHT_4K)
        except Exception:
            is_4k = False

    fanart_path = _resolve_output_path(args.input_path, args.output_dir, FANART_SUFFIX)
    try:
        compose_fanart(highres_path, fanart_path, is_4k=is_4k)
        print(f"   🖼️  Fanart-Bild erstellt: {fanart_path}", file=sys.stderr)
    except Exception as exc:
        print(f"   Error creating fanart: {exc}", file=sys.stderr)
        return None

    return fanart_path


def _run_manual(args: argparse.Namespace, frame_paths: list, mosaic_path: str, tmpdir: str) -> int:
    print("   🖼️  Mosaikbild geöffnet in Vorschau.", file=sys.stderr)
    open_image(mosaic_path)

    try:
        raw = input("\n   Wähle Frame 0–19 oder [ENTER] zum Abbrechen: ").strip()
    except (EOFError, KeyboardInterrupt):
        print("\n   Abgebrochen.", file=sys.stderr)
        return EXIT_NO_SELECTION

    if raw == "":
        print("   Keine Auswahl getroffen.", file=sys.stderr)
        return EXIT_NO_SELECTION

    try:
        frame_index = int(raw)
        if not (0 <= frame_index <= 19):
            raise ValueError
    except ValueError:
        print(f"   Ungültige Eingabe: '{raw}'. Bitte Zahl 0–19 eingeben.", file=sys.stderr)
        return EXIT_ERROR

    highres_path = os.path.join(tmpdir, "highres.jpg")
    try:
        extract_single_frame_highres(
            args.input_path, frame_index, highres_path,
            ffmpeg=args.ffmpeg_bin, ffprobe=args.ffprobe_bin
        )
    except Exception as exc:
        print(f"   Error extracting high-res frame: {exc}", file=sys.stderr)
        return EXIT_ERROR

    crop_position = ""
    if args.format == "poster":
        cli_crop = getattr(args, "crop_position", None)
        if cli_crop:
            crop_position = cli_crop
            print(f"   ✅ Crop-Position (vorgegeben): {crop_position}", file=sys.stderr)
        else:
            crop_position = _ask_crop_position_manual(highres_path)

    overlay_title = _resolve_overlay_title(args)
    category, category_logo_path = _resolve_category(args)
    note: Optional[str] = getattr(args, "overlay_note", None)
    detected_badges: List[str] = getattr(args, "detected_badges", [])
    poster_path = _resolve_output_path(args.input_path, args.output_dir, args.output_name_suffix)
    try:
        _apply_postprocess(highres_path, poster_path, args.format, crop_position, overlay_title,
                           overlay_category=category, overlay_category_logo_path=category_logo_path,
                           overlay_note=note, template=getattr(args, "template", None),
                           badges=detected_badges)
    except Exception as exc:
        print(f"   Error composing output image: {exc}", file=sys.stderr)
        return EXIT_ERROR

    metadata = build_metadata(
        source="frame", frame_index=frame_index, crop_position=crop_position,
        fmt=args.format, mode="manual", input_file=args.input_path,
        overlay_title=overlay_title, overlay_category=category or "",
        overlay_category_logo=category_logo_path or "", overlay_note=note or "",
        poster_template=getattr(args, "poster_template", "") or "",
    )
    embed_metadata(poster_path, metadata)

    fanart_path = _generate_fanart_if_requested(
        args, highres_path, getattr(args, "video_properties", None)
    )
    _emit_result(
        poster_path, frame_index, "manual", "", args.input_path, args.json,
        fmt=args.format, crop_position=crop_position, overlay_title=overlay_title,
        source="frame", overlay_category=category or "", overlay_note=note or "",
        badges=detected_badges or None, fanart_path=fanart_path,
    )
    return EXIT_SUCCESS


def _run_auto(args: argparse.Namespace, frame_paths: list, mosaic_path: str, tmpdir: str) -> int:
    api_key = args.claude_api_key
    if not api_key:
        print(
            "   Error: Claude API key required for auto mode. "
            "Set the CLAUDE_API_KEY env variable "
            "or run: video-thumbnail-creator config set claude.api_key <key>",
            file=sys.stderr,
        )
        return EXIT_AI_FAILED

    print("   🤖 K.I. analysiert Frames…", file=sys.stderr)
    try:
        frame_index, reasoning = select_frame_with_ai(
            mosaic_path,
            api_key=api_key,
            model=args.claude_model,
            description=args.description,
        )
    except RuntimeError as exc:
        print(f"   Error: {exc}", file=sys.stderr)
        return EXIT_AI_FAILED

    print(
        f'   ✅ K.I. Auswahl: Frame {frame_index} – "{reasoning}"',
        file=sys.stderr,
    )

    highres_path = os.path.join(tmpdir, "highres.jpg")
    try:
        extract_single_frame_highres(
            args.input_path, frame_index, highres_path,
            ffmpeg=args.ffmpeg_bin, ffprobe=args.ffprobe_bin
        )
    except Exception as exc:
        print(f"   Error extracting high-res frame: {exc}", file=sys.stderr)
        return EXIT_ERROR

    crop_position = ""
    if args.format == "poster":
        cli_crop = getattr(args, "crop_position", None)
        if cli_crop:
            crop_position = cli_crop
            print(f"   ✅ Crop-Position (vorgegeben): {crop_position}", file=sys.stderr)
        else:
            print("   🤖 K.I. bestimmt Crop-Position…", file=sys.stderr)
            try:
                crop_position, crop_reasoning = select_crop_with_ai(
                    highres_path, api_key=api_key, model=args.claude_model
                )
                reasoning = f"{reasoning} | Crop: {crop_reasoning}"
                print(
                    f'   ✅ Crop-Position: {crop_position} – "{crop_reasoning}"',
                    file=sys.stderr,
                )
            except RuntimeError as exc:
                print(f"   Error: {exc}", file=sys.stderr)
                return EXIT_AI_FAILED

    overlay_title = _resolve_overlay_title(args)
    category, category_logo_path = _resolve_category(args)
    note: Optional[str] = getattr(args, "overlay_note", None)
    detected_badges: List[str] = getattr(args, "detected_badges", [])
    poster_path = _resolve_output_path(args.input_path, args.output_dir, args.output_name_suffix)
    try:
        _apply_postprocess(highres_path, poster_path, args.format, crop_position, overlay_title,
                           overlay_category=category, overlay_category_logo_path=category_logo_path,
                           overlay_note=note, template=getattr(args, "template", None),
                           badges=detected_badges)
    except Exception as exc:
        print(f"   Error composing output image: {exc}", file=sys.stderr)
        return EXIT_ERROR

    metadata = build_metadata(
        source="frame", frame_index=frame_index, crop_position=crop_position,
        fmt=args.format, mode="auto", input_file=args.input_path,
        overlay_title=overlay_title, overlay_category=category or "",
        overlay_category_logo=category_logo_path or "", overlay_note=note or "",
        ai_reasoning=reasoning,
        poster_template=getattr(args, "poster_template", "") or "",
    )
    embed_metadata(poster_path, metadata)

    fanart_path = _generate_fanart_if_requested(
        args, highres_path, getattr(args, "video_properties", None)
    )
    _emit_result(
        poster_path, frame_index, "auto", reasoning, args.input_path, args.json,
        fmt=args.format, crop_position=crop_position, overlay_title=overlay_title,
        source="frame", overlay_category=category or "", overlay_note=note or "",
        badges=detected_badges or None, fanart_path=fanart_path,
    )
    return EXIT_SUCCESS


def _run_suggest(args: argparse.Namespace, frame_paths: list, mosaic_path: str, tmpdir: str) -> int:
    api_key = args.claude_api_key
    if not api_key:
        print(
            "   Error: Claude API key required for suggest mode. "
            "Set the CLAUDE_API_KEY env variable "
            "or run: video-thumbnail-creator config set claude.api_key <key>",
            file=sys.stderr,
        )
        return EXIT_AI_FAILED

    print("   🤖 K.I. analysiert Frames…", file=sys.stderr)
    try:
        ai_index, reasoning = select_frame_with_ai(
            mosaic_path,
            api_key=api_key,
            model=args.claude_model,
            description=args.description,
        )
    except RuntimeError as exc:
        print(f"   Error: {exc}", file=sys.stderr)
        return EXIT_AI_FAILED

    print(
        f'   🤖 K.I. Empfehlung: Frame {ai_index} – "{reasoning}"',
        file=sys.stderr,
    )
    print("   🖼️  Mosaikbild geöffnet in Vorschau.", file=sys.stderr)
    open_image(mosaic_path)

    try:
        raw = input(
            f"\n   [ENTER] = K.I.-Auswahl übernehmen (Frame {ai_index}), "
            "oder Zahl 0–19 eingeben: "
        ).strip()
    except (EOFError, KeyboardInterrupt):
        print("\n   Abgebrochen.", file=sys.stderr)
        return EXIT_NO_SELECTION

    if raw == "":
        frame_index = ai_index
        final_reasoning = reasoning
    else:
        try:
            frame_index = int(raw)
            if not (0 <= frame_index <= 19):
                raise ValueError
            final_reasoning = f"User overrode AI suggestion (AI: {ai_index})"
        except ValueError:
            print(
                f"   Ungültige Eingabe: '{raw}'. Bitte Zahl 0–19 eingeben.",
                file=sys.stderr,
            )
            return EXIT_ERROR

    highres_path = os.path.join(tmpdir, "highres.jpg")
    try:
        extract_single_frame_highres(
            args.input_path, frame_index, highres_path,
            ffmpeg=args.ffmpeg_bin, ffprobe=args.ffprobe_bin
        )
    except Exception as exc:
        print(f"   Error extracting high-res frame: {exc}", file=sys.stderr)
        return EXIT_ERROR

    crop_position = ""
    if args.format == "poster":
        cli_crop = getattr(args, "crop_position", None)
        if cli_crop:
            crop_position = cli_crop
            print(f"   ✅ Crop-Position (vorgegeben): {crop_position}", file=sys.stderr)
            final_reasoning = f"{final_reasoning} | Crop position provided via --crop-position."
        else:
            print("   🤖 K.I. bestimmt Crop-Position…", file=sys.stderr)
            try:
                ai_crop, crop_reasoning = select_crop_with_ai(
                    highres_path, api_key=api_key, model=args.claude_model
                )
                print(
                    f'   🤖 K.I. Crop-Empfehlung: {ai_crop} – "{crop_reasoning}"',
                    file=sys.stderr,
                )
            except RuntimeError as exc:
                print(f"   Error: {exc}", file=sys.stderr)
                return EXIT_AI_FAILED

            crop_position = _ask_crop_position_manual(highres_path)
            if not crop_position:
                crop_position = ai_crop

            if crop_position != ai_crop:
                final_reasoning = f"{final_reasoning} | Crop overridden by user (AI: {ai_crop})"
            else:
                final_reasoning = f"{final_reasoning} | Crop: {crop_reasoning}"

    overlay_title = _resolve_overlay_title(args)
    category, category_logo_path = _resolve_category(args)
    note: Optional[str] = getattr(args, "overlay_note", None)
    detected_badges: List[str] = getattr(args, "detected_badges", [])
    poster_path = _resolve_output_path(args.input_path, args.output_dir, args.output_name_suffix)
    try:
        _apply_postprocess(highres_path, poster_path, args.format, crop_position, overlay_title,
                           overlay_category=category, overlay_category_logo_path=category_logo_path,
                           overlay_note=note, template=getattr(args, "template", None),
                           badges=detected_badges)
    except Exception as exc:
        print(f"   Error composing output image: {exc}", file=sys.stderr)
        return EXIT_ERROR

    metadata = build_metadata(
        source="frame", frame_index=frame_index, crop_position=crop_position,
        fmt=args.format, mode="suggest", input_file=args.input_path,
        overlay_title=overlay_title, overlay_category=category or "",
        overlay_category_logo=category_logo_path or "", overlay_note=note or "",
        ai_reasoning=final_reasoning,
        poster_template=getattr(args, "poster_template", "") or "",
    )
    embed_metadata(poster_path, metadata)

    fanart_path = _generate_fanart_if_requested(
        args, highres_path, getattr(args, "video_properties", None)
    )
    _emit_result(
        poster_path, frame_index, "suggest", final_reasoning, args.input_path, args.json,
        fmt=args.format, crop_position=crop_position, overlay_title=overlay_title,
        source="frame", overlay_category=category or "", overlay_note=note or "",
        badges=detected_badges or None, fanart_path=fanart_path,
    )
    return EXIT_SUCCESS


def _run_with_image_source(
    args: argparse.Namespace,
    source_image: str,
    tmpdir: str,
    source: str,
    default_reasoning: str,
) -> int:
    """Process a pre-existing image (embedded, sidecar, or input image) as the source."""
    api_key = args.claude_api_key
    crop_position = ""
    reasoning = default_reasoning

    if args.format == "poster":
        cli_crop = getattr(args, "crop_position", None)
        if cli_crop:
            crop_position = cli_crop
            print(f"   ✅ Crop-Position (vorgegeben): {crop_position}", file=sys.stderr)
        elif args.mode == "manual":
            crop_position = _ask_crop_position_manual(source_image)
        else:
            # auto or suggest: use AI crop position
            if not api_key:
                print(
                    "   Error: Claude API key required for AI crop position. "
                    "Set the CLAUDE_API_KEY env variable "
                    "or run: video-thumbnail-creator config set claude.api_key <key>",
                    file=sys.stderr,
                )
                return EXIT_AI_FAILED
            print("   🤖 K.I. bestimmt Crop-Position…", file=sys.stderr)
            try:
                ai_crop, crop_reasoning = select_crop_with_ai(
                    source_image, api_key=api_key, model=args.claude_model
                )
            except RuntimeError as exc:
                print(f"   Error: {exc}", file=sys.stderr)
                return EXIT_AI_FAILED

            if args.mode == "suggest":
                print(
                    f'   🤖 K.I. Crop-Empfehlung: {ai_crop} – "{crop_reasoning}"',
                    file=sys.stderr,
                )
                crop_position = _ask_crop_position_manual(source_image)
                if not crop_position:
                    crop_position = ai_crop
                if crop_position != ai_crop:
                    reasoning = f"{reasoning} | Crop overridden by user (AI: {ai_crop})"
                else:
                    reasoning = f"{reasoning} | Crop: {crop_reasoning}"
            else:  # auto
                crop_position = ai_crop
                reasoning = f"{reasoning} | Crop: {crop_reasoning}"
                print(
                    f'   ✅ Crop-Position: {crop_position} – "{crop_reasoning}"',
                    file=sys.stderr,
                )

    overlay_title = _resolve_overlay_title(args)
    category, category_logo_path = _resolve_category(args)
    note: Optional[str] = getattr(args, "overlay_note", None)
    detected_badges: List[str] = getattr(args, "detected_badges", [])
    poster_path = _resolve_output_path(args.input_path, args.output_dir, args.output_name_suffix)
    try:
        _apply_postprocess(source_image, poster_path, args.format, crop_position, overlay_title,
                           overlay_category=category, overlay_category_logo_path=category_logo_path,
                           overlay_note=note, template=getattr(args, "template", None),
                           badges=detected_badges)
    except Exception as exc:
        print(f"   Error composing output image: {exc}", file=sys.stderr)
        return EXIT_ERROR

    metadata = build_metadata(
        source=source, frame_index=-1, crop_position=crop_position,
        fmt=args.format, mode=args.mode, input_file=args.input_path,
        overlay_title=overlay_title, overlay_category=category or "",
        overlay_category_logo=category_logo_path or "", overlay_note=note or "",
        ai_reasoning=reasoning,
        poster_template=getattr(args, "poster_template", "") or "",
    )
    embed_metadata(poster_path, metadata)

    fanart_path = _generate_fanart_if_requested(args, source_image, None)
    _emit_result(
        poster_path, -1, args.mode, reasoning, args.input_path, args.json,
        fmt=args.format, crop_position=crop_position, overlay_title=overlay_title,
        source=source, overlay_category=category or "", overlay_note=note or "",
        badges=detected_badges or None, fanart_path=fanart_path,
    )
    return EXIT_SUCCESS


def _run_with_embedded(args: argparse.Namespace, source_image: str, tmpdir: str) -> int:
    """Process a video using the embedded cover art image as the source."""
    return _run_with_image_source(
        args, source_image, tmpdir,
        source="embedded",
        default_reasoning="Embedded cover art was used as source image.",
    )


def _run_with_sidecar(args: argparse.Namespace, source_image: str, tmpdir: str) -> int:
    """Process a video using a sidecar image file as the source."""
    return _run_with_image_source(
        args, source_image, tmpdir,
        source="sidecar",
        default_reasoning="Sidecar image file was used as source.",
    )


def _run_with_image(args: argparse.Namespace, source_image: str, tmpdir: str) -> int:
    """Process an image file as the source for poster/landscape creation."""
    return _run_with_image_source(
        args, source_image, tmpdir,
        source="image",
        default_reasoning="Image file was used as source.",
    )


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="video-thumbnail-creator",
        description="Extract and select a thumbnail image from a video or image file.",
    )
    subparsers = parser.add_subparsers(dest="command")

    # ── extract subcommand ────────────────────────────────────────────────────
    extract = subparsers.add_parser(
        "extract",
        help="Extract a thumbnail from a video or image file.",
    )
    extract.add_argument("input_path", help="Path to the input video or image file.")
    extract.add_argument(
        "--mode",
        choices=["manual", "auto", "suggest"],
        default=None,
        help="Selection mode: manual (default), auto (AI), or suggest (AI + confirm).",
    )
    extract.add_argument(
        "--description",
        default=None,
        help="Optional description of the video for AI context (max 1000 chars).",
    )
    extract.add_argument(
        "--output-dir",
        default=None,
        dest="output_dir",
        help="Output directory (default: same directory as the video file).",
    )
    extract.add_argument(
        "--output-name-suffix",
        default=None,
        dest="output_name_suffix",
        help="Suffix appended to the video filename (without extension) to form the output name "
             "(default: -poster, e.g. video-poster.jpg).",
    )
    extract.add_argument(
        "--json",
        action="store_true",
        default=False,
        help="Emit machine-readable JSON to stdout.",
    )
    extract.add_argument(
        "--format",
        choices=["landscape", "poster"],
        default=None,
        dest="format",
        help="Output format: 'poster' (2:3, 1080×1620, default) or 'landscape' (16:9, 1920×1080).",
    )
    extract.add_argument(
        "--overlay-title",
        default=None,
        dest="overlay_title",
        help="Text to overlay on the output image.",
    )
    extract.add_argument(
        "--overlay-title-from-filename",
        action="store_true",
        default=False,
        dest="overlay_title_from_filename",
        help="Use the input filename stem as the overlay title.",
    )
    extract.add_argument(
        "--overlay-category",
        default=None,
        dest="overlay_category",
        help="Category label shown above the title in poster format.",
    )
    extract.add_argument(
        "--overlay-category-logo",
        default=None,
        dest="overlay_category_logo",
        metavar="PATH",
        help="PNG logo shown instead of category text in poster format. "
             "Wide logos are centered above the title; square/portrait logos appear left of it.",
    )
    extract.add_argument(
        "--overlay-note",
        default=None,
        dest="overlay_note",
        help="Small text shown at the bottom-right of the poster text area.",
    )
    extract.add_argument(
        "--poster-template",
        default=None,
        dest="poster_template",
        metavar="PATH",
        help="Path to a poster template TOML file for design customization. "
             "Default: ~/.config/video-thumbnail-creator/poster_template.toml",
    )
    extract.add_argument(
        "--embedded-image",
        choices=["prefer", "ignore", "ask"],
        default=None,
        dest="embedded_image",
        help="How to handle embedded cover art and sidecar images: prefer (use if available), "
             "ignore (always extract frames), ask (prompt user). Default from config.",
    )
    extract.add_argument(
        "--crop-position",
        choices=["left", "center-left", "center", "center-right", "right"],
        default=None,
        dest="crop_position",
        help="Crop position for poster format. Skips interactive prompt and AI crop selection. "
             "One of: left, center-left, center, center-right, right.",
    )
    extract.add_argument(
        "--no-badges",
        action="store_true",
        default=False,
        dest="no_badges",
        help="Disable automatic technical badges (4K, HDR, FHD) on the poster.",
    )
    extract.add_argument(
        "--fanart",
        action="store_true",
        default=False,
        dest="fanart",
        help="Generate an additional clean 16:9 fanart image (for Infuse/Emby) with -fanart suffix.",
    )

    # ── config subcommand ─────────────────────────────────────────────────────
    config_parser = subparsers.add_parser(
        "config",
        help="Manage configuration settings.",
    )
    config_sub = config_parser.add_subparsers(dest="config_command")

    config_set = config_sub.add_parser("set", help="Set a configuration value.")
    config_set.add_argument("key", help="Config key in section.field format (e.g. claude.api_key).")
    config_set.add_argument("value", help="Value to store.")

    config_get = config_sub.add_parser("get", help="Get a configuration value.")
    config_get.add_argument("key", help="Config key in section.field format.")

    config_sub.add_parser("list", help="List all configured values.")

    # ── info subcommand ────────────────────────────────────────────────────
    info_parser = subparsers.add_parser(
        "info",
        help="Display embedded metadata from a generated poster image.",
    )
    info_parser.add_argument("image_path", help="Path to a poster image file.")
    info_parser.add_argument(
        "--json",
        action="store_true",
        default=False,
        help="Emit metadata as JSON to stdout.",
    )

    return parser


def _handle_config_command(args: argparse.Namespace) -> None:
    """Dispatch config sub-commands."""
    if args.config_command == "set":
        try:
            set_value(args.key, args.value)
            print(f"Config '{args.key}' set to '{args.value}'.")
        except ValueError as exc:
            print(f"Error: {exc}", file=sys.stderr)
            sys.exit(EXIT_ERROR)
    elif args.config_command == "get":
        try:
            value = get_value(args.key)
        except ValueError as exc:
            print(f"Error: {exc}", file=sys.stderr)
            sys.exit(EXIT_ERROR)
        if value is None:
            print(f"Config key '{args.key}' is not set.", file=sys.stderr)
            sys.exit(EXIT_ERROR)
        print(value)
    elif args.config_command == "list":
        config = list_config()
        if not config:
            print("No configuration values set.")
        else:
            for section, values in sorted(config.items()):
                print(f"[{section}]")
                for key, val in sorted(values.items()):
                    print(f"  {key} = {val}")
    else:
        print("Usage: video-thumbnail-creator config <set|get|list>", file=sys.stderr)
        sys.exit(EXIT_ERROR)


def _handle_info_command(args: argparse.Namespace) -> None:
    """Display embedded metadata from a poster image."""
    from .metadata import read_metadata

    if not os.path.isfile(args.image_path):
        print(f"Error: File not found: '{args.image_path}'", file=sys.stderr)
        sys.exit(EXIT_ERROR)

    metadata = read_metadata(args.image_path)
    if metadata is None:
        print("No video-thumbnail-creator metadata found in this image.", file=sys.stderr)
        sys.exit(EXIT_ERROR)

    if args.json:
        print(json.dumps(metadata, ensure_ascii=False, indent=2))
    else:
        print("Poster Metadata:")
        field_labels = {
            "vtc_version": "Version",
            "source": "Source",
            "frame_index": "Frame Index",
            "crop_position": "Crop Position",
            "format": "Format",
            "mode": "Mode",
            "input_file": "Input File",
            "overlay_title": "Overlay Title",
            "overlay_category": "Category",
            "overlay_category_logo": "Category Logo",
            "overlay_note": "Note",
            "ai_reasoning": "AI Reasoning",
            "poster_template": "Template",
            "created_at": "Created",
        }
        for key, label in field_labels.items():
            if key in metadata:
                value = metadata[key]
                # Truncate long AI reasoning for display
                if key == "ai_reasoning" and len(str(value)) > 100:
                    value = str(value)[:100] + "…"
                print(f"  {label:18s} {value}")


def main() -> None:
    parser = _build_parser()
    args = parser.parse_args()

    if args.command is None:
        parser.print_help()
        sys.exit(EXIT_ERROR)

    if args.command == "config":
        _handle_config_command(args)
        sys.exit(EXIT_SUCCESS)

    if args.command == "info":
        _handle_info_command(args)
        sys.exit(EXIT_SUCCESS)

    # ── extract command ───────────────────────────────────────────────────────

    # Validate input file
    if not os.path.isfile(args.input_path):
        print(f"Error: Input file not found: '{args.input_path}'", file=sys.stderr)
        sys.exit(EXIT_ERROR)

    # Detect input type
    input_is_image = is_image_file(args.input_path)

    # Resolve effective values: CLI args > config file > built-in defaults
    effective = get_effective_config()

    mode = args.mode or effective["defaults"]["mode"]
    output_name_suffix = args.output_name_suffix or effective["defaults"]["output_name_suffix"]
    fmt = args.format or effective["defaults"]["format"]
    claude_api_key = (
        os.environ.get("CLAUDE_API_KEY", "")
        or effective.get("claude", {}).get("api_key", "")
    )
    claude_model = effective["claude"]["model"]
    ffmpeg_bin = effective["tools"]["ffmpeg"]
    ffprobe_bin = effective["tools"]["ffprobe"]

    # Store resolved values back into args for downstream helpers
    args.mode = mode
    args.output_name_suffix = output_name_suffix
    args.format = fmt
    args.claude_api_key = claude_api_key
    args.claude_model = claude_model
    args.ffmpeg_bin = ffmpeg_bin
    args.ffprobe_bin = ffprobe_bin

    if not input_is_image:
        check_dependencies(ffmpeg=ffmpeg_bin, ffprobe=ffprobe_bin)

    # Load poster design template
    poster_template_path = getattr(args, "poster_template", None)
    args.template = load_template(poster_template_path)

    embedded_image_mode = args.embedded_image or effective["defaults"]["embedded_image"]
    args.embedded_image = embedded_image_mode

    with tempfile.TemporaryDirectory(prefix="vtc_") as tmpdir:
        if input_is_image:
            # ── Image input flow ──────────────────────────────────────────────
            args.detected_badges = []  # No badge detection for image input
            prepared_path = os.path.join(tmpdir, "prepared_source.jpg")
            try:
                source_image = prepare_image_source(args.input_path, prepared_path)
            except Exception as exc:
                print(f"   Error preparing image: {exc}", file=sys.stderr)
                sys.exit(EXIT_ERROR)
            exit_code = _run_with_image(args, source_image, tmpdir)
        else:
            # ── Video input flow ──────────────────────────────────────────────
            # Detect video properties and badges (unless --no-badges)
            badges_enabled = (
                not getattr(args, "no_badges", False)
                and args.template.get("badges", {}).get("enabled", True)
            )
            fanart_requested = getattr(args, "fanart", False)
            if badges_enabled or fanart_requested:
                video_props = get_video_properties(args.input_path, ffprobe=ffprobe_bin)
                args.video_properties = video_props
                if badges_enabled:
                    args.detected_badges = detect_badges(video_props)
                    if args.detected_badges:
                        print(
                            f"   🏷️  Erkannte Badges: {', '.join(b.upper() for b in args.detected_badges)}",
                            file=sys.stderr,
                        )
                else:
                    args.detected_badges = []
            else:
                args.detected_badges = []

            # ── Embedded image handling ───────────────────────────────────────
            source_image_video: str | None = None

            if embedded_image_mode != "ignore":
                print("   📎 Prüfe auf eingebettetes Titelbild…", file=sys.stderr)
                has_embedded = detect_embedded_image(args.input_path, ffprobe=ffprobe_bin)

                if has_embedded:
                    print("   📎 Eingebettetes Titelbild gefunden!", file=sys.stderr)
                    use_embedded = False

                    if embedded_image_mode == "prefer":
                        use_embedded = True
                    elif embedded_image_mode == "ask":
                        # In non-interactive (auto) mode treat ask like prefer
                        if args.mode == "auto":
                            use_embedded = True
                        else:
                            try:
                                answer = input(
                                    "   Eingebettetes Titelbild gefunden. Verwenden? [J/n]: "
                                ).strip().lower()
                            except (EOFError, KeyboardInterrupt):
                                answer = "j"
                            use_embedded = answer in ("", "j", "y", "ja", "yes")

                    if use_embedded:
                        print("   📎 Eingebettetes Titelbild wird verwendet.", file=sys.stderr)
                        embedded_path = os.path.join(tmpdir, "embedded.jpg")
                        try:
                            source_image_video = extract_embedded_image(
                                args.input_path, embedded_path,
                                ffmpeg=ffmpeg_bin, ffprobe=ffprobe_bin,
                            )
                        except Exception as exc:
                            print(f"   Error extracting embedded image: {exc}", file=sys.stderr)
                            sys.exit(EXIT_ERROR)
                    else:
                        print("   📎 Eingebettetes Titelbild wird ignoriert.", file=sys.stderr)
                else:
                    print(
                        "   📎 Kein eingebettetes Titelbild gefunden. "
                        "Fahre mit Frame-Extraktion fort.",
                        file=sys.stderr,
                    )

            if source_image_video is not None:
                # Use the embedded image as the high-res source; skip mosaic
                exit_code = _run_with_embedded(args, source_image_video, tmpdir)
            else:
                # ── Sidecar image handling ────────────────────────────────────
                if embedded_image_mode != "ignore":
                    sidecar_path = detect_sidecar_image(args.input_path)
                    if sidecar_path:
                        print(
                            f"   📁 Sidecar-Titelbild gefunden: {os.path.basename(sidecar_path)}",
                            file=sys.stderr,
                        )
                        use_sidecar = False

                        if embedded_image_mode == "prefer":
                            use_sidecar = True
                        elif embedded_image_mode == "ask":
                            if args.mode == "auto":
                                use_sidecar = True
                            else:
                                try:
                                    answer = input(
                                        f"   Sidecar-Titelbild gefunden ({os.path.basename(sidecar_path)}). "
                                        "Verwenden? [J/n]: "
                                    ).strip().lower()
                                except (EOFError, KeyboardInterrupt):
                                    answer = "j"
                                use_sidecar = answer in ("", "j", "y", "ja", "yes")

                        if use_sidecar:
                            print("   📁 Sidecar-Titelbild wird verwendet.", file=sys.stderr)
                            prepared_path = os.path.join(tmpdir, "sidecar_prepared.jpg")
                            try:
                                source_image_video = prepare_image_source(sidecar_path, prepared_path)
                            except Exception as exc:
                                print(f"   Error preparing sidecar image: {exc}", file=sys.stderr)
                                sys.exit(EXIT_ERROR)
                        else:
                            print("   📁 Sidecar-Titelbild wird ignoriert.", file=sys.stderr)

                if source_image_video is not None:
                    # Use the sidecar image as the high-res source; skip mosaic
                    exit_code = _run_with_sidecar(args, source_image_video, tmpdir)
                else:
                    # ── Normal mosaic flow ────────────────────────────────────
                    print("   📽️  Extrahiere Frames…", file=sys.stderr)

                    frames_dir = os.path.join(tmpdir, "frames")
                    os.makedirs(frames_dir)

                    try:
                        frame_paths = extract_frames(
                            args.input_path, frames_dir, ffmpeg=ffmpeg_bin, ffprobe=ffprobe_bin
                        )
                    except Exception as exc:
                        print(f"Error during frame extraction: {exc}", file=sys.stderr)
                        sys.exit(EXIT_ERROR)

                    mosaic_path = os.path.join(tmpdir, "mosaic.jpg")
                    try:
                        create_mosaic(frame_paths, mosaic_path)
                    except Exception as exc:
                        print(f"Error creating mosaic: {exc}", file=sys.stderr)
                        sys.exit(EXIT_ERROR)

                    if args.mode == "manual":
                        exit_code = _run_manual(args, frame_paths, mosaic_path, tmpdir)
                    elif args.mode == "auto":
                        exit_code = _run_auto(args, frame_paths, mosaic_path, tmpdir)
                    else:  # suggest
                        exit_code = _run_suggest(args, frame_paths, mosaic_path, tmpdir)

    sys.exit(exit_code)


if __name__ == "__main__":
    main()
