"""Poster rebuild module: regenerate poster images for existing M4V files.

This module provides functions to rebuild poster images for finished M4V files
without going through the entire curation workflow. It works with local files
and remote files accessible via rclone.
"""
import os
import tempfile

from mutagen.mp4 import MP4, MP4Cover

from vorschaubild_manager import (
    ExtractFrameRequest,
    ExtractRequest,
    GetVideoPropertiesRequest,
    RenderPosterRequest,
    RuntimeOptions,
    extract,
    extract_frame,
    get_style,
    get_video_properties,
    read_poster_metadata,
    render_poster,
)

from .config import CONFIG
from .module_mosaic import (
    _POSTER_STYLE_NAME,
    _build_suggest_frame_description,
    _format_upload_date,
)
from .utils import run_shell


def _runtime_options() -> RuntimeOptions:
    """Build runtime options for vorschaubild-manager."""
    return RuntimeOptions(
        ffmpeg=CONFIG.get("ffmpeg", "ffmpeg"),
        ffprobe=CONFIG.get("ffprobe", "ffprobe"),
    )


def _is_rclone_path(path: str) -> bool:
    """Return True if path looks like an rclone remote path (e.g. 'lys-nas:/...')."""
    # rclone paths have a colon that is not a drive letter (Windows) – one letter before ":"
    # is a Windows drive, two or more letters before ":" is rclone remote
    if ":" not in path:
        return False
    prefix = path.split(":")[0]
    return len(prefix) > 1


def _ensure_local_m4v(path: str, tmp_dir: str) -> str:
    """Download an rclone remote M4V to tmp_dir and return the local path.

    If path is already a local path the function returns it unchanged.
    """
    if not _is_rclone_path(path):
        return path

    filename = os.path.basename(path)
    local_path = os.path.join(tmp_dir, filename)
    print(f"   ⬇️  Lade von Remote: {path}")
    result = run_shell(
        ["rclone", "copy", path, tmp_dir, "--progress"],
        capture_output=False,
    )
    if not os.path.exists(local_path):
        raise RuntimeError(f"Download fehlgeschlagen: {path}")
    print(f"   ✅ Heruntergeladen: {local_path}")
    return local_path


def _upload_m4v_back(local_path: str, remote_path: str) -> None:
    """Upload a local file back to an rclone remote path (directory)."""
    remote_dir = os.path.dirname(remote_path)
    print(f"   ⬆️  Lade zurück nach Remote: {remote_dir}")
    run_shell(
        ["rclone", "copy", local_path, remote_dir, "--progress"],
        capture_output=False,
    )
    print(f"   ✅ Upload abgeschlossen.")


def _extract_poster_from_m4v(m4v_path: str, output_path: str) -> bool:
    """Extract embedded cover art (covr) from an M4V file.

    Args:
        m4v_path: Path to the M4V file.
        output_path: Destination path for the extracted JPEG image.

    Returns:
        True if a poster was extracted, False otherwise.
    """
    try:
        mp4 = MP4(m4v_path)
        covers = mp4.get("covr")
        if not covers:
            return False
        cover = covers[0]
        with open(output_path, "wb") as f:
            f.write(bytes(cover))
        return True
    except Exception as e:
        print(f"   ⚠️  Fehler beim Extrahieren des Posters: {e}")
        return False


def _read_m4v_metadata(m4v_path: str) -> dict:
    """Read curated tags from an M4V file via mutagen.

    Returns a dict with keys: title, channel, upload_date, description.
    """
    meta = {}
    try:
        mp4 = MP4(m4v_path)
        raw_title = mp4.get("\xa9nam")
        if raw_title:
            meta["title"] = raw_title[0] if isinstance(raw_title, list) else raw_title

        raw_artist = mp4.get("\xa9ART")
        if raw_artist:
            meta["channel"] = raw_artist[0] if isinstance(raw_artist, list) else raw_artist

        raw_date = mp4.get("\xa9day")
        if raw_date:
            date_str = raw_date[0] if isinstance(raw_date, list) else raw_date
            # ISO date like "2022-12-08" or "20221208"
            meta["upload_date"] = date_str

        raw_desc = mp4.get("desc")
        if raw_desc:
            meta["description"] = raw_desc[0] if isinstance(raw_desc, list) else raw_desc
        else:
            raw_long_desc = mp4.get("ldes")
            if raw_long_desc:
                meta["description"] = raw_long_desc[0] if isinstance(raw_long_desc, list) else raw_long_desc
    except Exception as e:
        print(f"   ⚠️  Fehler beim Lesen der M4V-Metadaten: {e}")
    return meta


def _iso_date_to_overlay_note(iso_date: str) -> str:
    """Convert an ISO date string to a German overlay note.

    Accepts both YYYY-MM-DD and YYYYMMDD formats.
    """
    if not iso_date:
        return ""
    # Normalise to YYYYMMDD for _format_upload_date
    compact = iso_date.replace("-", "")
    return _format_upload_date(compact)


def _embed_poster_in_m4v(m4v_path: str, poster_path: str) -> None:
    """Replace the cover art in an M4V file with a new poster image.

    Args:
        m4v_path: Path to the M4V file (modified in-place).
        poster_path: Path to the new JPEG poster image.
    """
    is_jpeg = poster_path.lower().endswith((".jpg", ".jpeg"))
    fmt = MP4Cover.FORMAT_JPEG if is_jpeg else MP4Cover.FORMAT_PNG
    mp4 = MP4(m4v_path)
    with open(poster_path, "rb") as f:
        mp4["covr"] = [MP4Cover(f.read(), imageformat=fmt)]
    mp4.save()
    print(f"   ✅ Poster eingebettet: {os.path.basename(poster_path)}")


def rebuild_poster_for_m4v(m4v_path: str, *, new_frame: bool, fanart: "bool | None") -> bool:
    """Rebuild the poster for a single (local) M4V file.

    Args:
        m4v_path: Absolute path to a local M4V file.
        new_frame: When True the AI always selects a new frame, ignoring any
            saved frame_index in the existing poster EXIF.
        fanart: When True always generate fanart; when False never generate;
            when None use the CONFIG value for ``fanart_enabled``.

    Returns:
        True on success, False on failure.
    """
    print(f"\n🖼️  Poster neu generieren: {os.path.basename(m4v_path)}")

    if not os.path.exists(m4v_path):
        print(f"   ❌ Datei nicht gefunden: {m4v_path}")
        return False

    # Step 1: Read curated metadata from M4V
    meta = _read_m4v_metadata(m4v_path)
    title = meta.get("title", "")
    channel = meta.get("channel", "")
    upload_date = meta.get("upload_date", "")
    description = meta.get("description", "")

    overlay_note = _iso_date_to_overlay_note(upload_date)

    if title:
        print(f"   📝 Titel: {title}")
    if channel:
        print(f"   📂 Kanal: {channel}")
    if overlay_note:
        print(f"   📅 Datum: {overlay_note}")

    # Step 2: Extract existing poster and read EXIF frame_index / crop_position
    saved_frame_index = None
    saved_crop_position = None

    with tempfile.TemporaryDirectory() as tmp_dir:
        existing_poster_path = os.path.join(tmp_dir, "existing_poster.jpg")
        if _extract_poster_from_m4v(m4v_path, existing_poster_path):
            exif_data = read_poster_metadata(existing_poster_path)
            if exif_data:
                saved_frame_index = exif_data.get("frame_index")
                saved_crop_position = exif_data.get("crop_position")
                if saved_frame_index is not None:
                    print(f"   🔍 Gespeicherter Frame-Index: {saved_frame_index}")
                if saved_crop_position:
                    print(f"   🔍 Gespeicherte Crop-Position: {saved_crop_position}")

    # Step 3: Determine fanart setting
    if fanart is None:
        fanart_enabled = CONFIG.get("fanart_enabled", False)
    else:
        fanart_enabled = bool(fanart)

    # Step 4: Generate a new poster via the public API
    new_poster_path = os.path.splitext(m4v_path)[0] + "_new_poster.jpg"
    target_fanart_path = f"{os.path.splitext(m4v_path)[0]}-fanart.jpg"
    runtime = _runtime_options()
    try:
        video_properties = get_video_properties(
            GetVideoPropertiesRequest(input_path=m4v_path),
            runtime,
        )
    except Exception:
        video_properties = None
    frame_description = _build_suggest_frame_description(description)

    try:
        use_saved_frame = (
            saved_frame_index is not None
            and not new_frame
        )

        if use_saved_frame:
            with tempfile.TemporaryDirectory() as tmp_dir:
                print(f"   ♻️  Wiederverwendung von Frame {saved_frame_index} (kein K.I.-Aufruf)")
                frame = extract_frame(
                    ExtractFrameRequest(
                        input_path=m4v_path,
                        frame_index=saved_frame_index,
                        output_path=os.path.join(tmp_dir, "selected_frame.jpg"),
                    ),
                    runtime,
                )
                crop_position = saved_crop_position or "center"
                print(f"   🎯 Crop-Position: {crop_position}")

                result = render_poster(
                    RenderPosterRequest(
                        image_path=frame.frame_path,
                        format="poster",
                        output_path=new_poster_path,
                        output_dir=os.path.dirname(m4v_path),
                        crop_position=crop_position,
                        template=get_style(_POSTER_STYLE_NAME),
                        overlay_title=title or None,
                        overlay_category=channel or None,
                        overlay_note=overlay_note or None,
                        fanart=fanart_enabled,
                        video_properties=video_properties,
                    ),
                    runtime,
                )
        else:
            print("   🤖 K.I. wählt neuen Frame...")
            result = extract(
                ExtractRequest(
                    input_path=m4v_path,
                    format="poster",
                    output_path=new_poster_path,
                    output_dir=os.path.dirname(m4v_path),
                    crop_position=None,
                    overlay_title=title or None,
                    overlay_category=channel or None,
                    overlay_note=overlay_note or None,
                    fanart=fanart_enabled,
                    template=get_style(_POSTER_STYLE_NAME),
                    description=frame_description,
                ),
                runtime,
            )
            print(f"   ✅ K.I. Auswahl: Frame {result.frame_index}")
            if result.reasoning:
                print(f"   💡 Begründung: {result.reasoning}")
            print(f"   🎯 Crop-Position: {result.crop_position}")

        print(f"   ✅ Neues Poster gespeichert: {result.poster_path}")

        # Step 6: Embed new poster back into M4V
        _embed_poster_in_m4v(m4v_path, result.poster_path)

        # Step 7: Store fanart at the expected sidecar location
        if fanart_enabled and result.fanart_path:
            if os.path.abspath(result.fanart_path) != os.path.abspath(target_fanart_path):
                os.replace(result.fanart_path, target_fanart_path)
            print(f"   ✅ Fanart gespeichert: {target_fanart_path}")

    except Exception as e:
        print(f"   ❌ Fehler bei Poster-Generierung: {e}")
        return False
    finally:
        # Clean up temporary poster file
        if os.path.exists(new_poster_path):
            os.remove(new_poster_path)

    return True


def rebuild_poster(path: str, *, new_frame: bool = False, fanart: "bool | None" = None) -> bool:
    """Rebuild the poster for a single M4V file (local or rclone remote).

    Args:
        path: Local path or rclone remote path to an M4V file.
        new_frame: If True the AI always picks a new frame.
        fanart: True/False to force fanart on/off, None to use config default.

    Returns:
        True on success, False on failure.
    """
    if _is_rclone_path(path):
        with tempfile.TemporaryDirectory() as tmp_dir:
            try:
                local_path = _ensure_local_m4v(path, tmp_dir)
                success = rebuild_poster_for_m4v(local_path, new_frame=new_frame, fanart=fanart)
                if success:
                    # Upload updated M4V back
                    _upload_m4v_back(local_path, path)
                    # Upload fanart sidecar if it was created
                    fanart_enabled = fanart if fanart is not None else CONFIG.get("fanart_enabled", False)
                    if fanart_enabled:
                        local_stem = os.path.splitext(local_path)[0]
                        local_fanart = f"{local_stem}-fanart.jpg"
                        if os.path.exists(local_fanart):
                            remote_dir = os.path.dirname(path)
                            remote_stem = os.path.splitext(os.path.basename(path))[0]
                            remote_fanart = f"{remote_dir}/{remote_stem}-fanart.jpg"
                            run_shell(
                                ["rclone", "copyto", local_fanart, remote_fanart],
                                capture_output=False,
                            )
                return success
            except Exception as e:
                print(f"   ❌ Fehler: {e}")
                return False
    else:
        return rebuild_poster_for_m4v(path, new_frame=new_frame, fanart=fanart)


def rebuild_posters_batch(directory: str, *, new_frame: bool = False, fanart: "bool | None" = None, recursive: bool = False) -> None:
    """Rebuild posters for all M4V files in a directory (local or rclone remote).

    Args:
        directory: Local path or rclone remote directory path.
        new_frame: If True the AI always picks new frames.
        fanart: True/False to force fanart on/off, None to use config default.
        recursive: If True also search subdirectories for M4V files.
    """
    print(f"\n📁 Batch-Modus: {directory}")

    m4v_files = []

    if _is_rclone_path(directory):
        print("   🔎 Suche M4V-Dateien auf Remote...")
        try:
            cmd = ["rclone", "lsf", directory, "--files-only", "--include", "*.m4v"]
            if recursive:
                cmd.append("-R")
            result = run_shell(cmd, return_output=True)
            relative_paths = [line.strip() for line in result.splitlines() if line.strip()]
            m4v_files = [
                f"{directory.rstrip('/')}/{rel}" for rel in relative_paths
            ]
        except Exception as e:
            print(f"   ❌ Fehler beim Auflisten der Remote-Dateien: {e}")
            return
    else:
        if not os.path.isdir(directory):
            print(f"   ❌ Verzeichnis nicht gefunden: {directory}")
            return
        if recursive:
            for root, _dirs, files in os.walk(directory):
                for fname in sorted(files):
                    if fname.lower().endswith(".m4v"):
                        m4v_files.append(os.path.join(root, fname))
        else:
            for fname in sorted(os.listdir(directory)):
                if fname.lower().endswith(".m4v"):
                    m4v_files.append(os.path.join(directory, fname))

    if not m4v_files:
        print("   ⚠️  Keine M4V-Dateien gefunden.")
        return

    print(f"   📊 {len(m4v_files)} M4V-Datei(en) gefunden.")

    success_count = 0
    error_count = 0

    for m4v_path in m4v_files:
        try:
            ok = rebuild_poster(m4v_path, new_frame=new_frame, fanart=fanart)
            if ok:
                success_count += 1
            else:
                error_count += 1
        except Exception as e:
            print(f"   ❌ Unerwarteter Fehler bei {os.path.basename(m4v_path)}: {e}")
            error_count += 1

    total = success_count + error_count
    print(f"\n📊 Batch abgeschlossen: {success_count} ✅ Erfolge, {error_count} ❌ Fehler, {total} Total")
