"""Route plugin actions to archive browser screens."""
from __future__ import annotations

import sys
import traceback
from datetime import date, timedelta
from typing import Any

import xbmc
import xbmcgui
import xbmcplugin

from xbmcaddon import Addon

from .antik_api import AntikApi
from .constants import ADDON_ID
from . import logger
from .listing import add_item, begin_directory, channel_art, end_directory, item_icon
from .kodi_resume import RESUME_THRESHOLD_SECONDS, apply_playback_metadata, get_file_resume
from .archive_marker import clear_archive_playback, begin_archive_session
from .settings_sync import publish_from_addon
from .playback import build_play_item
from .storage import (
    ProgrammeMetaStore,
    SearchHistoryStore,
    VodItemMetaStore,
    VodWatchHistoryStore,
    WatchHistoryStore,
)
from .utils import (
    archive_list_label,
    build_play_url,
    build_url,
    build_vod_play_url,
    vod_item_art,
    vod_item_label,
    vod_package_art,
    history_list_label,
    parse_plugin_params,
    archive_days_from_channel,
    localized_weekday,
    programme_duration_seconds,
    show_label,
    vod_history_list_label,
)


class Router:
    _LABELS = {
        30010: "Antik Archive",
        30011: "Genres",
        30012: "Search",
        30013: "History",
        30014: "Season",
        30016: "Next page",
        30017: "Anonymous access — catalog may be limited",
        30019: "Programme",
        30020: "Stations",
        30021: "No archive channels available",
        30022: "No archived programmes for this day",
        30027: "Middleware URL is not configured",
        30030: "New search",
        30031: "Enter search query",
        30032: "Remove from search history",
        30033: "Remove from history",
        30034: "Removed from search history",
        30035: "Removed from watch history",
        30036: "No search results",
        30037: "No watched programmes yet",
        30060: "VOD",
        30061: "Top movies",
        30062: "No VOD results",
        30063: "VOD not available on middleware",
        30064: "Series",
        30065: "No watched VOD titles yet",
        30066: "VOD services",
        30067: "No VOD services available",
    }

    def __init__(self) -> None:
        self._addon = Addon(ADDON_ID)
        self._handle = int(sys.argv[1])
        self._params = parse_plugin_params()
        self._search_history = SearchHistoryStore(self._addon)
        self._vod_search_history = SearchHistoryStore(self._addon, "vod_search_history.json")
        self._watch_history = WatchHistoryStore(self._addon)
        self._vod_watch_history = VodWatchHistoryStore(self._addon)
        self._programme_meta = ProgrammeMetaStore(self._addon)
        self._vod_item_meta = VodItemMetaStore(self._addon)
        logger.info(f"Run action={self._params.get('action', 'root')} argv={sys.argv}")
        try:
            self._api = AntikApi()
        except ValueError as exc:
            self._api = None
            self._init_error = str(exc).strip() or self._label(30027)
        else:
            self._init_error = None
        publish_from_addon(self._addon)

    def _label(self, string_id: int) -> str:
        text = self._addon.getLocalizedString(string_id)
        if text and not text.isdigit():
            return text
        return self._LABELS.get(string_id, str(string_id))

    def _run_plugin(self, **kwargs: Any) -> str:
        return f"RunPlugin({build_url(**kwargs)})"

    def _slider_setting(self, key: str, default: int) -> int:
        try:
            raw = self._addon.getSetting(key)
            value = int(raw) if raw not in (None, "") else default
        except (TypeError, ValueError, RuntimeError):
            value = default
        return max(0, min(value, 30))

    def _archive_margins(self) -> tuple[int, int]:
        return (
            self._slider_setting("archive_margin_before", 5),
            self._slider_setting("archive_margin_after", 5),
        )

    def run(self) -> None:
        if self._api is None:
            xbmcgui.Dialog().ok(self._label(30010), self._init_error or self._label(30027))
            if self._handle >= 0:
                xbmcplugin.endOfDirectory(self._handle, succeeded=False)
            return

        action = self._params.get("action", "root")

        if action == "play":
            try:
                self._play()
            except Exception as exc:
                logger.error(f"Play failed: {exc}\n{traceback.format_exc()}")
                xbmcgui.Dialog().ok(self._label(30010), str(exc))
                xbmcplugin.setResolvedUrl(self._handle, False, xbmcgui.ListItem())
            return

        if action == "vod_play":
            try:
                self._vod_play()
            except Exception as exc:
                logger.error(f"VOD play failed: {exc}\n{traceback.format_exc()}")
                xbmcgui.Dialog().ok(self._label(30060), str(exc))
                xbmcplugin.setResolvedUrl(self._handle, False, xbmcgui.ListItem())
            return

        if action in (
            "remove_search_history",
            "remove_watch_history",
            "remove_vod_search_history",
            "remove_vod_watch_history",
        ):
            self._handle_remove(action)
            return

        handlers = {
            "root": self._root,
            "stations": self._stations,
            "station_dates": self._station_dates,
            "station_day": self._station_day,
            "genres": self._genres,
            "browse": self._browse,
            "series": self._series,
            "season": self._season,
            "search": self._search,
            "search_new": self._search_new,
            "search_results": self._search_results,
            "history": self._history,
            "vod_root": self._vod_root,
            "vod_packages": self._vod_packages,
            "vod_genres": self._vod_genres,
            "vod_package": self._vod_package,
            "vod_browse": self._vod_browse,
            "vod_top": self._vod_top,
            "vod_search": self._vod_search,
            "vod_search_new": self._vod_search_new,
            "vod_search_results": self._vod_search_results,
            "vod_series": self._vod_series,
            "vod_season": self._vod_season,
            "vod_blocked": self._vod_blocked,
            "vod_history": self._vod_history,
        }
        try:
            handler = handlers.get(action)
            if handler is None:
                raise KeyError(f"Unknown action: {action}")
            handler()
            self._programme_meta.flush()
            self._vod_item_meta.flush()
        except Exception as exc:
            logger.error(f"Action {action} failed: {exc}\n{traceback.format_exc()}")
            xbmcgui.Dialog().ok(self._label(30010), str(exc))
            xbmcplugin.endOfDirectory(self._handle, succeeded=False)

    def _handle_remove(self, action: str) -> None:
        if action == "remove_search_history":
            self._search_history.remove(self._params.get("q", ""))
            message = self._label(30034)
        elif action == "remove_vod_search_history":
            self._vod_search_history.remove(self._params.get("q", ""))
            message = self._label(30034)
        elif action == "remove_vod_watch_history":
            self._vod_watch_history.remove(int(self._params.get("movie_id", 0) or 0))
            message = self._label(30035)
        else:
            self._watch_history.remove(
                self._params.get("channel", ""),
                self._params.get("start", ""),
                self._params.get("stop", ""),
            )
            message = self._label(30035)
        xbmcgui.Dialog().notification(self._label(30010), message, xbmcgui.NOTIFICATION_INFO, 2000)
        if action == "remove_vod_search_history":
            refresh_action = "vod_search"
        elif action == "remove_vod_watch_history":
            refresh_action = "vod_history"
        elif action == "remove_search_history":
            refresh_action = "search"
        else:
            refresh_action = "history"
        self._refresh_listing(refresh_action)

    def _refresh_listing(self, action: str) -> None:
        url = build_url(action=action)
        xbmc.executebuiltin(f"Container.Update({url},replace)")

    def _season_label(self, item: dict[str, Any], season_number: int) -> str:
        title = (item.get("title") or "").strip()
        if title and title.lower() != "item":
            return title
        return f"{self._label(30014)} {season_number}"

    def _station_name(self, item: dict[str, Any], channel_id: str) -> str:
        return (item.get("channel") or "").strip() or self._api.channel_name(channel_id)

    def _programme_context(self, channel: str, start: str, stop: str) -> dict[str, str]:
        meta = self._programme_meta.get(channel, start, stop)
        watched = self._watch_history.get(channel, start, stop)
        return {
            "title": str(meta.get("title") or watched.get("title") or self._label(30019)),
            "station_name": str(
                meta.get("station_name")
                or watched.get("station_name")
                or self._api.channel_name(channel)
            ),
            "series_title": str(meta.get("series_title") or watched.get("series_title") or ""),
            "flow": str(meta.get("flow") or ""),
        }

    def _format_playback_label(
        self,
        title: str,
        station_name: str,
        start: str,
        series_title: str = "",
        flow: str = "",
    ) -> str:
        if flow == "station":
            return show_label(start, title)
        if series_title:
            return history_list_label(title, station_name, start, series_title)
        return archive_list_label(title, station_name, start)

    def _add_playable(
        self,
        label: str,
        channel: str,
        start: str,
        stop: str,
        *,
        title: str = "",
        station_name: str = "",
        series_title: str = "",
        flow: str = "",
        plot: str = "",
        art: dict[str, str] | None = None,
        context_menu: list[tuple[str, str]] | None = None,
    ) -> None:
        self._programme_meta.remember(
            channel,
            start,
            stop,
            title=title,
            station_name=station_name,
            series_title=series_title,
            flow=flow,
        )
        add_item(
            self._handle,
            label,
            build_play_url(channel, start, stop),
            is_folder=False,
            playable=True,
            info={"plot": plot},
            art=art,
            duration_seconds=programme_duration_seconds(start, stop),
            context_menu=context_menu,
        )

    def _root(self) -> None:
        begin_directory(self._handle)
        add_item(self._handle, self._label(30020), build_url(action="stations"))
        add_item(self._handle, self._label(30011), build_url(action="genres"))
        add_item(self._handle, self._label(30012), build_url(action="search"))
        add_item(self._handle, self._label(30013), build_url(action="history"))
        add_item(self._handle, self._label(30060), build_url(action="vod_root"))
        end_directory(self._handle)

    def _vod_available(self) -> bool:
        try:
            return bool(self._api.vod_status().get("available"))
        except Exception:
            return False

    def _vod_root(self) -> None:
        begin_directory(self._handle)
        if not self._vod_available():
            xbmcgui.Dialog().notification(
                self._label(30010),
                self._label(30063),
                xbmcgui.NOTIFICATION_WARNING,
                4000,
            )
        add_item(self._handle, self._label(30066), build_url(action="vod_packages"))
        add_item(self._handle, self._label(30011), build_url(action="vod_genres"))
        add_item(self._handle, self._label(30061), build_url(action="vod_top", offset=0))
        add_item(self._handle, self._label(30012), build_url(action="vod_search"))
        add_item(self._handle, self._label(30013), build_url(action="vod_history"))
        end_directory(self._handle)

    def _vod_packages(self) -> None:
        begin_directory(self._handle)
        packages = self._api.vod_packages()
        if not packages:
            xbmcgui.Dialog().notification(
                self._label(30060),
                self._label(30067),
                xbmcgui.NOTIFICATION_INFO,
                3000,
            )
        for pkg in packages:
            pid = int(pkg.get("id", 0))
            name = str(pkg.get("name") or pid)
            add_item(
                self._handle,
                name,
                build_url(action="vod_package", package_id=pid, package_title=name, offset=0),
                art=vod_package_art(name),
            )
        xbmcplugin.setPluginCategory(self._handle, self._label(30066))
        end_directory(self._handle)

    def _vod_genres(self) -> None:
        begin_directory(self._handle)
        for genre in self._api.vod_genres():
            gid = int(genre.get("id", 0))
            name = genre.get("name", str(gid))
            add_item(
                self._handle,
                name,
                build_url(action="vod_browse", genre=gid, genre_title=name, offset=0),
            )
        xbmcplugin.setPluginCategory(self._handle, self._label(30011))
        end_directory(self._handle)

    def _vod_package(self) -> None:
        begin_directory(self._handle)
        package_id = int(self._params["package_id"])
        offset = int(self._params.get("offset", 0))
        data = self._api.vod_package(package_id, offset=offset)
        package_name = self._params.get("package_title") or str(package_id)
        xbmcplugin.setPluginCategory(self._handle, f"{self._label(30060)}: {package_name}")
        self._render_vod_items(
            data,
            next_action="vod_package",
            package_id=package_id,
            package_title=package_name,
            offset=offset,
        )

    def _vod_browse(self) -> None:
        begin_directory(self._handle)
        genre = int(self._params.get("genre", 0))
        offset = int(self._params.get("offset", 0))
        data = self._api.vod_browse(genre, offset=offset)
        genre_name = self._params.get("genre_title") or str(genre)
        xbmcplugin.setPluginCategory(self._handle, f"{self._label(30060)}: {genre_name}")
        self._render_vod_items(
            data,
            next_action="vod_browse",
            genre=genre,
            genre_title=genre_name,
            offset=offset,
        )

    def _vod_top(self) -> None:
        begin_directory(self._handle)
        offset = int(self._params.get("offset", 0))
        data = self._api.vod_top(offset=offset)
        self._render_vod_items(data, next_action="vod_top", offset=offset)

    def _vod_search(self) -> None:
        begin_directory(self._handle)
        add_item(self._handle, self._label(30030), build_url(action="vod_search_new"))
        for query in self._vod_search_history.list():
            add_item(
                self._handle,
                query,
                build_url(action="vod_search_results", q=query, offset=0),
                context_menu=[
                    (
                        self._label(30032),
                        self._run_plugin(action="remove_vod_search_history", q=query),
                    )
                ],
            )
        xbmcplugin.setPluginCategory(self._handle, self._label(30012))
        end_directory(self._handle)

    def _vod_search_new(self) -> None:
        keyboard = xbmc.Keyboard("", self._label(30031))
        keyboard.doModal()
        if not keyboard.isConfirmed():
            xbmcplugin.endOfDirectory(self._handle, succeeded=False)
            return
        query = keyboard.getText().strip()
        if not query:
            xbmcplugin.endOfDirectory(self._handle, succeeded=False)
            return
        self._vod_search_history.touch(query)
        self._show_vod_search_results(query, 0)

    def _vod_search_results(self) -> None:
        query = self._params.get("q", "").strip()
        if not query:
            xbmcplugin.endOfDirectory(self._handle, succeeded=False)
            return
        self._vod_search_history.touch(query)
        offset = int(self._params.get("offset", 0))
        self._show_vod_search_results(query, offset)

    def _show_vod_search_results(self, query: str, offset: int) -> None:
        begin_directory(self._handle)
        data = self._api.vod_search(query, offset=offset)
        items = data.get("data", [])
        if not items and offset == 0:
            xbmcgui.Dialog().notification(
                self._label(30060),
                self._label(30062),
                xbmcgui.NOTIFICATION_INFO,
                3000,
            )
        self._render_vod_items(data, next_action="vod_search_results", q=query, offset=offset)
        xbmcplugin.setPluginCategory(self._handle, f"{self._label(30012)}: {query}")

    def _vod_series(self) -> None:
        begin_directory(self._handle)
        series_id = int(self._params["series_id"])
        series_title = self._params.get("series_title", "")
        offset = int(self._params.get("offset", 0))
        data = self._api.vod_series(series_id, offset=offset)
        if data.get("browse_level") == "episodes":
            xbmcplugin.setPluginCategory(
                self._handle,
                f"{self._label(30064)}: {series_title or series_id}",
            )
            self._render_vod_items(
                data,
                next_action="vod_season",
                series_id=series_id,
                series_title=series_title,
                season_id=int(data.get("season_id") or 0),
                offset=offset,
                playable_only=True,
            )
            return
        xbmcplugin.setPluginCategory(
            self._handle,
            f"{self._label(30064)}: {series_title or series_id}",
        )
        self._render_vod_items(
            data,
            next_action="vod_series",
            series_id=series_id,
            series_title=series_title,
            offset=offset,
            series_mode=True,
        )

    def _vod_season(self) -> None:
        begin_directory(self._handle)
        series_id = int(self._params["series_id"])
        series_title = self._params.get("series_title", "")
        season_id = int(self._params["season_id"])
        offset = int(self._params.get("offset", 0))
        data = self._api.vod_season(series_id, season_id, offset=offset)
        xbmcplugin.setPluginCategory(
            self._handle,
            f"{series_title or series_id} — {self._label(30014)} {season_id}",
        )
        self._render_vod_items(
            data,
            next_action="vod_season",
            series_id=series_id,
            series_title=series_title,
            season_id=season_id,
            offset=offset,
            playable_only=True,
        )

    def _vod_item_type(self, item: dict[str, Any], series_mode: bool, playable_only: bool) -> str:
        series_main = item.get("series_main")
        if series_main is not None and int(series_main) not in (-1, 0):
            return "series"
        parent = item.get("series_parent_id")
        season = item.get("series_season")
        if parent is not None and int(parent) != -1 and season is not None and int(season) != -1:
            return "season-link"
        if series_mode and not playable_only:
            return "season"
        return "movie"

    @staticmethod
    def _vod_season_id(item: dict[str, Any]) -> int:
        season = item.get("series_season")
        if season is not None and int(season) != -1:
            return int(season)
        return int(item["id"])

    def _render_vod_items(
        self,
        data: dict[str, Any],
        next_action: str,
        offset: int,
        series_mode: bool = False,
        playable_only: bool = False,
        **extra: Any,
    ) -> None:
        items = data.get("data", [])
        total = int(data.get("count", 0))
        middleware_base = self._api.base_url
        for raw in items:
            item = raw if isinstance(raw, dict) else {}
            title = vod_item_label(item)
            raw_title = str(item.get("title") or item.get("display_title") or "Item")
            art = vod_item_art(item, data, middleware_base=middleware_base, api=self._api)
            item_type = item.get("item_type") or self._vod_item_type(item, series_mode, playable_only)
            if item_type == "series":
                sid = int(item["id"])
                add_item(
                    self._handle,
                    title,
                    build_url(action="vod_series", series_id=sid, series_title=raw_title, offset=0),
                    art=art,
                )
            elif item_type in ("season", "season-link"):
                sid = int(item.get("series_parent_id") or extra.get("series_id", 0))
                season_id = self._vod_season_id(item)
                add_item(
                    self._handle,
                    title,
                    build_url(
                        action="vod_season",
                        series_id=sid,
                        season_id=season_id,
                        series_title=extra.get("series_title", ""),
                        offset=0,
                    ),
                    art=art,
                )
            else:
                movie_id = int(item["id"])
                plot = item.get("description") or ""
                playable = item.get("playable", True) is not False
                restriction = str(item.get("play_restriction") or "").strip()
                if not playable:
                    add_item(
                        self._handle,
                        f"{title} — {restriction or 'not playable'}",
                        build_url(action="vod_blocked", msg=restriction or "not playable"),
                        art=art,
                    )
                    continue
                self._add_vod_playable(
                    movie_id,
                    title,
                    title=raw_title,
                    display_title=str(item.get("display_title") or raw_title),
                    series_title=str(extra.get("series_title") or "").strip(),
                    meta=str(item.get("meta") or "").strip(),
                    year=str(item.get("year") or "").strip(),
                    plot=plot,
                    art=art,
                )
        next_offset = offset + len(items)
        if next_offset < total:
            page_extra = {**extra, "offset": next_offset}
            add_item(
                self._handle,
                self._label(30016),
                build_url(action=next_action, **page_extra),
            )
        end_directory(self._handle)

    def _vod_blocked(self) -> None:
        xbmcgui.Dialog().ok(
            self._label(30060),
            self._params.get("msg") or self._label(30063),
        )
        xbmcplugin.endOfDirectory(self._handle, succeeded=False)

    def _vod_context(self, movie_id: int) -> dict[str, str]:
        meta = self._vod_item_meta.get(movie_id)
        watched = self._vod_watch_history.get(movie_id)
        return {
            "title": str(meta.get("title") or watched.get("title") or self._label(30019)),
            "display_title": str(
                meta.get("display_title")
                or watched.get("display_title")
                or meta.get("title")
                or watched.get("title")
                or self._label(30019)
            ),
            "series_title": str(meta.get("series_title") or watched.get("series_title") or ""),
            "meta": str(meta.get("meta") or watched.get("meta") or ""),
            "year": str(meta.get("year") or watched.get("year") or ""),
        }

    def _add_vod_playable(
        self,
        movie_id: int,
        label: str,
        *,
        title: str = "",
        display_title: str = "",
        series_title: str = "",
        meta: str = "",
        year: str = "",
        plot: str = "",
        art: dict[str, str] | None = None,
        context_menu: list[tuple[str, str]] | None = None,
    ) -> None:
        raw_title = (title or display_title or label).strip() or self._label(30019)
        self._vod_item_meta.remember(
            movie_id,
            title=raw_title,
            display_title=(display_title or raw_title).strip(),
            series_title=series_title,
            meta=meta,
            year=year,
        )
        add_item(
            self._handle,
            label,
            build_vod_play_url(movie_id, title=raw_title),
            is_folder=False,
            playable=True,
            info={"plot": plot, "title": raw_title},
            art=art,
            context_menu=context_menu,
        )

    def _vod_history(self) -> None:
        begin_directory(self._handle)
        entries = self._vod_watch_history.list()
        if not entries:
            xbmcgui.Dialog().notification(
                self._label(30060),
                self._label(30065),
                xbmcgui.NOTIFICATION_INFO,
                3000,
            )
        middleware_base = self._api.base_url
        for entry in entries:
            if not isinstance(entry, dict):
                continue
            try:
                movie_id = int(entry.get("movie_id") or 0)
            except (TypeError, ValueError):
                continue
            if movie_id <= 0:
                continue
            title = str(entry.get("display_title") or entry.get("title") or self._label(30019))
            series_title = str(entry.get("series_title") or "").strip()
            meta = str(entry.get("meta") or "").strip()
            year = str(entry.get("year") or "").strip()
            label = vod_history_list_label(title, series_title, meta, year)
            art = vod_item_art({"id": movie_id}, middleware_base=middleware_base, api=self._api)
            self._add_vod_playable(
                movie_id,
                label,
                title=str(entry.get("title") or title),
                display_title=title,
                series_title=series_title,
                meta=meta,
                year=year,
                art=art,
                context_menu=[
                    (
                        self._label(30033),
                        self._run_plugin(
                            action="remove_vod_watch_history",
                            movie_id=movie_id,
                        ),
                    )
                ],
            )
        xbmcplugin.setPluginCategory(self._handle, f"{self._label(30060)}: {self._label(30013)}")
        end_directory(self._handle)

    def _vod_play(self) -> None:
        movie_id = int(self._params["movie_id"])
        ctx = self._vod_context(movie_id)
        title = self._params.get("title", "") or ctx["display_title"] or self._label(30019)
        play_url = build_vod_play_url(movie_id, title=title)
        user_resume, _ = get_file_resume(play_url)
        resume_seconds = user_resume if user_resume >= RESUME_THRESHOLD_SECONDS else 0.0
        info = self._api.vod_play_info(movie_id)
        detail = info.get("detail") or {}
        if not title or title == self._label(30019):
            title = str(detail.get("title") or ctx["title"] or title)
        play = info.get("play") or {}
        merged = {
            "streamUrl": play.get("url", ""),
            "licenseUrl": "",
            "seekMs": 0,
        }
        playlist = self._api.vod_playlist_url(movie_id)
        duration_sec = int(detail.get("duration") or 0)
        auth_headers, auth_suffix = self._api.playback_auth()
        item = build_play_item(
            merged,
            playlist,
            title=title,
            resume_seconds=resume_seconds,
            programme_duration_sec=duration_sec,
            auth_headers=auth_headers,
            auth_suffix=auth_suffix,
        )
        art = vod_item_art(
            detail if isinstance(detail, dict) else {},
            info,
            middleware_base=self._api.base_url,
            api=self._api,
        )
        if any(art.values()):
            item.setArt(art)
        plot = str(detail.get("description") or "").strip()
        if plot:
            item.setInfo("video", {"plot": plot, "title": title})
        apply_playback_metadata(item, play_url, title, duration_sec, plot=plot)
        clear_archive_playback()
        self._vod_watch_history.upsert(
            {
                "movie_id": movie_id,
                "title": str(detail.get("title") or ctx["title"] or title),
                "display_title": ctx["display_title"] or title,
                "series_title": ctx["series_title"],
                "meta": ctx["meta"],
                "year": str(detail.get("year") or ctx["year"] or "").strip(),
            }
        )
        xbmcplugin.setResolvedUrl(self._handle, True, item)

    def _search(self) -> None:
        begin_directory(self._handle)
        add_item(
            self._handle,
            self._label(30030),
            build_url(action="search_new"),
        )
        for query in self._search_history.list():
            add_item(
                self._handle,
                query,
                build_url(action="search_results", q=query, offset=0),
                context_menu=[
                    (
                        self._label(30032),
                        self._run_plugin(action="remove_search_history", q=query),
                    )
                ],
            )
        xbmcplugin.setPluginCategory(self._handle, self._label(30012))
        end_directory(self._handle)

    def _search_new(self) -> None:
        keyboard = xbmc.Keyboard("", self._label(30031))
        keyboard.doModal()
        if not keyboard.isConfirmed():
            xbmcplugin.endOfDirectory(self._handle, succeeded=False)
            return
        query = keyboard.getText().strip()
        if not query:
            xbmcplugin.endOfDirectory(self._handle, succeeded=False)
            return
        self._search_history.touch(query)
        self._show_search_results(query, 0)

    def _search_results(self) -> None:
        query = self._params.get("q", "").strip()
        if not query:
            xbmcplugin.endOfDirectory(self._handle, succeeded=False)
            return
        self._search_history.touch(query)
        offset = int(self._params.get("offset", 0))
        self._show_search_results(query, offset)

    def _show_search_results(self, query: str, offset: int) -> None:
        begin_directory(self._handle)
        data = self._api.search(query, offset=offset)
        items = data.get("items", [])
        if not items and offset == 0:
            xbmcgui.Dialog().notification(
                self._label(30010),
                self._label(30036),
                xbmcgui.NOTIFICATION_INFO,
                3000,
            )
        self._render_items(data, next_action="search_results", q=query, offset=offset)
        xbmcplugin.setPluginCategory(self._handle, f"{self._label(30012)}: {query}")

    def _history(self) -> None:
        begin_directory(self._handle)
        entries = self._watch_history.list()
        if not entries:
            xbmcgui.Dialog().notification(
                self._label(30010),
                self._label(30037),
                xbmcgui.NOTIFICATION_INFO,
                3000,
            )
        for entry in entries:
            if not isinstance(entry, dict):
                continue
            channel = str(entry.get("channel") or "").strip()
            start = str(entry.get("start") or "").strip()
            stop = str(entry.get("stop") or "").strip()
            if not channel or not start or not stop:
                continue
            title = str(entry.get("title") or "") or self._label(30019)
            station_name = str(entry.get("station_name") or "").strip() or self._api.channel_name(channel)
            series_title = str(entry.get("series_title") or "").strip()
            label = history_list_label(title, station_name, start, series_title)
            self._add_playable(
                label,
                channel,
                start,
                stop,
                title=title,
                station_name=station_name,
                series_title=series_title,
                context_menu=[
                    (
                        self._label(30033),
                        self._run_plugin(
                            action="remove_watch_history",
                            channel=channel,
                            start=start,
                            stop=stop,
                        ),
                    )
                ],
            )
        xbmcplugin.setPluginCategory(self._handle, self._label(30013))
        end_directory(self._handle)

    def _stations(self) -> None:
        begin_directory(self._handle)
        channels = self._api.archive_channels()
        if not channels:
            xbmcgui.Dialog().notification(
                self._label(30010),
                self._label(30021),
                xbmcgui.NOTIFICATION_INFO,
                3000,
            )
        for ch in channels:
            channel_id = ch.get("id_content") or ch.get("id") or ""
            name = ch.get("name") or channel_id
            days = archive_days_from_channel(ch)
            if days <= 0 and ch.get("archive"):
                days = 7
            if days <= 0:
                continue
            add_item(
                self._handle,
                name,
                build_url(action="station_dates", channel=channel_id, name=name, days=days),
                art=channel_art(ch),
            )
        end_directory(self._handle)

    def _station_dates(self) -> None:
        begin_directory(self._handle)
        channel_id = self._params["channel"]
        name = self._params.get("name", channel_id)
        try:
            url_days = int(self._params.get("days", 0) or 0)
        except (TypeError, ValueError):
            url_days = 0
        days = self._api.archive_days_for_channel(channel_id, fallback=url_days)
        if days <= 0:
            days = 7
        days = max(1, min(days, 90))
        today = date.today()
        for offset in range(days):
            day = today - timedelta(days=offset)
            day_str = day.isoformat()
            label = f"{day_str} {localized_weekday(day, self._addon)}"
            add_item(
                self._handle,
                label,
                build_url(
                    action="station_day",
                    channel=channel_id,
                    date=day_str,
                    name=name,
                ),
            )
        xbmcplugin.setPluginCategory(self._handle, name)
        end_directory(self._handle)

    def _station_day(self) -> None:
        begin_directory(self._handle)
        channel_id = self._params["channel"]
        day = self._params["date"]
        name = self._params.get("name", channel_id)
        programmes = sorted(
            (
                prog
                for prog in self._api.epg_for_day(channel_id, day)
                if prog.get("archived")
            ),
            key=lambda prog: prog.get("start", ""),
        )
        if not programmes:
            xbmcgui.Dialog().notification(
                self._label(30010),
                self._label(30022),
                xbmcgui.NOTIFICATION_INFO,
                3000,
            )
        for prog in programmes:
            start = str(prog.get("start") or "").strip()
            stop = str(prog.get("stop") or "").strip()
            if not start or not stop:
                continue
            title = prog.get("title") or self._label(30019)
            label = show_label(start, title)
            art = item_icon(prog)
            plot = prog.get("desc") or prog.get("description") or ""
            self._add_playable(
                label,
                channel_id,
                start,
                stop,
                title=title,
                station_name=name,
                flow="station",
                plot=plot,
                art=art,
            )
        xbmcplugin.setPluginCategory(self._handle, f"{name} — {day}")
        end_directory(self._handle)

    def _genres(self) -> None:
        begin_directory(self._handle)
        for genre in self._api.genres():
            add_item(
                self._handle,
                genre.get("title", genre.get("id", "?")),
                build_url(action="browse", genre=genre["id"], offset=0),
            )
        end_directory(self._handle)

    def _browse(self) -> None:
        begin_directory(self._handle)
        genre = self._params["genre"]
        offset = int(self._params.get("offset", 0))
        data = self._api.browse(genre, offset=offset)
        self._render_items(data, next_action="browse", genre=genre, offset=offset)

    def _series(self) -> None:
        begin_directory(self._handle)
        series_id = int(self._params["series_id"])
        series_title = self._params.get("series_title", "")
        offset = int(self._params.get("offset", 0))
        data = self._api.series(series_id, offset=offset)
        self._render_items(
            data,
            next_action="series",
            series_id=series_id,
            series_title=series_title,
            offset=offset,
            series_mode=True,
        )

    def _season(self) -> None:
        begin_directory(self._handle)
        series_id = int(self._params["series_id"])
        series_title = self._params.get("series_title", "")
        season = int(self._params["season"])
        offset = int(self._params.get("offset", 0))
        data = self._api.season(series_id, season, offset=offset)
        self._render_items(
            data,
            next_action="season",
            series_id=series_id,
            series_title=series_title,
            season=season,
            offset=offset,
            playable_only=True,
        )

    def _render_items(
        self,
        data: dict[str, Any],
        next_action: str,
        offset: int,
        series_mode: bool = False,
        playable_only: bool = False,
        **extra: Any,
    ) -> None:
        items = data.get("items", [])
        total = int(data.get("total_count", 0))
        for raw in items:
            item = raw if isinstance(raw, dict) else {}
            title = item.get("title", "Item")
            art = item_icon(item)
            if item.get("type_series") and not playable_only and not series_mode:
                sid = item["type_series"]["id"]
                add_item(
                    self._handle,
                    title,
                    build_url(action="series", series_id=sid, series_title=title, offset=0),
                    art=art,
                )
            elif item.get("type_season") and not playable_only:
                sn = item["type_season"].get("season_number", 0)
                sid = extra.get("series_id") or item.get("type_series", {}).get("id", 0)
                series_title = extra.get("series_title", "")
                season_title = self._season_label(item, sn)
                add_item(
                    self._handle,
                    season_title,
                    build_url(
                        action="season",
                        series_id=sid,
                        season=sn,
                        series_title=series_title,
                        offset=0,
                    ),
                    art=art,
                )
            else:
                channel = item.get("channel_id_content") or item.get("channel", "")
                start = str(item.get("start") or "").strip()
                stop = str(item.get("stop") or "").strip()
                if channel and start and stop:
                    prog_title = (item.get("title") or "").strip() or self._label(30019)
                    station_name = self._station_name(item, channel)
                    series_title = extra.get("series_title", "")
                    label = archive_list_label(prog_title, station_name, start)
                    plot = item.get("description") or ""
                    self._add_playable(
                        label,
                        channel,
                        start,
                        stop,
                        title=prog_title,
                        station_name=station_name,
                        series_title=series_title,
                        plot=plot,
                        art=art,
                    )
        next_offset = offset + len(items)
        if next_offset < total:
            page_extra = {**extra, "offset": next_offset}
            add_item(
                self._handle,
                self._label(30016),
                build_url(action=next_action, **page_extra),
            )
        end_directory(self._handle)

    def _play(self) -> None:
        channel = self._params["channel"]
        start = self._params["start"]
        stop = self._params["stop"]
        play_url = build_play_url(channel, start, stop)
        ctx = self._programme_context(channel, start, stop)
        display_label = self._format_playback_label(
            ctx["title"],
            ctx["station_name"],
            start,
            ctx["series_title"],
            ctx["flow"],
        )
        duration = programme_duration_seconds(start, stop)
        margin_before, margin_after = self._archive_margins()
        user_resume, _ = get_file_resume(play_url)
        resume_seconds = user_resume if user_resume >= RESUME_THRESHOLD_SECONDS else 0.0
        logger.info(
            f"Resolve play channel={channel} from={start} to={stop} "
            f"margins={margin_before}/{margin_after}min resume={resume_seconds:.0f}s"
        )
        info = self._api.play_info(channel, start, stop, margin_before, margin_after)
        if resume_seconds > 0:
            info = {**info, "seekMs": 0}
        else:
            seek_ms = int(info.get("seekMs") or info.get("seek_ms") or 0)
            margin_seek_ms = margin_before * 60_000
            if margin_before > 0 and seek_ms < margin_seek_ms:
                seek_ms = margin_seek_ms
            if seek_ms > 0:
                info = {**info, "seekMs": seek_ms}
        playlist = self._api.archive_playlist_url(
            channel,
            start,
            stop,
            margin_before,
            margin_after,
            start_offset_ms=0 if resume_seconds > 0 else None,
        )
        auth_headers, auth_suffix = self._api.playback_auth()
        item = build_play_item(
            info,
            playlist,
            title=display_label,
            resume_seconds=resume_seconds,
            programme_duration_sec=duration,
            auth_headers=auth_headers,
            auth_suffix=auth_suffix,
        )
        apply_playback_metadata(item, play_url, display_label, duration)
        begin_archive_session(self._api.hls_playlist_url(playlist))
        self._watch_history.upsert(
            {
                "channel": channel,
                "start": start,
                "stop": stop,
                "title": ctx["title"],
                "station_name": ctx["station_name"],
                "series_title": ctx["series_title"],
            }
        )
        xbmcplugin.setResolvedUrl(self._handle, True, item)
