"""Hlavní routing pluginu — orchestrace akcí, kořenové menu."""

import json
import os
import sys

import xbmc
import xbmcaddon
import xbmcgui
import xbmcplugin
import xbmcvfs
import shutil

from resources.lib.core.router_state import (
    ADDON_ID,
    ADDON,
    ADDON_NAME,
    _url,
    _handle,
    _addon,
    _profile,
    log,
    get_url,
    popinfo,
    parse_qsl,
    _ADDON_DATA_SIZE_CACHE_TTL_SEC,
)
from resources.lib.core.router_domains import (
    _ensure_router_session_hooks,
    _update_progress_view,
    _resolve_trakt_id_and_call,
    _validate_streams_action,
    _rescan_invalid_action,
    _trakt_auth_with_status,
    _trakt_refresh_status,
)

try:
    from xbmc import translatePath
except ImportError:
    from xbmcvfs import translatePath

# Webshare / EPG: načtou se až při prvním přístupu k atributu (PEP 562 __getattr__)
# nebo při sestavení _ROUTER_ACTIONS_CACHE — ne při samotném importu routeru.
_LAZY_FROM_WEBSHARE = frozenset(
    {
        'api',
        'revalidate',
        'CATEGORIES',
        'SORTS',
        'NONE_WHAT',
        'history',
        'info',
        'play',
        '_vip_start_popup_if_needed',
    }
)
_LAZY_FROM_EPG = frozenset({'ensure_epg_fresh', '_get_tv_program'})

_ROUTER_ACTIONS_CACHE = None


def __getattr__(name):
    if name in _LAZY_FROM_WEBSHARE:
        from resources.lib.core import router_webshare as _rw

        obj = getattr(_rw, name)
        globals()[name] = obj
        return obj
    if name in _LAZY_FROM_EPG:
        from resources.lib.core import router_epg as _re

        obj = getattr(_re, name)
        globals()[name] = obj
        return obj
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def cloud_sync_now(params, addon):
    from resources.lib.utils.cloud_sync import CloudSync

    cs = CloudSync(addon)
    cs.sync_all()

    xbmcgui.Dialog().notification(
        "Cloud Sync",
        "Synchronizace dokončena",
        xbmcgui.NOTIFICATION_INFO,
        3000,
    )


def _tmdb():
    return __import__('resources.lib.tmdb.tmdb_series_detail', fromlist=['*'])


def ask(what):
    if what is None:
        what = ''
    kb = xbmc.Keyboard(what, 'Zadejte hledaný výraz')
    kb.doModal()
    if kb.isConfirmed():
        return kb.getText()
    return None


def settings(params):
    _addon.openSettings()


def delete_history_item(tmdb_id, media_type):
    from resources.lib.history import local_history

    if local_history.remove_item(tmdb_id, media_type):
        xbmcgui.Dialog().notification(
            ADDON_NAME,
            "Položka smazána z historie.",
            xbmcgui.NOTIFICATION_INFO,
        )
    xbmc.executebuiltin('Container.Refresh()')


def _probe_history_url(url, timeout=5.0):
    """Rychlé ověření dostupnosti URL pro historický záznam.

    Zkusí HEAD, při 405/501 spadne na Range GET (bytes=0-0). Žádná výjimka
    nevybublá ven – při chybě/timeoutu vrací (False, None). Pokud není k
    dispozici modul `requests`, probe se přeskočí (vrací True, None).

    Returns: (alive: bool, status: int | None)
    """
    try:
        import requests
    except Exception:
        return True, None
    ua = 'Mozilla/5.0 (Kodi plugin.video.mmirousek_v3)'
    headers = {'User-Agent': ua}
    try:
        resp = requests.head(url, allow_redirects=True, timeout=timeout, headers=headers)
        status = resp.status_code
        if status in (405, 501):
            resp = requests.get(
                url,
                stream=True,
                timeout=timeout,
                headers={**headers, 'Range': 'bytes=0-0'},
            )
            status = resp.status_code
            try:
                resp.close()
            except Exception:
                pass
        return (status < 400), status
    except Exception as e:
        log.warning(f"[PlayHistory] probe failed: {e}")
        return False, None


def play_history_item(url, tmdb_id, media_type):
    from resources.lib.history import local_history

    if not url:
        xbmcgui.Dialog().notification(
            ADDON_NAME,
            "Nelze přehrát: Chybí stream URL.",
            xbmcgui.NOTIFICATION_ERROR,
        )
        return

    full_item = local_history.get_single_item(tmdb_id, media_type)

    alive, status = _probe_history_url(url)
    log.info(
        f"[PlayHistory] probe tmdb_id={tmdb_id} alive={alive} "
        f"status={status} url={(url or '')[:80]}"
    )
    if not alive:
        title_for_search = ''
        if full_item:
            title_for_search = (
                full_item.get('title')
                or full_item.get('name')
                or ((full_item.get('trakt_payload') or {}).get('movie') or {}).get('title')
                or ''
            )
        msg = "Odkaz na stream vypršel nebo nefunguje."
        if status:
            msg += f"\n(HTTP {status})"
        msg += "\nVyhledat nový stream na Webshare?"
        do_search = xbmcgui.Dialog().yesno(
            ADDON_NAME, msg, yeslabel="Hledat", nolabel="Zrušit"
        )
        xbmcplugin.setResolvedUrl(_handle, False, xbmcgui.ListItem())
        if do_search and title_for_search:
            xbmc.executebuiltin(
                f"Container.Update({get_url(action='search', q=title_for_search, tmdb_id=tmdb_id, mediatype=(media_type or 'movie'))})"
            )
        elif do_search:
            xbmc.executebuiltin(f"Container.Update({get_url(action='search')})")
        return

    if not full_item:
        log.warning(
            f"Položka nenalezena: tmdb_id={tmdb_id}, media_type={media_type}"
        )
    else:
        try:
            addon = xbmcaddon.Addon(ADDON_ID)
            payload = full_item.get('trakt_payload', {})
            if payload:
                addon.setSetting('trakt_last_payload', json.dumps(payload))
                addon.setSetting('trakt_payload_locked', 'true')
                progress = full_item.get('progress', 0.0)
                addon.setSetting('trakt_last_progress', str(progress))
                log.info(
                    f"Scrobbling připraven: "
                    f"{full_item.get('name', 'Neznámý')} ({progress}%)"
                )
            else:
                log.warning(
                    "Položka nemá trakt_payload – scrobbling nebude fungovat."
                )
        except Exception as e:
            log.error(f"Chyba při přípravě scrobblingu: {e}")

    play_item = xbmcgui.ListItem(path=url)
    play_item.setProperty('IsPlayable', 'true')
    xbmcplugin.setResolvedUrl(_handle, True, play_item)


def csfd_play_or_open(params):
    media_type = (params.get('media_type') or '').lower()
    title = params.get('title') or ''
    year = None
    try:
        if params.get('year'):
            year = int(params.get('year'))
    except Exception:
        year = None
    tmdb_id = int(params.get('tmdb_id') or 0)

    if media_type == 'tv':
        xbmc.executebuiltin(
            f"Container.Update({get_url(action='series_search', what=title)})"
        )
        return

    m3u_path = ADDON.getSetting('playlist_path') or ''
    if not m3u_path:
        xbmcgui.Dialog().notification(
            ADDON_NAME,
            "Chybí cesta k M3U v nastavení.",
            xbmcgui.NOTIFICATION_WARNING,
        )
        xbmc.executebuiltin(
            f"Container.Update({get_url(action='search', what=title)})"
        )
        return

    from resources.lib.playlists import playlist_index

    idx = playlist_index.load_index_json()
    if not idx or playlist_index.needs_rebuild(m3u_path, idx):
        idx = playlist_index.rebuild_if_needed(
            m3u_path, is_tv=False, force=True, show_progress=False
        )
    if not idx:
        if xbmcgui.Dialog().yesno(
            ADDON_NAME,
            "Index playlistu nenalezen. Hledat na Webshare?",
        ):
            xbmc.executebuiltin(
                f"Container.Update({get_url(action='search', what=title)})"
            )
        return

    best = playlist_index.find_best_match(
        idx, tmdb_id=tmdb_id, title=title, year=year
    )
    if best and (best.get('m3u_url') or ''):
        from resources.lib.playlists import playlist_loader

        playlist_loader.play_m3u_stream(best['m3u_url'], handle=_handle)
        return

    if xbmcgui.Dialog().yesno(
        ADDON_NAME,
        f"„{title}“ v playlistu nenalezen.\nHledat na Webshare?",
    ):
        xbmc.executebuiltin(
            f"Container.Update({get_url(action='search', what=title)})"
        )
    else:
        xbmcplugin.endOfDirectory(_handle, succeeded=False)


def _translate_profile_path():
    try:
        return xbmcvfs.translatePath(ADDON.getAddonInfo('profile'))
    except Exception:
        try:
            from xbmc import translatePath as xbmc_translate

            return xbmc_translate(ADDON.getAddonInfo('profile'))
        except Exception:
            return ADDON.getAddonInfo('profile')


def get_storage_info():
    try:
        profile_path = _translate_profile_path()
        usage = shutil.disk_usage(profile_path)
        free_gb = round(usage.free / (1024**3), 2)
        total_gb = round(usage.total / (1024**3), 2)
        return free_gb, total_gb
    except Exception as e:
        log.warning(f"disk_usage failed: {e}")
        return 0.0, 0.0


def _size_os_walk(root_path: str) -> int:
    try:
        total = 0
        for r, _dnames, fnames in os.walk(root_path):
            for fn in fnames:
                fp = os.path.join(r, fn)
                try:
                    total += os.path.getsize(fp)
                except Exception:
                    pass
        return total
    except Exception as e:
        log.info(f"os.walk failed: {e}")
        return -1


def _size_vfs_recursive(root_path: str) -> int:
    total = 0
    try:
        dirs, files = xbmcvfs.listdir(root_path)
        for f in files:
            fp = os.path.join(root_path, f)
            try:
                st = xbmcvfs.Stat(fp)
                if hasattr(st, "st_size"):
                    size_attr = getattr(st, "st_size")
                    size = int(size_attr() if callable(size_attr) else size_attr)
                else:
                    size = os.path.getsize(fp)
                total += max(size, 0)
            except Exception:
                try:
                    total += os.path.getsize(fp)
                except Exception:
                    pass
        for d in dirs:
            sub = os.path.join(root_path, d)
            total += _size_vfs_recursive(sub)
    except Exception as e:
        log.warning(f"vfs recursion failed at {root_path}: {e}")
    return total


def get_addon_data_size_mb():
    """Velikost profilu doplňku v MB; výsledek je cachovaný v okně 10000 (TTL)."""
    import time

    try:
        win = xbmcgui.Window(10000)
        now = time.time()
        ts_prop = win.getProperty('mm_addon_data_mb_ts')
        val_prop = win.getProperty('mm_addon_data_mb_cached')
        if ts_prop and val_prop:
            try:
                age = now - float(ts_prop)
                if 0 <= age < _ADDON_DATA_SIZE_CACHE_TTL_SEC:
                    return float(val_prop)
            except (ValueError, TypeError):
                pass

        profile_path = _translate_profile_path()
        total_bytes = _size_os_walk(profile_path)
        if total_bytes < 0:
            total_bytes = _size_vfs_recursive(profile_path)
        mb = round(total_bytes / (1024 * 1024), 1)
        try:
            win.setProperty('mm_addon_data_mb_ts', str(now))
            win.setProperty('mm_addon_data_mb_cached', str(mb))
        except Exception:
            pass
        return mb
    except Exception as e:
        log.error(f"failed: {e}")
        return None


def _filter_log_lines(lines, limit=100):
    result = []
    for line in lines:
        is_ours = 'SM2-' in line
        is_our_error = (
            ('ERROR' in line or 'EXCEPTION' in line)
            and 'mmirousek' in line
        )
        if is_ours or is_our_error:
            result.append(line)
    return result[-limit:]


def _show_kodi_log():
    try:
        log_path = xbmcvfs.translatePath('special://logpath/kodi.log')
        if not xbmcvfs.exists(log_path):
            xbmcgui.Dialog().ok(
                "Chyba", "Soubor s logy (kodi.log) nebyl nalezen."
            )
            return

        with xbmcvfs.File(log_path, 'r') as f:
            lines = f.read().splitlines()

        filtered = _filter_log_lines(lines, limit=100)
        content = (
            "\n".join(filtered)
            if filtered
            else "Žádné relevantní záznamy v logu."
        )
        xbmcgui.Dialog().textviewer(
            "Log doplňku — posledních 100 řádků (SM2- + ERRORy)",
            content,
        )
    except Exception as e:
        log.error(f"Nepodařilo se zobrazit log: {e}", exc_info=True)
        xbmcgui.Dialog().ok(
            "Chyba", f"Nepodařilo se zobrazit log: {e}"
        )


def _save_kodi_log():
    try:
        log_path = xbmcvfs.translatePath('special://logpath/kodi.log')
        if not xbmcvfs.exists(log_path):
            xbmcgui.Dialog().ok(
                "Chyba", "Soubor s logy (kodi.log) nebyl nalezen."
            )
            return

        with xbmcvfs.File(log_path, 'r') as f:
            lines = f.read().splitlines()

        filtered = _filter_log_lines(lines, limit=100)
        content = "\n".join(filtered) if filtered else "(prázdné)"

        save_path = xbmcvfs.translatePath('special://profile/sm2_log.txt')
        with xbmcvfs.File(save_path, 'w') as out:
            out.write(content)

        upload_ok = False
        upload_remote = None
        try:
            from resources.lib.utils.cloud_sync import upload_support_log
            upload_ok, upload_remote = upload_support_log(save_path)
        except Exception as up_ex:
            log.warning(f"Support log FTP upload exception: {up_ex}")

        xbmcgui.Dialog().ok(
            "Log uložen",
            f"Log byl uložen do:\n{save_path}\n\nObsahuje {len(filtered)} záznamů.",
        )
        if upload_ok:
            xbmcgui.Dialog().notification(
                ADDON_NAME,
                f"Log nahrán na FTP: {upload_remote}",
                xbmcgui.NOTIFICATION_INFO,
                3000,
            )
        else:
            xbmcgui.Dialog().notification(
                ADDON_NAME,
                "Log uložen lokálně, ale upload na FTP selhal.",
                xbmcgui.NOTIFICATION_WARNING,
                3500,
            )
    except Exception as e:
        log.error(f"Nepodařilo se uložit log: {e}", exc_info=True)
        xbmcgui.Dialog().ok(
            "Chyba", f"Nepodařilo se uložit log: {e}"
        )


def _background_enrich_ratings():
    try:
        from resources.lib.utils.background_enrich import (
            background_enrich_omdb_ratings,
            background_enrich_tmdb_titles,
        )

        log.info("Spouštím background enrich...")
        background_enrich_tmdb_titles()
        background_enrich_omdb_ratings()
        xbmcgui.Dialog().notification(
            ADDON_NAME,
            "Doplňování hodnocení dokončeno.",
            xbmcgui.NOTIFICATION_INFO,
        )
    except ImportError as e:
        log.warning(f"Background enrich import selhal: {e}")
        xbmcgui.Dialog().notification(
            ADDON_NAME,
            "Chyba při importu background enrich.",
            xbmcgui.NOTIFICATION_ERROR,
        )


def _clear_menu_cache_ui():
    from resources.lib.cache.menu_cache_server import get_menu_cache_server

    count = get_menu_cache_server().invalidate_menu()
    xbmcgui.Dialog().notification(
        'Menu Cache',
        f'Vymazáno {count} položek z cache.',
        xbmcgui.NOTIFICATION_INFO,
        2500,
    )
    xbmc.executebuiltin('Container.Refresh')


def _offline_scan_now_ui():
    """Ručně vynutí offline scan – projde `dfolder`, aktualizuje tabulku
    `offline_cache` a zobrazí souhrn. Zapisuje podrobný log (viz značky
    [DEBUG-SCAN] v offline_scanner.py)."""
    try:
        from resources.lib.utils.offline_scanner import run_offline_scan
        from resources.lib.database.metadata_db import db as _db
        from resources.lib.down.offline_cache_utils import resolve_dfolder_path
    except Exception as e:
        log.error(f"offline_scan_now import error: {e}")
        xbmcgui.Dialog().notification(
            'Offline scan',
            f'Chyba importu: {e}',
            xbmcgui.NOTIFICATION_ERROR,
            3000,
        )
        return

    dlg = xbmcgui.DialogProgressBG()
    try:
        dlg.create('Offline scan', 'Procházím složky stažených seriálů…')
        dlg.update(10)
        run_offline_scan(force=True)
        dlg.update(90, 'Načítám výsledek z DB…')
        cache_all = _db.get_offline_cache_all() or {}
        total_series = sum(1 for v in cache_all.values() if v)
        total_eps = sum(len(v) for v in cache_all.values())
        scanned_root = resolve_dfolder_path(ADDON) or '(nenastaveno)'
    except Exception as e:
        log.error(f"offline_scan_now run error: {e}")
        try:
            dlg.close()
        except Exception:
            pass
        xbmcgui.Dialog().notification(
            'Offline scan',
            f'Chyba: {e}',
            xbmcgui.NOTIFICATION_ERROR,
            3000,
        )
        return
    finally:
        try:
            dlg.close()
        except Exception:
            pass

    msg = f'Hotovo: {total_series} seriálů, {total_eps} epizod v cache.'
    if total_eps == 0:
        msg += f'\nSložka: {scanned_root}'
    xbmcgui.Dialog().notification(
        'Offline scan',
        msg,
        xbmcgui.NOTIFICATION_INFO,
        4500 if total_eps == 0 else 3500,
    )
    xbmc.executebuiltin('Container.Refresh')


def _subcaps_clear_all_ui():
    if not xbmcgui.Dialog().yesno(
        'Subcaps cache',
        'Opravdu vymazat všechny záznamy cache ffprobe / titulků (subcaps_cache.json)?',
        nolabel='Zpět',
        yeslabel='Vymazat',
    ):
        return
    from resources.lib.series import subcaps_cache

    subcaps_cache.clear_all()
    xbmcgui.Dialog().notification(
        'Subcaps cache',
        'Celá cache byla vymazána.',
        xbmcgui.NOTIFICATION_INFO,
        2500,
    )
    xbmc.executebuiltin('Container.Refresh')


def _series_streams_filter_select_action(series_name, season, current_filter):
    """Dialog pro výběr filtru v `series_streams_list` + Container.Update.

    Po výběru se aktuální kontejner přepíše URL nesoucí nový ``filter_mode``;
    `replace` zamezí nahromadění historie filtrů v navigaci zpět.
    """
    from urllib.parse import urlencode
    options = [
        ('all', 'Vše'),
        ('cz_dab', 'jen CZdab'),
        ('cz_tit', 'jen CZtit'),
        ('cz_any', 'CZdab nebo CZtit'),
    ]
    keys = [k for k, _ in options]
    labels = [lbl for _, lbl in options]
    cur = (current_filter or 'all').lower()
    if cur == 'cz_both':
        cur = 'cz_any'
    try:
        preselect = keys.index(cur)
    except ValueError:
        preselect = 0
    idx = xbmcgui.Dialog().select('Filtr streamů', labels, preselect=preselect)
    if idx is None or idx < 0:
        return
    new_filter = keys[idx]

    qs = {
        'action': 'series_streams_list',
        'series_name': series_name or '',
        'filter_mode': new_filter,
    }
    if season:
        qs['season'] = season
    try:
        import sys
        base = sys.argv[0]
    except Exception:
        base = 'plugin://plugin.video.mmirousek_v3/'
    url = f"{base}?{urlencode(qs, encoding='utf-8')}"
    xbmc.executebuiltin(f'Container.Update({url},replace)')


def router(paramstring):
    global _ROUTER_ACTIONS_CACHE

    import time as _time_router

    _t_router = _time_router.perf_counter()
    _ensure_router_session_hooks()
    params = dict(parse_qsl(paramstring)) if paramstring else {}
    action = (params.get('action') or '').strip()

    log.debug(f"in: action={action} params={params}")

    def _safe_int(v, default=0):
        try:
            return int(v)
        except Exception:
            return default

    def _optional_int_param(p, key, min_value=1):
        """Z params[key] vrátí int >= min_value, jinak None (prázdné / nečíslo). Sezóna: min_value=0."""
        v = p.get(key)
        if v is None:
            return None
        if isinstance(v, str) and not str(v).strip():
            return None
        try:
            i = int(v)
            return i if i >= min_value else None
        except (ValueError, TypeError):
            return None

    def _mark_skip_full_fetch():
        try:
            xbmcgui.Window(10000).setProperty('mm_skip_full_fetch', '1')
            log.info("set flag mm_skip_full_fetch=1")
        except Exception as e:
            log.warning(f"skip_full_fetch flag error: {e}")

    def _set_full_fetch_cooldown(seconds=300):
        try:
            import time

            until = time.time() + float(seconds)
            xbmcgui.Window(10000).setProperty(
                'mm_full_fetch_cooldown_until', str(until)
            )
            log.info(f"full-fetch cooldown set: {int(seconds)}s")
        except Exception as e:
            log.warning(f"cooldown set error: {e}")

    def _delete_history_prompt_and_refresh(p):
        try:
            from resources.lib.trakt.trakt_module import trakt_delete_history

            kind = (p.get('kind') or '').lower()
            trakt_id = _safe_int(p.get('trakt_id'), 0)
            season_num = p.get('season')

            if (
                kind == 'season'
                and (season_num is None or not str(season_num).isdigit())
            ):
                kb = xbmc.Keyboard('', 'Číslo sezóny pro smazání')
                kb.doModal()
                if not kb.isConfirmed():
                    return
                season_txt = (kb.getText() or '').strip()
                if not season_txt.isdigit():
                    xbmcgui.Dialog().notification(
                        'Trakt',
                        'Zadejte číslo sezóny (jen čísla).',
                        xbmcgui.NOTIFICATION_WARNING,
                    )
                    return
                season_num = season_txt

            trakt_delete_history(
                kind=kind,
                trakt_id=trakt_id,
                season=int(season_num) if season_num else None,
            )
            _mark_skip_full_fetch()
            _set_full_fetch_cooldown(seconds=300)
            _update_progress_view()
        except Exception as e:
            log.error(f"trakt_delete_history error: {e}")
            xbmcgui.Dialog().notification(
                'Trakt',
                'Chyba mazání historie.',
                xbmcgui.NOTIFICATION_ERROR,
                3000,
            )

    def _download_whole_show(p):
        try:
            from resources.lib.series import series_manager as sm_mod
            from resources.lib.down.offline_manager import save_whole_show

            sm = sm_mod.SeriesManager(_addon, _profile)
            save_whole_show(p.get('series_name', ''), sm)
        except Exception as e:
            log.error(f"download_whole_show error: {e}")
            xbmcgui.Dialog().notification(
                ADDON_NAME,
                "Chyba stahování.",
                xbmcgui.NOTIFICATION_ERROR,
                3000,
            )

    def _show_watch_history_list_entry(p):
        try:
            from resources.lib.history import local_history as lh_mod

            media_type = p.get('type') or 'movie'
            items = lh_mod.get_list_items(media_type=media_type)
            if not items:
                xbmcgui.Dialog().notification(
                    _addon.getAddonInfo('name'),
                    "Historie je prázdná.",
                    xbmcgui.NOTIFICATION_INFO,
                    2500,
                )
                xbmcplugin.endOfDirectory(_handle, succeeded=True)
                return
            xbmcplugin.addDirectoryItems(_handle, items)
            xbmcplugin.endOfDirectory(_handle)
        except Exception as e:
            log.error(f"watch_history_list error: {e}")
            xbmcgui.Dialog().notification(
                _addon.getAddonInfo('name'),
                "Chyba při načítání historie.",
                xbmcgui.NOTIFICATION_ERROR,
                3000,
            )
            xbmcplugin.endOfDirectory(_handle, succeeded=False)

    def _play_m3u(m3u_url):
        try:
            from resources.lib.playlists import playlist_loader

            try:
                playlist_loader.play_m3u_stream(m3u_url, handle=_handle)
            except TypeError:
                playlist_loader.play_m3u_stream(m3u_url)
        except Exception as e:
            log.error(f"play_m3u_stream error: {e}")
            xbmcgui.Dialog().notification(
                _addon.getAddonInfo('name'),
                "Chyba M3U streamu.",
                xbmcgui.NOTIFICATION_ERROR,
                3000,
            )

    ALIASES = {
        'trak_movies_hub': 'trakt_movies_hub',
        'trakt_movie_hub': 'trakt_movies_hub',
        'trak_shows_hub': 'trakt_shows_hub',
        'trak_show_hub': 'trakt_shows_hub',
        'trak_lists_hub': 'trakt_lists_hub',
        'csfd_play_m3u_stream': 'play_m3u_stream',
        'trak_show_next_last': 'trakt_show_next_last',
        'trakt_next_last': 'trakt_show_next_last',
        'series_trakt_mark_season_prompt': 'trakt_prompt_mark_season',
        'series_trakt_mark_episode_prompt': 'trakt_prompt_mark_episode',
        'series_trakt_mark_show_prompt': 'trakt_prompt_mark_season',
    }
    action = ALIASES.get(action, action)

    if not action:
        import time as _time_root

        _t_root = _time_root.perf_counter()
        from resources.lib.utils.ui_utils import pick_icon

        xbmcplugin.setPluginCategory(
            _handle, _addon.getAddonInfo('name')
        )
        try:
            win = xbmcgui.Window(10000)
            if win.getProperty('mm_scrobble_service_started') != '1':
                addon_path = xbmcvfs.translatePath(
                    _addon.getAddonInfo('path')
                )
                script_path = os.path.join(addon_path, 'service.py')
                # Bez wait: při běžícím service z guardu skript hned skončí;
                # neblokujeme vykreslení menu (ARCHITECTURE bod 3).
                xbmc.executebuiltin(f'RunScript("{script_path}")')
                win.setProperty('mm_scrobble_service_started', '1')
                log.info('service.py spuštěn při vstupu do doplňku')
        except Exception as e:
            log.warning(f'start služby selhal: {e}')

        from resources.lib.utils.ui_utils import add_menu_item

        free_gb, total_gb, addon_mb = "?", "?", "?"
        try:
            free_gb, total_gb = get_storage_info()
        except Exception:
            pass
        try:
            addon_mb = get_addon_data_size_mb()
        except Exception:
            pass
        target_info = ""
        dfolder = ADDON.getSetting('dfolder') or ''
        if dfolder:
            try:
                target_real = xbmcvfs.translatePath(dfolder)
                if target_real:
                    if not target_real.endswith(('/', '\\')):
                        target_real += '/'
                    is_local = (
                        target_real[0].isalpha()
                        and target_real[1:3] == ':\\'
                        or target_real.startswith('/')
                        or target_real.startswith('/storage/')
                        or target_real.startswith('/sdcard/')
                    )
                    if is_local:
                        try:
                            usage = shutil.disk_usage(target_real)
                            free_t = round(
                                usage.free / (1024**3), 2
                            )
                            total_t = round(
                                usage.total / (1024**3), 2
                            )
                            target_info = (
                                " • [COLOR orange]"
                                f"Stahování: {free_t}/{total_t} GB volno"
                                "[/COLOR]"
                            )
                        except Exception:
                            target_info = (
                                " • [COLOR orange]"
                                "Stahování: nelze zjistit místo"
                                "[/COLOR]"
                            )
                    else:
                        short = '/'.join(
                            target_real.rstrip('/').split('/')[-2:]
                        )
                        target_info = (
                            " • [COLOR orange]"
                            f"Stahování: {short} [síťový disk]"
                            "[/COLOR]"
                        )
            except Exception as e:
                log.warning(
                    f"Chyba při zpracování dfolder: {e}"
                )
                target_info = (
                    " • [COLOR red]Stahování: chyba[/COLOR]"
                )
        else:
            target_info = (
                " • [COLOR gray]Stahování: nenastaveno[/COLOR]"
            )
        addon_mb_txt = (
            addon_mb if addon_mb is not None else '?'
        )
        label_info = (
            f"[COLOR green]Úložiště: {free_gb}/{total_gb} GB "
            f"• Data: {addon_mb_txt} MB[/COLOR]{target_info}"
        )
        li_info = xbmcgui.ListItem(label=label_info)
        li_info.setArt({'icon': 'DefaultHardDisk.png'})
        xbmcplugin.addDirectoryItem(
            _handle, get_url(action='refresh_main'), li_info, isFolder=False
        )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='Vyhledávání',
            plot=(
                "Vyhledávání souborů na Webshare. \n"
                "Ukládá dotazy do historie pro rychlé opakování."
            ),
            action='search',
            art_icon=pick_icon(
                _addon, 'search.png', 'DefaultAddonsSearch.png'
            ),
            is_folder=True,
        )
        from resources.lib.down.download_queue import (
            get_queue_summary,
            get_queue_stats,
        )

        summary = get_queue_summary()
        downloading_count = summary.get("downloading", 0)
        pending_count = summary.get("pending", 0)
        total_count = summary.get("total", 0)
        try:
            (
                pending_c,
                downloading_c,
                total_gb,
                remaining_gb,
                eta_min,
            ) = get_queue_stats(avg_speed_mbps=10.0)
        except Exception:
            total_gb, remaining_gb, eta_min = 0, 0, None
        label = "Fronta stahování"
        if downloading_count > 0 or pending_count > 0:
            if downloading_count > 0:
                prefix = "[COLOR lime][Stahuje se][/COLOR] "
            else:
                prefix = "[COLOR orange][Čeká ve frontě][/COLOR] "

            status_tag = (
                f"[COLOR lime]↓ stahuje: {downloading_count}[/COLOR] • "
                f"[COLOR gray]ve frontě: {pending_count}[/COLOR]"
            )
            if remaining_gb > 0:
                status_tag += (
                    f" • [COLOR yellow]zbývá: {remaining_gb} GB[/COLOR]"
                )
            if eta_min:
                status_tag += (
                    f" • [COLOR orange]ETA: {eta_min} min[/COLOR]"
                )
            label = f"{prefix}Fronta stahování • {status_tag}"
        plot = (
            f"Celkem položek: {total_count} • "
            f"Stahuje: {downloading_count} • "
            f"Čeká: {pending_count}"
        )
        if total_gb > 0:
            plot += f" • Celkem: {total_gb} GB"
        if remaining_gb > 0:
            plot += f" • Zbývá: {remaining_gb} GB"
        if eta_min:
            plot += f" • Odhad dokončení: {eta_min} min"
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label=label,
            plot=plot,
            action='queue_menu',
            art_icon=pick_icon(
                _addon, 'download_queue.png', 'DefaultHardDisk.png'
            ),
            is_folder=True,
        )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='Seriály',
            plot=(
                "Správce seriálů:\n"
                "• Vyhledá a setřídí epizody do sezón\n"
                "• Nabízí aktualizaci a detailní navigaci\n"
                "• Možnost stahování do zvolené složky\n"
                "• Přehrávání pak probíhá offline\n"
            ),
            action='series',
            art_icon=pick_icon(
                _addon, 'series.png', 'DefaultTvShowTitle.png'
            ),
            is_folder=True,
        )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='Trakt kalendář (30 dní)',
            plot=(
                'Zobrazí přehled nadcházejících epizod '
                'z Trakt.tv na 30 dní dopředu.'
            ),
            action='trakt_calendar',
            art_icon=pick_icon(
                _addon, 'trakt.png', 'DefaultPicture.png'
            ),
            is_folder=True,
        )
        try:
            if ADDON.getSettingBool('experimental') or ADDON.getSettingBool('author_mode_enabled'):
                add_menu_item(
                    handle=_handle,
                    build_url_fn=get_url,
                    label='Nástroje a údržba',
                    plot=(
                        'Záloha nastavení, logy, cache, TMDb audit, offline scan '
                        'a další podpůrné nástroje.'
                    ),
                    action='exp_menu',
                    art_icon=pick_icon(
                        _addon,
                        'settings_experimental.png',
                        'DefaultAddonService.png',
                    ),
                    is_folder=True,
                )
        except Exception:
            if (
                (ADDON.getSetting('experimental') or '').lower() == 'true'
                or (ADDON.getSetting('author_mode_enabled') or '').lower() == 'true'
            ):
                add_menu_item(
                    handle=_handle,
                    build_url_fn=get_url,
                    label='Nástroje a údržba',
                    plot=(
                        'Záloha nastavení, logy, cache, TMDb audit, offline scan '
                        'a další podpůrné nástroje.'
                    ),
                    action='exp_menu',
                    art_icon=pick_icon(
                        _addon,
                        'settings_experimental.png',
                        'DefaultAddonService.png',
                    ),
                    is_folder=True,
                )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='Filmy',
            plot=(
                "Sekce Filmy: přístup k historii sledování "
                "a staženým filmům."
            ),
            action='movies_menu',
            art_icon=pick_icon(
                _addon, 'filmy.png', 'DefaultMovieTitle.png'
            ),
            is_folder=True,
        )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='TMDb databáze',
            plot=(
                "Procházení a vyhledávání filmů/seriálů "
                "v TheMovieDatabase. Vybraný film hledá "
                "na Webshare, seriál do Správce seriálů."
            ),
            action='tmdb',
            art_icon=pick_icon(
                _addon, 'tmdb.png', 'DefaultPicture.png'
            ),
            is_folder=True,
        )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='Trakt.tv',
            plot=(
                "Procházení Trakt: trendy, populární, kalendář, "
                "veřejné seznamy a filtry. Integrovaný scrobbling "
                "při přehrávání."
            ),
            action='trakt_menu',
            art_icon=pick_icon(
                _addon, 'trakt.png', 'DefaultPicture.png'
            ),
            is_folder=True,
        )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='ČSFD TOP 300',
            plot=(
                "Stažení a procházení TOP 300 filmů a seriálů "
                "dle ČSFD, včetně cache detailů a filtrů podle země."
            ),
            action='csfd_toplist',
            art_icon=pick_icon(
                _addon, 'csfd.png', 'DefaultStar.png'
            ),
            is_folder=True,
        )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='TV Program',
            plot=(
                "Elektronický program (EPG): Dnes, Zítra, "
                "Aktuálně běží a Žánry. Aktualizace EPG po "
                "půlnoci při vstupu."
            ),
            action='tvprog_root',
            art_icon=pick_icon(
                _addon, 'tvprogram.png', 'DefaultCalendar.png'
            ),
            is_folder=True,
        )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='Flixpatrol TOP 10',
            plot=(
                'TOP 10 z platforem + kalendář premiér '
                '(FlixPatrol).'
            ),
            action='streaming_report_menu',
            art_icon=pick_icon(
                _addon, 'flixpatrol.png', 'DefaultCalendar.png'
            ),
            is_folder=True,
        )
        add_menu_item(
            handle=_handle,
            build_url_fn=get_url,
            label='Nastavení',
            plot=(
                "Nastavení doplňku — cesty, integrace, chování "
                "vyhledávání i TV Programu."
            ),
            action='settings',
            art_icon=pick_icon(
                _addon, 'settings.png', 'DefaultAddonService.png'
            ),
            is_folder=True,
        )
        xbmcplugin.endOfDirectory(_handle)
        # Po vykreslení menu — synchronní HTTP nebrzdí první paint (bod 3).
        from resources.lib.core.router_webshare import (
            _vip_start_popup_if_needed as _vip_root_popup,
        )

        _vip_root_popup(threshold_days=11)
        log.info(
            "SM2-perf: root menu %.1f ms | router session %.1f ms"
            % (
                (_time_root.perf_counter() - _t_root) * 1000.0,
                (_time_root.perf_counter() - _t_router) * 1000.0,
            )
        )
        return

    if _ROUTER_ACTIONS_CACHE is None:
        from resources.lib.core.router_epg import (
            ensure_epg_fresh,
            _get_tv_program,
        )
        from resources.lib.core.router_webshare import (
            CATEGORIES,
            NONE_WHAT,
            SORTS,
            api,
            history,
            info,
            play,
            revalidate,
        )
        from resources.lib.series import series_manager
        from resources.lib.series.series_ui import series_detect_czsubs_season
        from resources.lib.search.search_ui import (
            search,
            loadsearch,
            storesearch,
            removesearch,
            dosearch,
            show_search_dialog,
        )

        _ROUTER_ACTIONS_CACHE = {
            'play': play,
            'info': info,
            'settings': settings,
            'history': history,
            'refresh_main': lambda p: xbmc.executebuiltin('Container.Refresh'),
            'search': lambda p: search(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
                categories=CATEGORIES,
                sorts=SORTS,
                none_placeholder=NONE_WHAT,
            ),
            'show_search_dialog': lambda p: (
                log.info(
                    "show_search_dialog handler: "
                    f"cz_title='{p.get('cz_title')}', "
                    f"eng_title='{p.get('eng_title')}', "
                    f"year='{p.get('year')}', "
                    f"tmdb_id={p.get('tmdb_id')}, "
                    f"mediatype={p.get('mediatype')}, "
                    f"cancel_return_url='{p.get('cancel_return_url')}'"
                ),
                show_search_dialog(
                    _addon,
                    _handle,
                    _profile,
                    get_url,
                    api,
                    revalidate,
                    popinfo,
                    cz_title=p.get('cz_title', '').strip(),
                    eng_title=p.get('eng_title', '').strip(),
                    year=p.get('year', '').strip(),
                    tmdb_id=p.get('tmdb_id'),
                    mediatype=p.get('mediatype'),
                    cancel_return_url=p.get('cancel_return_url', '').strip(),
                    return_after_play_url=p.get('return_after_play_url', '').strip(),
                ),
            ),
            'toggle_watchlist': lambda p: __import__(
                'resources.lib.utils.ui_utils',
                fromlist=['toggle_watchlist'],
            ).toggle_watchlist(
                tmdb_id=int(p.get('tmdb_id'))
                if p.get('tmdb_id')
                else None
            ),
            'watchlist_list': lambda p: __import__(
                'resources.lib.down.offline_movie',
                fromlist=['list_watchlist_items'],
            ).list_watchlist_items(
                get_url, _handle, _addon, p
            ),
            'webshare_detect_cz': lambda p: (
                __import__(
                    'resources.lib.testing.android_range_probe_test',
                    fromlist=['run_android_range_probe_test'],
                ).run_android_range_probe_test(
                    p, _addon, _handle, api, revalidate, popinfo
                )
                if not ((_addon.getSetting('ffprobe_path') or '').strip())
                else __import__(
                    'resources.lib.search.search_ui',
                    fromlist=['webshare_detect_cz'],
                ).webshare_detect_cz(
                    p, _addon, _handle, api, revalidate, popinfo
                )
            ),
            'webshare_detect_cz_android_test': lambda p: __import__(
                'resources.lib.testing.android_range_probe_test',
                fromlist=['run_android_range_probe_test'],
            ).run_android_range_probe_test(
                p, _addon, _handle, api, revalidate, popinfo
            ),
            'download': lambda p: __import__(
                'resources.lib.down.download_manager',
                fromlist=['download'],
            ).download(p),
            'cancel_download': lambda p: (
                __import__(
                    'resources.lib.down.download_manager',
                    fromlist=['cancel_download'],
                ).cancel_download(),
                xbmcgui.Dialog().notification(
                    "Stahování",
                    "Zrušeno uživatelem",
                    xbmcgui.NOTIFICATION_INFO,
                ),
            ),
            'series_filtered': lambda p: __import__(
                'resources.lib.series.series_manager',
                fromlist=['create_series_menu_filtered'],
            ).create_series_menu_filtered(
                series_manager.SeriesManager(_addon, _profile),
                _handle,
                (p.get('status') or 'in_progress').lower(),
            ),
            'series_next_episodes': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['create_next_episodes_menu'],
            ).create_next_episodes_menu(
                series_manager.SeriesManager(_addon, _profile),
                _handle,
                _addon,
                get_url,
                _profile,
            ),
            'series': lambda p: series_manager.series_menu(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            )
            if hasattr(series_manager, 'series_menu')
            else __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_menu'],
            ).series_menu(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_search': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_search'],
            ).series_search(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_detail': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_detail'],
            ).series_detail(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_season': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_season'],
            ).series_season(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_bulk_new_episodes': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_bulk_new_episodes'],
            ).series_bulk_new_episodes(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_refresh_smart': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_refresh_smart'],
            ).series_refresh_smart(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_refresh_episode_smart': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_refresh_episode_smart'],
            ).series_refresh_episode_smart(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_detect_cz_episode': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_detect_cz_episode'],
            ).series_detect_cz_episode(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_set_max_streams': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_set_max_streams'],
            ).series_set_max_streams(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_remove': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_remove'],
            ).series_remove(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_remove_season': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_remove_season'],
            ).series_remove_season(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_remove_episode': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_remove_episode'],
            ).series_remove_episode(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_mark_season': lambda p: __import__(
                'resources.lib.series.series_actions',
                fromlist=['series_mark_season'],
            ).series_mark_season(
                p, _addon=_addon, _profile=_profile
            ),
            'series_mark_episode': lambda p: __import__(
                'resources.lib.series.series_actions',
                fromlist=['series_mark_episode'],
            ).series_mark_episode(
                p, _addon=_addon, _profile=_profile
            ),
            'series_offline_manager': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_offline_manager'],
            ).series_offline_manager(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_offline_delete': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_offline_delete'],
            ).series_offline_delete(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_offline_manager_series': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_offline_manager_series'],
            ).series_offline_manager_series(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_offline_delete_series': lambda p: __import__(
                'resources.lib.series.series_ui',
                fromlist=['series_offline_delete_series'],
            ).series_offline_delete_series(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'series_export_library': lambda p: __import__(
                'resources.lib.library.library_export',
                fromlist=['export_series_to_kodi_library'],
            ).export_series_to_kodi_library(),
            'series_export_selected_library': lambda p: __import__(
                'resources.lib.library.library_export',
                fromlist=['export_selected_series_to_kodi_library'],
            ).export_selected_series_to_kodi_library(
                p.get('series_name', '')
            ),
            'series_export_season_library': lambda p: __import__(
                'resources.lib.library.library_export',
                fromlist=['export_selected_season_to_kodi_library'],
            ).export_selected_season_to_kodi_library(
                p.get('series_name', ''), p.get('season', '')
            ),
            'movies_export_from_m3u': lambda p: __import__(
                'resources.lib.library.library_export',
                fromlist=['export_movies_from_m3u'],
            ).export_movies_from_m3u(),
            'series_download_season': lambda p: __import__(
                'resources.lib.down.offline_manager',
                fromlist=['save_season'],
            ).save_season(
                p.get('series_name', ''),
                p.get('season', ''),
                series_manager.SeriesManager(_addon, _profile),
            ),
            'series_download_episode': lambda p: __import__(
                'resources.lib.down.offline_manager',
                fromlist=['save_episode'],
            ).save_episode(
                p.get('series_name', ''),
                str(p.get('season', '')),
                str(p.get('episode', '')),
                series_manager.SeriesManager(_addon, _profile),
            ),
            'series_download_show': _download_whole_show,
            'series_csfd_lookup': lambda p: __import__(
                'resources.lib.series.series_actions',
                fromlist=['csfd_lookup'],
            ).csfd_lookup(p.get('series_name')),
            'csfd_lookup_enhanced': lambda p: __import__(
                'resources.lib.series.series_actions',
                fromlist=['csfd_lookup_enhanced'],
            ).csfd_lookup_enhanced(
                p.get('series_name') or p.get('title') or '',
                tmdb_id=p.get('tmdb_id'),
                media_type=p.get('media_type', 'movies'),
            ),
            'series_validate_streams': lambda p: _validate_streams_action(
                p.get('series_name', '')
            ),
            'series_rescan_invalid': lambda p: _rescan_invalid_action(
                p.get('series_name', '')
            ),
            'series_streams_list': lambda p: series_manager.list_saved_streams(
                series_manager.SeriesManager(_addon, _profile),
                _handle,
                p.get('series_name'),
                build_url_fn=get_url,
                season=p.get('season'),
                filter_mode=p.get('filter_mode'),
            ),
            'series_streams_filter_select': lambda p: _series_streams_filter_select_action(
                p.get('series_name', ''),
                p.get('season', ''),
                p.get('filter_mode', 'all'),
            ),
            'diag_series_manager': lambda p: (
                log.info(
                    "series_manager.__file__ = "
                    f"{__import__('resources.lib.series.series_manager').__file__}"
                ),
                log.info(
                    "SeriesManager methods = "
                    f"{dir(__import__('resources.lib.series.series_manager').SeriesManager)}"
                ),
                xbmcgui.Dialog().notification(
                    ADDON_NAME,
                    "DIAG hotovo (viz Kodi log)",
                    xbmcgui.NOTIFICATION_INFO,
                    3000,
                ),
            ),
            'series_detect_czsubs_season': lambda p: series_detect_czsubs_season(
                p,
                _addon,
                _handle,
                _profile,
                get_url,
                api,
                revalidate,
                popinfo,
            ),
            'trakt_menu': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).show_main_menu(),
            'trakt_movies_hub': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).trakt_movies_hub(),
            'trakt_shows_hub': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).trakt_shows_hub(),
            'trakt_lists_hub': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).trakt_lists_hub(),
            'trakt_movies_list': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).list_movies_by_mode(
                p.get('mode', 'trending'),
                _safe_int(p.get('page', 1), 1),
                _safe_int(p.get('limit', 20), 20),
                p.get('genres') or None,
                p.get('years') or None,
            ),
            'trakt_shows_list': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).list_shows_by_mode(
                p.get('mode', 'trending'),
                _safe_int(p.get('page', 1), 1),
                _safe_int(p.get('limit', 20), 20),
                p.get('genres') or None,
                p.get('years') or None,
            ),
            'trakt_lists_browse': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).list_public_lists(
                p.get('mode', 'popular'),
                _safe_int(p.get('page', 1), 1),
                _safe_int(p.get('limit', 20), 20),
            ),
            'trakt_list_items': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).list_items_of_list(
                p.get('list_id', ''),
                _safe_int(p.get('page', 1), 1),
                _safe_int(p.get('limit', 20), 20),
            ),
            'trakt_my_hub': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).show_my_hub(),
            'trakt_watched_movies': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).list_watched_movies(
                page=_safe_int(p.get('page', 1), 1),
                limit=_safe_int(p.get('limit', 20), 20),
            ),
            'trakt_watched_shows': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).list_watched_shows(
                url=sys.argv[2],
                page=_safe_int(p.get('page', 1), 1),
                limit=_safe_int(p.get('limit', 20), 20),
            ),
            'list_watched_shows': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).list_watched_shows(
                url=sys.argv[2],
                page=_safe_int(p.get('page', 1), 1),
                limit=_safe_int(p.get('limit', 20), 20),
            ),
            'trakt_people': lambda p: _resolve_trakt_id_and_call('people', p),
            'trakt_related': lambda p: _resolve_trakt_id_and_call('related', p),
            'trakt_show_next_last': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).show_next_last_episode(
                p.get('id', ''), p.get('title', '')
            ),
            'trakt_list': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).show_trakt_list(
                p.get('list_type', 'watchlist')
            ),
            'trakt_calendar': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).list_calendar_shows(
                days=int(p.get('days', 30))
                if str(p.get('days', '')).isdigit()
                else 30,
                start_date=p.get('start_date'),
            ),
            'trakt_auth': lambda p: _trakt_auth_with_status(),
            'trakt_refresh_status': lambda p: _trakt_refresh_status(),
            'trakt_shows_progress': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(
                _handle, xbmcaddon.Addon(ADDON_ID)
            ).list_shows_progress(
                page=_safe_int(p.get('page', 1), 1),
                limit=_safe_int(p.get('limit', 20), 20),
                year=p.get('year', ''),
                mode=p.get('mode', 'cache'),
                start_at=p.get('start_at', ''),
                end_at=p.get('end_at', ''),
                only_incomplete=p.get('only_incomplete', ''),
            ),
            'trakt_progress_cache_refresh_full_ui': lambda p: __import__(
                'resources.lib.trakt.trakt_service',
                fromlist=['trakt_progress_cache_refresh_full_ui'],
            ).trakt_progress_cache_refresh_full_ui(),
            'trakt_delete_history': _delete_history_prompt_and_refresh,
            'trakt_search_dialog': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['trakt_search_dialog'],
            ).trakt_search_dialog(
                get_url
            ),
            'trakt_search_results': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['TraktMenu'],
            ).TraktMenu(_handle, _addon).search_trakt(p.get('query')),
            'trakt_mark_movie': lambda p: __import__(
                'resources.lib.trakt.trakt_bulk',
                fromlist=['mark_movie_watched'],
            ).mark_movie_watched(_safe_int(p.get('tmdb_id'), 0)),
            'trakt_mark_show': lambda p: __import__(
                'resources.lib.trakt.trakt_bulk',
                fromlist=['mark_show_watched'],
            ).mark_show_watched(_safe_int(p.get('tmdb_id'), 0)),
            'trakt_mark_season': lambda p: __import__(
                'resources.lib.trakt.trakt_bulk',
                fromlist=['mark_season_watched'],
            ).mark_season_watched(
                _safe_int(p.get('tmdb_id'), 0),
                _safe_int(p.get('season'), 0),
            ),
            'trakt_mark_episode': lambda p: __import__(
                'resources.lib.trakt.trakt_bulk',
                fromlist=['mark_episode_watched'],
            ).mark_episode_watched(
                _safe_int(p.get('tmdb_id'), 0),
                _safe_int(p.get('season'), 0),
                _safe_int(p.get('episode'), 0),
            ),
            'series_trakt_mark_show': lambda p: __import__(
                'resources.lib.trakt.trakt_bulk',
                fromlist=['mark_show_watched'],
            ).mark_show_watched(_safe_int(p.get('tmdb_id'), 0)),
            'series_trakt_mark_season': lambda p: __import__(
                'resources.lib.trakt.trakt_bulk',
                fromlist=['mark_season_watched'],
            ).mark_season_watched(
                _safe_int(p.get('tmdb_id'), 0),
                _safe_int(p.get('season'), 0),
            ),
            'series_trakt_mark_episode': lambda p: __import__(
                'resources.lib.trakt.trakt_bulk',
                fromlist=['mark_episode_watched'],
            ).mark_episode_watched(
                _safe_int(p.get('tmdb_id'), 0),
                _safe_int(p.get('season'), 0),
                _safe_int(p.get('episode'), 0),
            ),
            'trakt_prompt_mark_season': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['trakt_prompt_mark_season'],
            ).trakt_prompt_mark_season(
                _safe_int(p.get('tmdb_id'), 0),
                season=_optional_int_param(p, 'season', min_value=0),
            ),
            'trakt_prompt_mark_episode': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['trakt_prompt_mark_episode'],
            ).trakt_prompt_mark_episode(
                _safe_int(p.get('tmdb_id'), 0),
                season=_optional_int_param(p, 'season', min_value=0),
                episode=_optional_int_param(p, 'episode', min_value=1),
            ),
            'sync_offline_folder': lambda p: __import__(
                'resources.lib.utils.cloud_sync',
                fromlist=['CloudSync'],
            ).CloudSync(_addon).sync_offline_folders_ui(ADDON_NAME),
            'cloud_settings_export': lambda p: __import__(
                'resources.lib.utils.cloud_sync',
                fromlist=['CloudSync'],
            ).CloudSync(_addon).export_offline_settings_ui(ADDON_NAME),
            'cloud_settings_import': lambda p: __import__(
                'resources.lib.utils.cloud_sync',
                fromlist=['CloudSync'],
            ).CloudSync(_addon).import_offline_settings_ui(ADDON_NAME),
            'trakt_watchlist_add': lambda p: __import__(
                'resources.lib.trakt.trakt_watchlist',
                fromlist=['add_to_watchlist'],
            ).add_to_watchlist(),
            'trakt_watchlist_remove': lambda p: __import__(
                'resources.lib.trakt.trakt_watchlist',
                fromlist=['remove_from_watchlist'],
            ).remove_from_watchlist(),
            'trakt_prompt_filter_watched_shows': lambda p: __import__(
                'resources.lib.utils.ui_utils',
                fromlist=['prompt_and_filter_list'],
            ).prompt_and_filter_list(
                'list_watched_shows',
                'title_filter',
                'Zadejte počáteční písmena pro filtr historie',
            ),
            'tmdb': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['show_tmdb_menu'],
            ).show_tmdb_menu(),
            'tmdb_search': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['tmdb_search_menu'],
            ).tmdb_search_menu(),
            'tmdb_list': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['show_tmdb_list'],
            ).show_tmdb_list(p),
            'tmdb_seasons': lambda p: _tmdb().show_seasons(
                p.get('tmdb_id'),
                p.get('title'),
                p.get('trakt_id'),
                p.get('slug'),
                fallback_title=p.get('fallback_title'),
            ),
            'tmdb_episodes': lambda p: _tmdb().show_episodes(
                p.get('tmdb_id'),
                p.get('season_num'),
                p.get('title'),
                p.get('trakt_id'),
                p.get('slug'),
                fallback_title=p.get('fallback_title'),
            ),
            'tmdb_movie_categories': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['show_movie_categories'],
            ).show_movie_categories(),
            'tmdb_tv_categories': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['show_tv_categories'],
            ).show_tv_categories(),
            'tmdb_person_categories': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['show_person_categories'],
            ).show_person_categories(),
            'tmdb_search_person': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['tmdb_search_person_menu'],
            ).tmdb_search_person_menu(),
            'person_detail': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['show_person_detail'],
            ).show_person_detail(p),
            'tmdb_person_detail': lambda p: __import__(
                'resources.lib.trakt.trakt_module',
                fromlist=['show_tmdb_person_detail'],
            ).show_tmdb_person_detail(int(p.get('tmdb_id', 0))),
            'tmdb_advanced_filter_menu': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['show_advanced_filter_menu'],
            ).show_advanced_filter_menu(),
            'tmdb_discover_list': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['show_tmdb_discover_list'],
            ).show_tmdb_discover_list(p),
            'tmdb_discover_resume': lambda p: __import__(
                'resources.lib.tmdb.tmdb_kodi',
                fromlist=['tmdb_discover_resume'],
            ).tmdb_discover_resume(p),
            'tmdb_vod_premieres_menu': lambda p: __import__(
                'resources.lib.tmdb.vod_kodi',
                fromlist=['show_vod_premieres_menu'],
            ).show_vod_premieres_menu(),
            'tmdb_vod_premieres_list': lambda p: __import__(
                'resources.lib.tmdb.vod_kodi',
                fromlist=['show_vod_premieres_list'],
            ).show_vod_premieres_list(p),
            'tmdb_vod_cz_streaming_menu': lambda p: __import__(
                'resources.lib.vod.vod_streaming_kodi',
                fromlist=['show_vod_cz_streaming_menu'],
            ).show_vod_cz_streaming_menu(),
            'tmdb_vod_cz_streaming_catalog_menu': lambda p: __import__(
                'resources.lib.vod.vod_streaming_kodi',
                fromlist=['show_vod_cz_streaming_catalog_menu'],
            ).show_vod_cz_streaming_catalog_menu(p),
            'tmdb_vod_cz_streaming_list': lambda p: __import__(
                'resources.lib.vod.vod_streaming_kodi',
                fromlist=['show_vod_cz_streaming_list'],
            ).show_vod_cz_streaming_list(p),
            'csfd_toplist': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['show_root'],
            ).show_root(),
            'csfd_prepare_menu': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['prepare_menu'],
            ).prepare_menu(),
            'csfd_clean_cache_menu': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['clean_cache_menu'],
            ).clean_cache_menu(),
            'csfd_clean_cache': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['clean_cache'],
            ).clean_cache(p.get('what', 'all')),
            'csfd_prepare_all': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['prepare_all'],
            ).prepare_all(
                (p.get('category', 'films') or 'films').lower()
            ),
            'csfd_update_all': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['update_all'],
            ).update_all(),
            'csfd_browse_menu': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['browse_menu'],
            ).browse_menu(
                (p.get('category', 'series') or 'series').lower()
            ),
            'csfd_list_countries': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['list_countries'],
            ).list_countries(
                (p.get('category', 'series') or 'series').lower()
            ),
            'csfd_browse': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['browse'],
            ).browse((p.get('category', 'series') or 'series').lower()),
            'csfd_browse_filtered': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['browse_filtered'],
            ).browse_filtered(
                (p.get('category', 'series') or 'series').lower(),
                p.get('countries'),
                p.get('country'),
            ),
            'csfd_prepare_m3u_index': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['prepare_m3u_index'],
            ).prepare_m3u_index(),
            'csfd_play_or_open': csfd_play_or_open,
            'play_m3u_stream': lambda p: _play_m3u(p.get('m3u_url', '')),
            'show_playlist': lambda p: __import__(
                'resources.lib.playlists.playlist_loader',
                fromlist=['load_m3u_playlist'],
            ).load_m3u_playlist(
                translatePath(ADDON.getSetting('playlist_path'))
            ),
            'extra_playlist': lambda p: __import__(
                'resources.lib.extra_playlists',
                fromlist=['show_extra_playlist'],
            ).show_extra_playlist(p),
            'csfd_extra_playlist': lambda p: __import__(
                'resources.lib.csfd_toplist.router',
                fromlist=['show_extra_playlist_movies'],
            ).show_extra_playlist_movies(p),
            'tvprog_root': lambda p: (
                ensure_epg_fresh(),
                _get_tv_program().show_root(),
            )[1],
            'tvprog_channels': lambda p: _get_tv_program().show_channels_by_date(
                p.get('day')
            ),
            'tvprog_channel': lambda p: _get_tv_program().show_channel_programs(
                p.get('ch'), p.get('day')
            ),
            'tvprog_now': lambda p: _get_tv_program().show_now_playing(
                p.get('day')
            ),
            'tvprog_genres': lambda p: _get_tv_program().show_genres(
                p.get('day')
            ),
            'tvprog_genre': lambda p: _get_tv_program().show_programs_by_genre(
                p.get('day'), p.get('g')
            ),
            'tvprog_info': lambda p: _get_tv_program().show_program_info(
                p.get('ch'), p.get('i') or p.get('idx'), p.get('day')
            ),
            'queue_menu': lambda p: __import__(
                'resources.lib.down.download_queue',
                fromlist=['show_queue_menu'],
            ).show_queue_menu(),
            'queue_add': lambda p: __import__(
                'resources.lib.down.download_queue',
                fromlist=['add_to_queue'],
            ).add_to_queue(p),
            'queue_remove': lambda p: __import__(
                'resources.lib.down.download_queue',
                fromlist=['remove_from_queue'],
            ).remove_from_queue(p.get('ident')),
            'queue_start': lambda p: __import__(
                'resources.lib.down.download_queue',
                fromlist=['start_download'],
            ).start_download(p.get('ident')),
            'queue_start_all': lambda p: __import__(
                'resources.lib.down.download_queue',
                fromlist=['start_all'],
            ).start_all(),
            'queue_clear': lambda p: __import__(
                'resources.lib.down.download_queue',
                fromlist=['clear_queue'],
            ).clear_queue(),
            'queue_stop_all': lambda p: __import__(
                'resources.lib.down.download_queue',
                fromlist=['stop_all_downloads'],
            ).stop_all_downloads(),
            'streaming_report_menu': lambda p: __import__(
                'resources.lib.reports.streaming_report',
                fromlist=['show_report_menu'],
            ).show_report_menu(_handle, get_url),
            'streaming_report_platform': lambda p: __import__(
                'resources.lib.reports.streaming_report',
                fromlist=['show_platform_titles'],
            ).show_platform_titles(
                _handle, get_url, p.get('p') or 'Netflix'
            ),
            'streaming_report_refresh': lambda p: (
                __import__(
                    'resources.lib.reports.streaming_report',
                    fromlist=['refresh_report'],
                ).refresh_report(force=True, notify=True),
                xbmc.executebuiltin('Container.Refresh'),
            ),
            'watch_history_list': _show_watch_history_list_entry,
            'play_history_item': lambda p: play_history_item(
                p.get('url'),
                p.get('tmdb_id'),
                p.get('media_type'),
            ),
            'delete_history_item': lambda p: delete_history_item(
                p.get('tmdb_id'),
                p.get('media_type'),
            ),
            'fix_and_scrobble_item': lambda p: __import__(
                'resources.lib.history.local_history',
                fromlist=['fix_and_scrobble_item'],
            ).fix_and_scrobble_item(
                int(p.get('index', -1)),
                (p.get('media_type') or 'movie').lower(),
            ),
            'movies_menu': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['movies_menu'],
            ).movies_menu(
                _handle, _addon, get_url
            ),
            'movies_downloaded': lambda p: __import__(
                'resources.lib.down.offline_movie',
                fromlist=['show_downloaded_movies'],
            ).show_downloaded_movies(_handle),
            'delete_downloaded_file': lambda p: __import__(
                'resources.lib.down.offline_movie',
                fromlist=['delete_downloaded_file'],
            ).delete_downloaded_file(p.get('path', '')),
            'show_folder': lambda p: __import__(
                'resources.lib.down.offline_movie',
                fromlist=['show_folder'],
            ).show_folder(_handle, p.get('path', '')),
            'ws_refresh_vip': lambda p: (
                __import__(
                    'resources.lib.auth.webshare_auth',
                    fromlist=['WebshareClient'],
                ).WebshareClient(ADDON_ID).refresh_vip_settings(
                    show_toast=True
                ),
                xbmcplugin.endOfDirectory(_handle),
            ),
            'show_kodi_log': lambda p: _show_kodi_log(),
            'save_kodi_log': lambda p: _save_kodi_log(),
            'cloud_sync_now': lambda p: cloud_sync_now(p, _addon),
            'exp_menu': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['show_experimental_menu'],
            ).show_experimental_menu(
                _handle, _addon, get_url
            ),
            'settings_backup_now': lambda p: __import__(
                'resources.lib.utils.settings_backup',
                fromlist=['backup_now'],
            ).backup_now(_addon, silent=False, force=True),
            'settings_restore_menu': lambda p: __import__(
                'resources.lib.utils.settings_backup',
                fromlist=['restore_menu'],
            ).restore_menu(_addon),
            'exp_toggle_bool': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['exp_toggle_bool'],
            ).exp_toggle_bool(
                _addon, p.get('key')
            ),
            'exp_browse_file': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['exp_browse_file'],
            ).exp_browse_file(
                _addon, p.get('key')
            ),
            'telemetry_months_menu': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['telemetry_months_menu'],
            ).telemetry_months_menu(_handle, _addon, get_url),
            'telemetry_view_start_log': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['telemetry_view_start_log'],
            ).telemetry_view_start_log(_addon, p.get('month')),
            'telemetry_upload_now': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['telemetry_upload_now'],
            ).telemetry_upload_now(_addon),
            'telemetry_migrate_cloud': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['telemetry_migrate_cloud'],
            ).telemetry_migrate_cloud(_addon),
            'clear_menu_cache': lambda p: _clear_menu_cache_ui(),
            'offline_scan_now': lambda p: _offline_scan_now_ui(),
            'subcaps_clear_all': lambda p: _subcaps_clear_all_ui(),
            'subcaps_clear_series_menu': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['subcaps_clear_series_dialog'],
            ).subcaps_clear_series_dialog(_addon),
            'series_reextract_features': lambda p: __import__(
                'resources.lib.utils.ui_menus',
                fromlist=['series_reextract_features_dialog'],
            ).series_reextract_features_dialog(_addon),
            'background_enrich_ratings': lambda p: _background_enrich_ratings(),
            'tmdb_audit_fill': lambda p: __import__(
                'resources.lib.utils.background_enrich',
                fromlist=['background_audit_fill_tmdb_metadata'],
            ).background_audit_fill_tmdb_metadata(),
            'tmdb_audit_fill_full': lambda p: __import__(
                'resources.lib.utils.background_enrich',
                fromlist=['background_audit_fill_tmdb_metadata_full'],
            ).background_audit_fill_tmdb_metadata_full(),
            'titulky_prompt': lambda p: __import__(
                'resources.lib.integrations.titulky_integration',
                fromlist=['run_titulky_prompt'],
            ).run_titulky_prompt(p),
        }

    handler = _ROUTER_ACTIONS_CACHE.get(action)
    if handler:
        try:
            handler(params)
        except Exception as e:
            log.error(
                f"handler error for action={action}: {e}", exc_info=True
            )
            xbmcgui.Dialog().notification(
                _addon.getAddonInfo('name'),
                "Chyba akce.",
                xbmcgui.NOTIFICATION_ERROR,
                3000,
            )
    elif action == 'noop':
        xbmcplugin.endOfDirectory(_handle, succeeded=True)
        log.info(
            "SM2-perf: router action=noop %.1f ms"
            % ((_time_router.perf_counter() - _t_router) * 1000.0)
        )
        return
    else:
        log.error(f"unknown action: {action}")
        popinfo(
            f'Neznama akce: {action}',
            icon=xbmcgui.NOTIFICATION_ERROR,
        )

    log.info(
        "SM2-perf: router action=%s %.1f ms"
        % (
            action or "(empty)",
            (_time_router.perf_counter() - _t_router) * 1000.0,
        )
    )

