from __future__ import annotations
import asyncio
import os
import re
import threading
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional, Tuple

from tmdb import (
    TmdbCastMember,
    fetch_tv_episode_credits,
    lookup_movie,
    lookup_tv_season,
    lookup_tv_season_summary,
    lookup_tv_show,
)

try:
    import xbmc
    import xbmcaddon
    import xbmcvfs
except ImportError:
    xbmc = None
    xbmcaddon = None
    xbmcvfs = None

# Caches
_TMDB_CACHE: dict[str, tuple[dict[str, str], dict[str, str]]] = {}
_TMDB_CAST_CACHE: dict[str, list[TmdbCastMember]] = {}
_TMDB_SEASON_CACHE: dict[tuple[int, int, str, str], dict[int, tuple[dict[str, str], dict[str, str]]]] = {}
_TMDB_SEASON_SUMMARY_CACHE: dict[tuple[int, int, str, str], tuple[dict[str, str], dict[str, str]]] = {}
_TMDB_EPISODE_CAST_CACHE: dict[tuple[int, int, int, str], list[TmdbCastMember]] = {}
_TMDB_ID_CACHE: dict[str, int] = {}
_TMDB_LOG_PATH: str | None = None
_TMDB_LOCK = threading.RLock()


# Dependency Injection variables
_initialized: bool = False
_get_setting_string: Callable[[str], str] = lambda k: ""
_get_setting_bool: Callable[[str, bool], bool] = lambda k, default=False: default
_get_setting_int: Callable[[str, int], int] = lambda k, default=0: default
_log: Callable[[str, int], None] = lambda msg, level=0: None
_run_async: Callable[[Any], Any] = lambda coro: None
_extract_first_int: Callable[[str], Optional[int]] = lambda val: None


def _require_init() -> None:
    """Gibt eine Warnung aus, wenn metadata.init() noch nicht aufgerufen wurde."""
    if not _initialized:
        import sys
        print("[ViewIT/metadata] WARNUNG: metadata.init() wurde nicht aufgerufen – Metadaten-Funktionen arbeiten mit Standardwerten!", file=sys.stderr)


def init(
    *,
    get_setting_string: Callable[[str], str],
    get_setting_bool: Callable[..., bool],
    get_setting_int: Callable[..., int],
    log_fn: Callable[[str, int], None],
    run_async_fn: Callable[[Any], Any],
    extract_first_int_fn: Callable[[str], Optional[int]],
) -> None:
    global _initialized, _get_setting_string, _get_setting_bool, _get_setting_int, _log, _run_async, _extract_first_int
    _get_setting_string = get_setting_string
    _get_setting_bool = get_setting_bool
    _get_setting_int = get_setting_int
    _log = log_fn
    _run_async = run_async_fn
    _extract_first_int = extract_first_int_fn
    _initialized = True


def _get_log_path(filename: str) -> str:
    if xbmcaddon and xbmcvfs:
        addon = xbmcaddon.Addon()
        profile = xbmcvfs.translatePath(addon.getAddonInfo("profile"))
        log_dir = os.path.join(profile, "logs")
        if not xbmcvfs.exists(log_dir):
            xbmcvfs.mkdirs(log_dir)
        return os.path.join(log_dir, filename)
    return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), filename)


def tmdb_file_log(message: str) -> None:
    global _TMDB_LOG_PATH
    if _TMDB_LOG_PATH is None:
        _TMDB_LOG_PATH = _get_log_path("tmdb.log")
    timestamp = datetime.utcnow().isoformat(timespec="seconds") + "Z"
    line = f"{timestamp}\t{message}\n"
    try:
        with open(_TMDB_LOG_PATH, "a", encoding="utf-8") as handle:
            handle.write(line)
    except Exception:
        if xbmcvfs is None:
            return
        try:
            handle = xbmcvfs.File(_TMDB_LOG_PATH, "a") # type: ignore
            handle.write(line) # type: ignore
            handle.close() # type: ignore
        except Exception:
            return


def tmdb_cache_get(cache: dict, key, default=None):
    with _TMDB_LOCK:
        return cache.get(key, default)


def tmdb_cache_set(cache: dict, key, value) -> None:
    with _TMDB_LOCK:
        cache[key] = value


def tmdb_prefetch_concurrency() -> int:
    try:
        raw = _get_setting_string("tmdb_prefetch_concurrency").strip()
        value = int(raw) if raw else 6
    except Exception:
        value = 6
    return max(1, min(20, value))


def tmdb_enabled() -> bool:
    _require_init()
    return _get_setting_bool("tmdb_enabled", default=True)


def tmdb_list_enabled() -> bool:
    return tmdb_enabled() and _get_setting_bool("tmdb_genre_metadata", default=False)


def tmdb_labels_and_art(title: str) -> tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]:
    if not tmdb_enabled():
        return {}, {}, []
    title_key = (title or "").strip().casefold()
    language = _get_setting_string("tmdb_language").strip() or "de-DE"
    show_plot = _get_setting_bool("tmdb_show_plot", default=True)
    show_art = _get_setting_bool("tmdb_show_art", default=True)
    show_fanart = _get_setting_bool("tmdb_show_fanart", default=True)
    show_rating = _get_setting_bool("tmdb_show_rating", default=True)
    show_votes = _get_setting_bool("tmdb_show_votes", default=False)
    show_cast = _get_setting_bool("tmdb_show_cast", default=False)
    flags = f"p{int(show_plot)}a{int(show_art)}f{int(show_fanart)}r{int(show_rating)}v{int(show_votes)}c{int(show_cast)}"
    cache_key = f"{language}|{flags}|{title_key}"
    cached = tmdb_cache_get(_TMDB_CACHE, cache_key)
    if cached is not None:
        info, art = cached
        cast_cached = tmdb_cache_get(_TMDB_CAST_CACHE, cache_key, [])
        return info, art, list(cast_cached)

    info_labels: dict[str, str] = {"title": title}
    art: dict[str, str] = {}
    cast: list[TmdbCastMember] = []
    query = (title or "").strip()
    api_key = _get_setting_string("tmdb_api_key").strip()
    log_requests = _get_setting_bool("tmdb_log_requests", default=False)
    log_responses = _get_setting_bool("tmdb_log_responses", default=False)
    if api_key:
        try:
            log_fn = tmdb_file_log if (log_requests or log_responses) else None
            candidates: list[str] = []
            if query:
                candidates.append(query)
                simplified = re.sub(r"\s*[-–]\s*der\s+film\s*$", "", query, flags=re.IGNORECASE).strip()
                if simplified and simplified not in candidates:
                    candidates.append(simplified)

            meta = None
            is_tv = False
            for candidate in candidates:
                meta = lookup_tv_show(
                    title=candidate,
                    api_key=api_key,
                    language=language,
                    log=log_fn,
                    log_responses=log_responses,
                    include_cast=show_cast,
                )
                if meta:
                    is_tv = True
                    break
            if not meta:
                for candidate in candidates:
                    movie = lookup_movie(
                        title=candidate,
                        api_key=api_key,
                        language=language,
                        log=log_fn,
                        log_responses=log_responses,
                        include_cast=show_cast,
                    )
                    if movie:
                        meta = movie
                        break
        except Exception as exc:
            try:
                tmdb_file_log(f"TMDB ERROR lookup_failed title={title!r} error={exc!r}")
            except Exception:
                pass
            _log(f"TMDB Meta fehlgeschlagen: {exc}", 1) # LOGWARNING/LOGDEBUG fallback
            meta = None
        if meta:
            if is_tv:
                tmdb_cache_set(_TMDB_ID_CACHE, title_key, int(getattr(meta, "tmdb_id", 0) or 0))
                info_labels.setdefault("mediatype", "tvshow")
            else:
                info_labels.setdefault("mediatype", "movie")
            if show_plot and getattr(meta, "plot", ""):
                info_labels["plot"] = getattr(meta, "plot", "")
            runtime_minutes = int(getattr(meta, "runtime_minutes", 0) or 0)
            if runtime_minutes > 0 and not is_tv:
                info_labels["duration"] = str(runtime_minutes * 60)
            rating = getattr(meta, "rating", 0.0) or 0.0
            votes = getattr(meta, "votes", 0) or 0
            if show_rating and rating:
                info_labels["rating"] = str(rating)
            if show_votes and votes:
                info_labels["votes"] = str(votes)
            if show_art and getattr(meta, "poster", ""):
                poster = getattr(meta, "poster", "")
                art.update({"thumb": poster, "poster": poster, "icon": poster})
            if show_fanart and getattr(meta, "fanart", ""):
                fanart = getattr(meta, "fanart", "")
                if fanart:
                    art.update({"fanart": fanart, "landscape": fanart})
            if show_cast:
                cast = list(getattr(meta, "cast", []) or [])
        elif log_requests or log_responses:
            tmdb_file_log(f"TMDB MISS title={title!r}")

    tmdb_cache_set(_TMDB_CACHE, cache_key, (info_labels, art))
    tmdb_cache_set(_TMDB_CAST_CACHE, cache_key, list(cast))
    return info_labels, art, list(cast)


async def _tmdb_labels_and_art_bulk_async(
    titles: list[str],
) -> dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]]:
    titles = [str(t).strip() for t in (titles or []) if t and str(t).strip()]
    if not titles:
        return {}

    unique_titles: list[str] = list(dict.fromkeys(titles))
    limit = tmdb_prefetch_concurrency()
    semaphore = asyncio.Semaphore(limit)

    async def fetch_one(title: str):
        async with semaphore:
            return title, await asyncio.to_thread(tmdb_labels_and_art, title)

    tasks = [fetch_one(title) for title in unique_titles]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    mapped: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {}
    for entry in results:
        if isinstance(entry, Exception):
            continue
        try:
            title, payload = entry
        except Exception:
            continue
        if isinstance(title, str) and isinstance(payload, tuple) and len(payload) == 3:
            mapped[title] = payload  # type: ignore[assignment]
    return mapped


def tmdb_labels_and_art_bulk(
    titles: list[str],
) -> dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]]:
    if not tmdb_enabled():
        return {}
    return _run_async(_tmdb_labels_and_art_bulk_async(titles))


def tmdb_episode_labels_and_art(*, title: str, season_label: str, episode_label: str) -> tuple[dict[str, str], dict[str, str]]:
    if not tmdb_enabled():
        return {"title": episode_label}, {}
    title_key = (title or "").strip().casefold()
    tmdb_id = tmdb_cache_get(_TMDB_ID_CACHE, title_key)
    if not tmdb_id:
        tmdb_labels_and_art(title)
        tmdb_id = tmdb_cache_get(_TMDB_ID_CACHE, title_key)
    if not tmdb_id:
        return {"title": episode_label}, {}

    season_number = _extract_first_int(season_label)
    episode_number = _extract_first_int(episode_label)
    if season_number is None or episode_number is None:
        return {"title": episode_label}, {}

    language = _get_setting_string("tmdb_language").strip() or "de-DE"
    show_plot = _get_setting_bool("tmdb_show_plot", default=True)
    show_art = _get_setting_bool("tmdb_show_art", default=True)
    flags = f"p{int(show_plot)}a{int(show_art)}"
    season_key = (tmdb_id, season_number, language, flags)
    cached_season = tmdb_cache_get(_TMDB_SEASON_CACHE, season_key)
    if cached_season is None:
        api_key = _get_setting_string("tmdb_api_key").strip()
        if not api_key:
            return {"title": episode_label}, {}
        log_requests = _get_setting_bool("tmdb_log_requests", default=False)
        log_responses = _get_setting_bool("tmdb_log_responses", default=False)
        log_fn = tmdb_file_log if (log_requests or log_responses) else None
        try:
            season_meta = lookup_tv_season(
                tmdb_id=tmdb_id,
                season_number=season_number,
                api_key=api_key,
                language=language,
                log=log_fn,
                log_responses=log_responses,
            )
        except Exception as exc:
            if log_fn:
                log_fn(f"TMDB ERROR season_lookup_failed tmdb_id={tmdb_id} season={season_number} error={exc!r}")
            season_meta = None
        mapped: dict[int, tuple[dict[str, str], dict[str, str]]] = {}
        if season_meta:
            for ep_no, ep in season_meta.items():
                info: dict[str, str] = {"title": f"Episode {ep_no}"}
                if show_plot and ep.plot:
                    info["plot"] = ep.plot
                if getattr(ep, "runtime_minutes", 0):
                    info["duration"] = str(int(getattr(ep, "runtime_minutes", 0)) * 60)
                art: dict[str, str] = {}
                if show_art and ep.thumb:
                    art = {"thumb": ep.thumb}
                mapped[ep_no] = (info, art)
        tmdb_cache_set(_TMDB_SEASON_CACHE, season_key, mapped)
        cached_season = mapped

    return cached_season.get(episode_number, ({"title": episode_label}, {}))


def tmdb_episode_cast(*, title: str, season_label: str, episode_label: str) -> list[TmdbCastMember]:
    if not tmdb_enabled():
        return []
    show_episode_cast = _get_setting_bool("tmdb_show_episode_cast", default=False)
    if not show_episode_cast:
        return []

    title_key = (title or "").strip().casefold()
    tmdb_id = tmdb_cache_get(_TMDB_ID_CACHE, title_key)
    if not tmdb_id:
        tmdb_labels_and_art(title)
        tmdb_id = tmdb_cache_get(_TMDB_ID_CACHE, title_key)
    if not tmdb_id:
        return []

    season_number = _extract_first_int(season_label)
    episode_number = _extract_first_int(episode_label)
    if season_number is None or episode_number is None:
        return []

    language = _get_setting_string("tmdb_language").strip() or "de-DE"
    cache_key = (tmdb_id, season_number, episode_number, language)
    cached = tmdb_cache_get(_TMDB_EPISODE_CAST_CACHE, cache_key)
    if cached is not None:
        return list(cached)

    api_key = _get_setting_string("tmdb_api_key").strip()
    if not api_key:
        tmdb_cache_set(_TMDB_EPISODE_CAST_CACHE, cache_key, [])
        return []

    log_requests = _get_setting_bool("tmdb_log_requests", default=False)
    log_responses = _get_setting_bool("tmdb_log_responses", default=False)
    log_fn = tmdb_file_log if (log_requests or log_responses) else None
    try:
        cast = fetch_tv_episode_credits(
            tmdb_id=tmdb_id,
            season_number=season_number,
            episode_number=episode_number,
            api_key=api_key,
            language=language,
            log=log_fn,
            log_responses=log_responses,
        )
    except Exception as exc:
        if log_fn:
            log_fn(
                f"TMDB ERROR episode_credits_failed tmdb_id={tmdb_id} season={season_number} episode={episode_number} error={exc!r}"
            )
        cast = []
    tmdb_cache_set(_TMDB_EPISODE_CAST_CACHE, cache_key, list(cast))
    return list(cast)


def tmdb_season_labels_and_art(
    *,
    title: str,
    season: str,
    title_info_labels: dict[str, str] | None = None,
) -> tuple[dict[str, str], dict[str, str]]:
    if not tmdb_enabled():
        return {"title": season}, {}
    language = _get_setting_string("tmdb_language").strip() or "de-DE"
    show_plot = _get_setting_bool("tmdb_show_plot", default=True)
    show_art = _get_setting_bool("tmdb_show_art", default=True)
    flags = f"p{int(show_plot)}a{int(show_art)}"
    api_key = _get_setting_string("tmdb_api_key").strip()
    log_requests = _get_setting_bool("tmdb_log_requests", default=False)
    log_responses = _get_setting_bool("tmdb_log_responses", default=False)
    log_fn = tmdb_file_log if (log_requests or log_responses) else None

    info_labels: dict[str, str] | None = None
    art: dict[str, str] | None = None
    season_number = _extract_first_int(season)

    if api_key and season_number is not None:
        title_key = (title or "").strip().casefold()
        tmdb_id = tmdb_cache_get(_TMDB_ID_CACHE, title_key) or 0
        cache_key = (tmdb_id, season_number, language, flags)
        cached = tmdb_cache_get(_TMDB_SEASON_SUMMARY_CACHE, cache_key)

        if cached is None and tmdb_id:
            try:
                meta = lookup_tv_season_summary(
                    tmdb_id=tmdb_id,
                    season_number=season_number,
                    api_key=api_key,
                    language=language,
                    log=log_fn,
                    log_responses=log_responses,
                )
            except Exception as exc:
                if log_fn:
                    log_fn(f"TMDB ERROR season_summary_failed tmdb_id={tmdb_id} season={season_number} error={exc!r}")
                meta = None
            labels = {"title": season}
            art_map: dict[str, str] = {}
            if meta:
                if show_plot and meta.plot:
                    labels["plot"] = meta.plot
                if show_art and meta.poster:
                    art_map = {"thumb": meta.poster, "poster": meta.poster}
            cached = (labels, art_map)
            tmdb_cache_set(_TMDB_SEASON_SUMMARY_CACHE, cache_key, cached)

        if cached is not None:
            info_labels, art = cached

    merged_labels = dict(info_labels or {})
    if title_info_labels:
        merged_labels = dict(title_info_labels)
        merged_labels.update(dict(info_labels or {}))

    return merged_labels, art or {}
