"""Mid-level Library API: ThumbnailSession for step-by-step thumbnail creation."""

import dataclasses
import os
import shutil
import tempfile
from typing import List, Optional

from .ai_selector import DEFAULT_CLAUDE_MODEL, select_frame_with_ai
from .badges import detect_badges
from .crop_selector import select_crop_with_ai
from .extractor import extract_frames, extract_single_frame_highres
from .fanart_composer import FANART_SUFFIX, compose_fanart
from .image_converter import is_image_file, prepare_image_source
from .mosaic import create_mosaic
from .poster_composer import compose_landscape_with_text, compose_poster
from .utils import get_video_properties


@dataclasses.dataclass
class ThumbnailResult:
    """Result of a thumbnail composition operation."""

    poster_path: str
    fanart_path: Optional[str] = None
    frame_index: int = -1
    crop_position: str = "center"
    reasoning: str = ""
    format: str = "poster"
    source: str = "frame"  # "frame", "image", "embedded", "sidecar"


class ThumbnailSession:
    """Multi-step thumbnail creation session.

    Supports the context manager protocol for automatic cleanup of temporary files.

    Usage::

        with ThumbnailSession("/path/to/video.mp4") as session:
            # Step 1: Access mosaic and frames (created during __init__)
            session.mosaic_path       # path to mosaic image
            session.frame_paths       # list of 20 frame image paths
            session.video_properties  # {"width", "height", "is_4k", "is_hdr", "is_hd", "fps", "is_hfr"}

            # Step 2 (optional): Ask AI for suggestion
            suggestion = session.suggest_frame(title="...", description="...")
            # Returns: {"frame_index": 12, "reasoning": "..."}

            # Step 3: Select a frame (from AI suggestion or manual choice)
            session.select_frame(12)
            session.highres_path      # path to high-res extracted frame

            # Step 4 (optional): Ask AI for crop position suggestion
            crop_suggestion = session.suggest_crop()
            # Returns: {"crop_position": "center-left", "reasoning": "..."}

            # Step 5: Compose final output
            result = session.compose(
                crop_position="center-left",
                overlay_title="My Title",
                format="poster",
                output_dir="/output/",
                fanart=True,
            )
            # result.poster_path, result.fanart_path, etc.
    """

    def __init__(
        self,
        input_path: str,
        *,
        ffmpeg: str = "ffmpeg",
        ffprobe: str = "ffprobe",
    ) -> None:
        if not os.path.isfile(input_path):
            raise FileNotFoundError(f"Input file not found: '{input_path}'")

        self.input_path = input_path
        self._ffmpeg = ffmpeg
        self._ffprobe = ffprobe
        self._is_image = is_image_file(input_path)
        self._selected_frame_index: int = -1
        self._frame_selected: bool = False
        self._tmpdir: str = tempfile.mkdtemp(prefix="vtc_session_")

        if self._is_image:
            highres_out = os.path.join(self._tmpdir, "highres.jpg")
            self.highres_path: Optional[str] = prepare_image_source(input_path, highres_out)
            self.mosaic_path: Optional[str] = None
            self.frame_paths: List[str] = []

            from PIL import Image as _Image
            with _Image.open(self.highres_path) as _img:
                _w, _h = _img.size
            self.video_properties: dict = {
                "width": _w,
                "height": _h,
                "is_4k": _w >= 3840 or _h >= 2160,
                "is_hdr": False,
                "is_hd": (_w >= 1920 or _h >= 1080) and not (_w >= 3840 or _h >= 2160),
                "fps": 0.0,
                "is_hfr": False,
            }
            self._frame_selected = True
        else:
            self.video_properties = get_video_properties(input_path, ffprobe=ffprobe)
            self.frame_paths = extract_frames(
                input_path, self._tmpdir, ffmpeg=ffmpeg, ffprobe=ffprobe
            )
            mosaic_out = os.path.join(self._tmpdir, "mosaic.jpg")
            self.mosaic_path = create_mosaic(self.frame_paths, mosaic_out)
            self.highres_path = None
            self._frame_selected = False

    def _get_api_credentials(self):
        """Return (api_key, model) from the environment or stored config."""
        from .config import get_effective_config

        effective = get_effective_config()
        api_key = (
            os.environ.get("CLAUDE_API_KEY", "")
            or effective.get("claude", {}).get("api_key", "")
        )
        model = effective.get("claude", {}).get("model", DEFAULT_CLAUDE_MODEL)
        if not api_key:
            raise RuntimeError(
                "Claude API key required. Set the CLAUDE_API_KEY environment variable "
                "or run: video-thumbnail-creator config set claude.api_key <key>"
            )
        return api_key, model

    def suggest_frame(self, *, title: Optional[str] = None, description: Optional[str] = None) -> dict:
        """Ask the AI to suggest the best frame from the mosaic.

        Parameters
        ----------
        title:
            Optional title of the video, used as additional context for the AI.
        description:
            Optional description of the video content for context.

        Returns
        -------
        dict with ``{"frame_index": int, "reasoning": str}``.

        Raises
        ------
        RuntimeError
            If the input is an image (no frame selection needed) or if the AI
            call fails.
        """
        if self._is_image:
            raise RuntimeError(
                "suggest_frame() is not available for image input — no frame selection needed."
            )
        if self.mosaic_path is None:
            raise RuntimeError(
                "No mosaic available. The session was not initialized correctly."
            )

        api_key, model = self._get_api_credentials()

        # Combine title and description for the AI context
        parts = [p for p in (title, description) if p]
        context = ": ".join(parts) if parts else None

        frame_index, reasoning = select_frame_with_ai(
            self.mosaic_path,
            api_key,
            model=model,
            description=context,
        )
        return {"frame_index": frame_index, "reasoning": reasoning}

    def select_frame(self, frame_index: int) -> None:
        """Extract a high-res frame and mark it as selected.

        Parameters
        ----------
        frame_index:
            Index of the frame to select (0–19).

        Raises
        ------
        RuntimeError
            If the input is an image.
        ValueError
            If ``frame_index`` is outside 0–19.
        """
        if self._is_image:
            raise RuntimeError(
                "select_frame() is not available for image input."
            )
        if not (0 <= frame_index <= 19):
            raise ValueError(f"frame_index must be 0–19, got {frame_index}.")

        highres_out = os.path.join(self._tmpdir, "highres.jpg")
        self.highres_path = extract_single_frame_highres(
            self.input_path,
            frame_index,
            highres_out,
            ffmpeg=self._ffmpeg,
            ffprobe=self._ffprobe,
        )
        self._selected_frame_index = frame_index
        self._frame_selected = True

    def suggest_crop(self) -> dict:
        """Ask the AI to suggest the best 1:1 crop position for a poster.

        Returns
        -------
        dict with ``{"crop_position": str, "reasoning": str}``.

        Raises
        ------
        RuntimeError
            If no frame has been selected yet or if the AI call fails.
        """
        if not self._frame_selected:
            raise RuntimeError(
                "Call select_frame() before suggest_crop()."
            )

        api_key, model = self._get_api_credentials()
        crop_position, reasoning = select_crop_with_ai(
            self.highres_path, api_key, model
        )
        return {"crop_position": crop_position, "reasoning": reasoning}

    def compose(
        self,
        *,
        crop_position: str = "center",
        overlay_title: Optional[str] = None,
        format: str = "poster",
        output_dir: Optional[str] = None,
        output_path: Optional[str] = None,
        output_name_suffix: str = "-poster",
        fanart: bool = False,
        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,
        no_badges: bool = False,
    ) -> ThumbnailResult:
        """Compose the final thumbnail image.

        Parameters
        ----------
        crop_position:
            Horizontal crop position for poster format (``left``, ``center-left``,
            ``center``, ``center-right``, ``right``).
        overlay_title:
            Optional title text to render on the image.
        format:
            Output format: ``"poster"`` (2:3) or ``"landscape"`` (16:9).
        output_dir:
            Directory for the output file.  Defaults to the same directory as
            ``input_path``.
        output_path:
            Explicit output file path.  Takes priority over ``output_dir`` and
            ``output_name_suffix``.
        output_name_suffix:
            Suffix appended to the input file stem for the output file name.
        fanart:
            When ``True`` also compose a 16:9 fanart image.
        overlay_category:
            Optional category label shown above the title (poster only).
        overlay_category_logo_path:
            Optional path to a PNG logo shown instead of category text.
        overlay_note:
            Optional small text at the bottom-right of the text area (poster only).
        template:
            Optional design template dict.
        badges:
            Explicit list of badge types (e.g. ``["4k", "hdr"]``).  When
            ``None`` and ``no_badges`` is ``False`` and ``format`` is
            ``"poster"``, badges are auto-detected from ``video_properties``.
        no_badges:
            When ``True`` suppress all badges even if auto-detected.

        Returns
        -------
        :class:`ThumbnailResult`

        Raises
        ------
        RuntimeError
            If no frame/image has been selected yet.
        """
        if not self._frame_selected:
            raise RuntimeError(
                "Call select_frame() before compose()."
            )

        # Resolve output path
        if output_path is None:
            base_dir = output_dir or os.path.dirname(os.path.abspath(self.input_path))
            os.makedirs(base_dir, exist_ok=True)
            input_stem = os.path.splitext(os.path.basename(self.input_path))[0]
            output_path = os.path.join(base_dir, f"{input_stem}{output_name_suffix}.jpg")

        # Auto-detect badges when not explicitly provided
        if badges is None and not no_badges and format == "poster":
            badges = detect_badges(self.video_properties)

        # Compose main output
        if format == "poster":
            compose_poster(
                self.highres_path,
                crop_position or "center",
                overlay_title,
                output_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(
                self.highres_path, overlay_title, output_path, template=template
            )
        else:
            shutil.copy2(self.highres_path, output_path)

        # Compose fanart if requested
        fanart_out: Optional[str] = None
        if fanart:
            is_4k = bool(self.video_properties.get("is_4k", False))
            fanart_dir = output_dir or os.path.dirname(os.path.abspath(self.input_path))
            fanart_stem = os.path.splitext(os.path.basename(self.input_path))[0]
            fanart_path_out = os.path.join(fanart_dir, f"{fanart_stem}{FANART_SUFFIX}.jpg")
            fanart_out = compose_fanart(self.highres_path, fanart_path_out, is_4k=is_4k)

        source = "image" if self._is_image else "frame"
        return ThumbnailResult(
            poster_path=os.path.abspath(output_path),
            fanart_path=fanart_out,
            frame_index=self._selected_frame_index,
            crop_position=crop_position or "center",
            reasoning="",
            format=format,
            source=source,
        )

    def cleanup(self) -> None:
        """Remove the temporary directory and all intermediate files.

        Safe to call multiple times.
        """
        if self._tmpdir and os.path.isdir(self._tmpdir):
            shutil.rmtree(self._tmpdir, ignore_errors=True)
            self._tmpdir = ""

    def __enter__(self) -> "ThumbnailSession":
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        self.cleanup()
        return False
