# -*- coding: utf-8 -*-
"""One item menu and editor across listings, dialogs and inherited widget contexts."""
import json
import re
from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit

import xbmc
import xbmcaddon
import xbmcgui
from wcs.skin import family

BASE = 'plugin://plugin.video.milionar/'
_listing_context = ('', '')
LABELS = {
    'play': 32377, 'restart': 32378, 'source': 32379, 'detail': 32380, 'folder': 32381,
    'season': 32382, 'next': 32383, 'next_source': 32384, 'similar': 32385, 'game': 32386,
    'list': 32387, 'channel': 32388, 'watched': 32389, 'channel_open': 32390,
    'live': 32391, 'live_shuffle': 32392, 'first': 32393, 'random': 32394,
    'open': 32396, 'collection': 32416, 'hide_continue': 32407,
    'play_all': 32522, 'shuffle': 32523, 'play_from': 32524, 'queue': 32525,
    'favorite': 32612, 'add_playlist': 32614, 'create_playlist': 32615,
    'rename_playlist': 32616, 'delete_playlist': 32617, 'remove_song': 32618,
    'album': 32619, 'artist': 32620, 'rename_channel': 32621, 'delete_channel': 32622,
    'edit_channel': 32623, 'move_channel': 32624, 'song_up': 32625, 'song_down': 32626,
    'best': 32724, 'file_info': 30211, 'download': 30212, 'download_pick': 32725,
    'autoplay': 32726, 'remove_favorite': 32727, 'history': 32728, 'web': 32729,
    'edit_progress': 32730, 'remove_program': 32731, 'move_program': 32732,
    'plot': 32679, 'trailer': 32739, 'director': 32738, 'creators': 32680,
    'tab_movies': 32747, 'tab_series': 32748, 'random_game': 32749, 'ai_costs': 32800,
    'rate': 35331, 'fy_mode': 35345,
}
GAME_ROWS = ('widget_game_topics', 'widget_demo_movies', 'widget_demo_series')
GAME_HERO = ('tab_movies', 'tab_series', 'random_game', 'ai_costs')   # the hero buttons of the Milionář section
VIDEO = ('play', 'restart', 'source', 'detail', 'folder', 'season', 'next', 'next_source',
         'watched', 'list', 'hide_continue', 'channel', 'similar', 'collection', 'game', 'plot', 'trailer', 'director', 'creators',
         'rate')
MUSIC = ('open', 'play', 'play_all', 'shuffle', 'queue', 'play_from', 'favorite', 'add_playlist',
         'create_playlist', 'rename_playlist', 'delete_playlist', 'remove_song', 'album', 'artist',
         'song_up', 'song_down')
CHANNEL = ('live', 'open', 'live_shuffle', 'first', 'random', 'edit_channel', 'rename_channel',
           'move_channel', 'delete_channel')
CONTEXT_ONLY = ('edit_progress', 'remove_program', 'move_program', 'plot', 'trailer', 'director', 'creators')


def text(number):
    return xbmcaddon.Addon('plugin.video.milionar').getLocalizedString(number)


def catalog(row):
    """Include descendants: a genre/series/playlist opens other kinds of items."""
    if row.startswith('home_'):
        from wcs.skin import navigation
        return navigation.catalog(row)
    if row.startswith('shared:'):
        from wcs.skin import context_profiles
        return context_profiles.catalog(row)
    if row.startswith('vinyl_'):
        return MUSIC
    if row in ('widget_mytv_channels', 'widget_mycinema_channels'):
        return tuple(a for a in CHANNEL if row != 'widget_mycinema_channels' or a not in ('live_shuffle', 'first')) + VIDEO + ('channel_open', 'edit_progress', 'remove_program', 'move_program')
    if row == 'widget_mytv_next':
        return ('play', 'detail', 'folder', 'season', 'similar', 'game', 'channel_open', 'edit_progress', 'remove_program', 'move_program')
    if row == 'widget_ai_recent':
        return ('open',) + VIDEO
    if row in GAME_ROWS:
        return GAME_HERO + (VIDEO if row == 'widget_game_topics' else ())
    if row.startswith('fy_'):
        return VIDEO + ('fy_mode',)                        # Pro tebe: what the section shows (wcs/foryou/mode.py)
    return (('open',) + VIDEO) if row.endswith('_genres') or row in ('widget_kraska_root', 'widget_csfd_charts') else VIDEO


def defaults(row):
    if row.startswith('home_'):
        return {'main': list(catalog(row)), 'more': [], 'hidden': []}
    if row.startswith('shared:'):
        from wcs.skin import context_profiles
        return context_profiles.defaults(row)
    if row.startswith('vinyl_'):
        primary = ('play', 'queue', 'favorite') if row.endswith('_songs') else ('open', 'play_all', 'play', 'queue', 'favorite')
        if row in ('vinyl_genres', 'vinyl_radios'):
            primary = ('open', 'play_all', 'play', 'queue', 'favorite')
    elif row in ('widget_mytv_channels', 'widget_mycinema_channels'):
        primary = ('live', 'open', 'play', 'folder', 'next', 'detail')
    elif row in GAME_ROWS:
        primary = ('game', 'detail')
    elif row in ('cont_movies', 'my_movies_progress'):
        primary = ('play', 'restart', 'hide_continue')
    elif row in ('cont_series', 'my_series_next', 'my_series_new', 'my_series_upcoming', 'widget_mytv_next'):
        primary = ('play', 'season', 'watched')
    elif row in ('my_series_added', 'my_series_unwatched', 'my_series_all', 'widget_tmdb_popular_series'):
        primary = ('folder', 'next', 'detail', 'play', 'season', 'watched')
    elif row.endswith('_genres') or row in ('widget_kraska_root', 'widget_csfd_charts'):
        primary = ('open', 'play', 'detail')
    elif row.startswith('fy_') and row != 'fy_continue':
        primary = ('play', 'detail', 'rate', 'list')       # Pro tebe: rating is what teaches the recommendations
    else:
        primary = ('play', 'detail', 'list')
    allowed = catalog(row)
    return {'main': [a for a in primary if a in allowed],
            'more': [a for a in allowed if a not in primary], 'hidden': []}


def normalize(row, config=None):
    """Preserve custom order/hidden actions; new supported actions go to More."""
    if not isinstance(config, dict):
        return defaults(row)
    if row.startswith('home_') and any('open' in config.get(group, []) for group in ('main', 'more', 'hidden')):
        # The first action-row version exposed both cards as one generic Open action.
        config = {group: ['browse' if action == 'open' else action for action in config.get(group, [])]
                  for group in ('main', 'more', 'hidden')}
        if 'add' in catalog(row) and not any('add' in values for values in config.values()):
            config['main'].insert(0, 'add')
    allowed, seen, result = catalog(row), set(), {}
    for group in ('main', 'more', 'hidden'):
        values = config.get(group, [])
        result[group] = []
        for action in values if isinstance(values, list) else []:
            if isinstance(action, str) and action in allowed and action not in seen:
                result[group].append(action)
                seen.add(action)
    result['more'].extend(a for a in allowed if a not in seen)
    return result


def label(row, action):
    if row.startswith('home_'):
        from wcs.skin import navigation
        return navigation.label(row, action)
    if action == 'open':
        return text(32521 if row.startswith(('vinyl_', 'shared:vinyl_')) else 32397 if row in ('widget_mycinema_channels', 'shared:channel_cinema') else
                    32396 if row in ('widget_mytv_channels', 'shared:channel_tv') else 32634)
    return text(LABELS[action])


def encode(config):
    # Skin.SetString quotes are not JSON-safe; persist percent-encoded JSON.
    return quote(json.dumps(config, separators=(',', ':')), safe='') if config else ''


def decode(raw):
    from urllib.parse import unquote
    try:
        return json.loads(unquote(raw)) if raw else None
    except (ValueError, TypeError):
        return None


def origin(params):
    if params.get('ctx'):
        return params['ctx']
    sec, slot = params.get('sec'), params.get('slot')
    if sec and slot:
        return xbmc.getInfoLabel('Skin.String(mil.{}.{}.uid)'.format(sec, slot)) or '{}:{}'.format(sec, slot)
    return ''


def find_widget(identity):
    from wcs.skin import slots
    for sec in slots.SECTIONS:
        for n in range(1, slots.SLOTS + 1):
            w = slots.current(sec, n)
            if (w.get('uid') or '{}:{}'.format(sec, n)) == identity and w['type'] in slots.TITLES:
                return sec, n, w
    return '', 0, None


def with_origin(path, identity, row):
    if not identity or not path.startswith(BASE.rstrip('/')):
        return path
    root, _, query = path.partition('?')
    values = dict(parse_qsl(query))
    values.update(ctx=identity, ctxrow=row)
    if values.get('action') == 'skin_run' and values.get('run'):
        run = dict(parse_qsl(values['run']))
        run.update(ctx=identity, ctxrow=row)
        values['run'] = urlencode(run)
    return root + '?' + urlencode(values)


def attach(path, item, folder, identity, row, hero=False, lent=None):
    """Tag every item at the output boundary, including legacy providers and descendant folders.

    `hero`: the widget's hero button configuration (None: its defaults); False for a listing that has no hero.
    `lent`: the layouts of the section's rows that borrow this row's titles for their hero (hero_buttons.lent)."""
    q = dict(parse_qsl(urlsplit(path).query))
    if q.get('action') == 'skin_run':
        q.update(dict(parse_qsl(q.get('run', ''))))
    data = unpack(item.getProperty('milionar.ctxdata'))
    data.update(ctx=identity, row=row, title=item.getProperty('milionar.ctx_title') or item.getLabel())
    for key in ('type', 'tmdb', 's', 'e', 'channel_id', 'channel_kind'):
        value = item.getProperty('milionar.' + key)
        if value:
            data[key] = value
    action = q.get('action', '')
    person = next((role for role in ('actor', 'director', 'writer') if q.get(role + '_id')), None)
    if person and (data.get('type') == 'person' or not folder or action == 'skin_detail_actor'):
        data.update(type='person', id=q[person + '_id'], role=person)
    elif action.startswith('vinyl_') and (q.get('kind') or data.get('kind')):
        data.update({k: v for k, v in q.items() if k in ('kind', 'id', 'name', 'artist', 'row', 'ids', 'parent_playlist', 'owner', 'album', 'album_id')})
        data['type'] = 'vinyl'
    elif q.get('channel_id') and q.get('action') in ('channel_play', 'my_tv_dialog', 'my_cinema_dialog'):
        data.update(type='channel', id=q['channel_id'], kind='cinema' if q.get('kind') == 'cinema' or q['action'] == 'my_cinema_dialog' else 'tv')
    elif not data.get('tmdb'):
        tmdb = q.get('tmdb') or q.get('tmdb_id') or q.get('movie_id') or q.get('series_id')
        if tmdb and str(tmdb).isdigit() and (not folder or action in ('mlist', 'show_wcs_details', 'ctx_next')):
            from wcs.library2 import clicks
            row_kind = clicks.kind_of(row)
            data.update(tmdb=tmdb, type=q.get('media_type') or q.get('type') or
                        ('episode' if q.get('episode_number') else 'tv' if q.get('kind') == 'seasons' or
                         action == 'ctx_next' or 'series' in action or row_kind == 'series' else 'movie'))
            data.update(s=q.get('s') or q.get('season_number', ''), e=q.get('e') or q.get('episode_number', ''))
    if q.get('mytv_channel_id'):
        data['channel_id'] = q['mytv_channel_id']
    if q.get('mycinema_channel_id'):
        data.update(channel_id=q['mycinema_channel_id'], channel_kind='cinema')
    if action == 'skin_detail_season':
        from wcs.skin import detail
        state = detail._state()
        if state:
            data.update(type='season', tmdb=str(state['id']), s=q.get('s', ''))
    if not data.get('type'):
        data['type'] = ('folder' if folder else 'file' if item.getProperty('IsPlayable') == 'true' and
                        not path.startswith('plugin://') else 'action')
    if action.startswith('vinyl_') and data['type'] != 'vinyl':
        data['music'] = '1'
    if data.get('type') in ('series', 'tvshow'):
        data['type'] = 'tv'
    if data.get('s') == 'all' or q.get('s') == 'all':
        data['type'] = 'folder'
    if data.get('type') == 'tv' and str(data.get('s', '')).lstrip('-').isdigit():
        data['type'] = 'episode' if data.get('e') not in (None, '') else 'season'
    path = with_origin(path, identity, row)
    item.setPath(path)
    command_path = with_origin(data.get('path') or path, identity, row) if data['type'] == 'vinyl' else path
    data['path'], data['folder'] = command_path, '1' if folder else ''
    if item.getProperty('milionar.exec'):
        from wcs.library2 import clicks
        item.setProperty('milionar.exec', ('ActivateWindow(Music,{},return)'.format(command_path) if folder and data['type'] == 'vinyl'
                                         else clicks.exec_for(command_path, folder)))
    if data['type'] == 'vinyl':
        ready = [a for a in item.getProperty('milionar.hero.directs').split(',') if a]
        for name in ['milionar.hero.play', 'milionar.hero.detail'] + ['milionar.hero.do.' + a for a in ready]:
            command = item.getProperty(name)
            if command.startswith('RunPlugin('):
                item.setProperty(name, 'RunPlugin(' + with_origin(command[10:-1], identity, row) + ')')
    encoded = pack(data)
    item.setProperty('milionar.ctxdata', encoded)
    item.setProperty('milionar.ctx', identity)
    item.addContextMenuItems(entry(data, visible=True), True)
    if hero is not False:
        from wcs.skin import hero_buttons
        hero_buttons.compose(item, data, row, hero, encoded)
        for slot, (other, config, owner) in (lent or {}).items():
            hero_buttons.compose(item, data, other, config, encoded, 's{}.'.format(slot), owner)
    return path, item, folder


def hero_config(params):
    """(hero buttons of the Home slot a listing fills, layouts lent to it); (False, None): no row of a hero section."""
    from wcs.skin.hero_contract import SECTIONS
    sec, slot = params.get('sec'), params.get('slot')
    if sec not in SECTIONS or not slot or params.get('ctx'):
        return False, None
    from wcs.skin import hero_buttons
    return hero_buttons.stored(sec, slot), hero_buttons.lent(sec, slot)


def unpack(raw):
    # A skin property is encoded once more than the `data` value parsed by main.py.
    return dict(parse_qsl(raw if '=' in raw else unquote(raw))) if raw else {}


def install_listing_context(params):
    """Only this plugin invocation is decorated; unrelated Kodi/addon listings are untouched."""
    global _listing_context
    identity = origin(params)
    import xbmcplugin
    row = params.get('ctxrow') or params.get('row', '')
    if params.get('action') == 'skin_detail_row':
        from wcs.skin import detail
        state = detail._state() or {}
        identity, row = state.get('ctx', ''), state.get('ctxrow', '')
    _listing_context = identity, row
    hero, lent = hero_config(params)
    # Kodi reuses this module between plugin calls. Never wrap a previous invocation's closure.
    originals = list(getattr(xbmcplugin, '_milionar_directory_originals',
                            (xbmcplugin.addDirectoryItem, xbmcplugin.addDirectoryItems)))
    for i, name in enumerate(('single', 'multiple')):
        # Also unwrap closures left by versions before the original functions were recorded.
        while getattr(originals[i], '__module__', '') == __name__ and originals[i].__closure__:
            cells = dict(zip(originals[i].__code__.co_freevars, originals[i].__closure__))
            if name not in cells:
                break
            originals[i] = cells[name].cell_contents
    xbmcplugin._milionar_directory_originals = tuple(originals)
    single, multiple = originals

    def add_one(handle, url, listitem, isFolder=False, totalItems=0):
        path, item, folder = attach(url, listitem, isFolder, identity, row, hero, lent)
        return single(handle, path, item, folder, totalItems)

    def add_many(handle, items, totalItems=0):
        return multiple(handle, [attach(p, i, f, identity, row, hero, lent) for p, i, f in items], totalItems)

    xbmcplugin.addDirectoryItem, xbmcplugin.addDirectoryItems = add_one, add_many


def builtin(params, folder=False, music=False):
    path = BASE + '?' + urlencode(params)
    return 'ActivateWindow({},{},return)'.format('Music' if music else 'Videos', path) if folder else 'RunPlugin({})'.format(path)


def pack(data):
    return quote(urlencode({k: v for k, v in data.items() if v is not None}), safe='')


def entry(data, visible=False):
    """Legacy descriptor transport; display its fallback entry only in other skins."""
    if visible and family.active():
        return []
    return [(text(32735), builtin({'action': 'ctx_open', 'data': pack(data)}))]


def add_legacy(item, entries):
    """Keep provider-specific actions by stable id; titles use the shared action builder."""
    data = unpack(item.getProperty('milionar.ctxdata'))
    extras = decode(data.get('extras', '')) or {}
    ids = {'info': 'file_info', 'file_info': 'file_info', 'download_file': 'download', 'open_csfd_url': 'web',
           'remove_recently_played_item': 'history', 'play_next_episode_with_metadata': 'next'}
    for title, command in entries:
        title = re.sub(r'\[/?(?:COLOR(?: [^\]]+)?|B|I)\]', '', title)
        url = command[command.find('(') + 1:command.rfind(')')].split(',return')[0]
        args = dict(parse_qsl(urlsplit(url).query))
        if args.get('action') == 'ctx_open' and args.get('data'):
            incoming = unpack(args['data'])
            extras.update(decode(incoming.pop('extras', '')) or {})
            data.update(incoming)
        key = ids.get(args.get('action'))
        if key:
            extras[key] = [title, command]
            if key in ('file_info', 'download'):
                data['type'] = 'file'
    if extras:
        data['extras'] = encode(extras)
    item.setProperty('milionar.ctxdata', pack(data))
    item.addContextMenuItems(entry(data, visible=True), True)


def video_actions(data):
    from wcs.library2 import clicks, menu
    kind, tmdb = data['type'], int(data['tmdb'])
    s, e = (int(data[k]) if data.get(k) not in (None, '') else None for k in ('s', 'e'))
    result = {}
    # The existing cache/store rules supply labels and eligibility; ids never depend on translated text or src=dialog.
    model = menu.Menu()
    playing = model._play_items(kind, tmdb, s, e)
    if kind in ('movie', 'episode') and playing:
        result['play'], result['source'] = playing[0], playing[-1]
        result['restart'] = playing[1] if len(playing) == 3 else (text(32401), playing[0][1])
    elif playing:
        result['next'] = playing[0]
    states = model._state_items(kind, tmdb, s, e)
    result['watched'] = states[0]
    if len(states) > 1:
        result['hide_continue'] = states[1]
    listed = model._list_items(kind, tmdb)
    result['list'], result['channel'] = listed
    discovery = model._discovery_items(kind, tmdb, s, e, data.get('title', ''))
    result['detail'], result['similar'], result['game'] = discovery[0], discovery[1], discovery[-1]
    if len(discovery) == 4:
        result['collection'] = discovery[2]
    result['rate'] = (text(LABELS['rate']), builtin({'action': 'foryou_rate', 'type': 'movie' if kind == 'movie' else 'tv',
                                                       'tmdb': tmdb, 'title': data.get('title', '')}))
    if kind in ('tv', 'season', 'episode'):
        result['folder'] = (text(32381), 'ActivateWindow(Videos,{},return)'.format(clicks.seasons_path(tmdb)))
    if kind == 'episode' and s is not None:
        result['season'] = (text(32382), 'ActivateWindow(Videos,{},return)'.format(clicks.season_path(tmdb, s)))
    if kind in ('tv', 'season') and 'next' in result:
        result['next_source'] = (text(32384), builtin({'action': 'ctx_next', 'tmdb': tmdb, 'src': 'dialog'}))
    if 'mupcoming' in data.get('path', '') or data.get('s') == '-1':
        result = {k: v for k, v in result.items() if k not in ('play', 'restart', 'source', 'next', 'next_source', 'watched')}
    if data.get('channel_id'):
        # MyTV playback/progress is separate from the library store.
        if data.get('channel_play'):
            result['play' if kind in ('movie', 'episode') else 'next'] = (text(32377 if kind in ('movie', 'episode') else 32383), data['channel_play'])
        else:
            # Rebuild play, rather than reusing the widget's configurable default click (which can be Detail).
            q = {'action': 'play_episode_from_addon', 'tmdb_id': tmdb, 'season_number': s, 'episode_number': e,
                 'series_name': data.get('title', ''), 'mytv_channel_id': data['channel_id']}
            result['play'] = (text(32377), builtin(q))
        result['channel_open'] = (text(32390), builtin({'action': 'my_cinema_dialog' if data.get('channel_kind') == 'cinema' else 'my_tv_dialog',
                                                     'channel_id': data['channel_id']}))
        for action in ('watched', 'hide_continue', 'restart', 'source', 'next_source'):
            result.pop(action, None)
    return result


def person_actions(data):
    from wcs import user_data
    role, identity = data.get('role', 'actor'), data.get('id')
    if role not in ('actor', 'director', 'writer') or not identity:
        return {}
    name = data.get('title', '')
    result = {
        'detail': (text(32380), builtin({'action': 'display_' + role + '_dialog', role + '_id': identity,
                                        role + '_name': name})),
        'open': (text(32634), builtin({'action': 'tmdb_search_' + role + '_movies', role + '_id': identity,
                                      role + '_name': name}, folder=True)),
        'game': (text(32386), builtin({'action': 'milionar_game', 'media_type': role, 'tmdb_id': identity,
                                      'title': name, 'plot': data.get('plot', ''), 'poster': data.get('poster', '')})),
    }
    favorites = getattr(user_data, 'load_favorite_' + role + 's')(xbmcaddon.Addon('plugin.video.milionar'))
    index = next((i for i, person in enumerate(favorites) if str(person.get('id')) == str(identity)), None)
    if index is None:
        result['favorite'] = (text(32612), builtin({'action': 'tmdb_add_' + role + '_to_favorites',
                                                   role + '_id': identity, role + '_name': name}))
    else:
        result['remove_favorite'] = (text(32727), builtin({'action': 'tmdb_remove_favorite_' + role, 'idx': index}))
    return result


def channel_actions(data):
    from wcs.library2 import clicks
    kind = data['kind']
    result = {}
    for action in ('live', 'open', 'random') + (('live_shuffle', 'first') if kind == 'tv' else ()):
        params = {'action': 'channel_play', 'kind': kind, 'channel_id': data['id'], 'mode': action}
        if action == 'open':
            params = {'action': 'my_cinema_dialog' if kind == 'cinema' else 'my_tv_dialog', 'channel_id': data['id']}
        result[action] = (clicks.label('widget_mycinema_channels' if kind == 'cinema' else 'widget_mytv_channels', action), builtin(params))
    for action in ('edit_channel', 'rename_channel', 'move_channel', 'delete_channel'):
        result[action] = (text(LABELS[action]), builtin({'action': 'skin_channel_edit', 'kind': kind, 'id': data['id'], 'op': action,
                                                       'ctx': data.get('ctx', ''), 'ctxrow': data.get('row', '')}))
    return result


def resolve(data):
    """(section, slot, widget, row, {action: (label, builtin)}) of an item: what it can do right now."""
    sec, slot, widget = find_widget(data.get('ctx')) if data.get('ctx') else ('', 0, None)
    row = widget['type'] if widget else data.get('row', '')
    kind = data.get('type')
    if row.startswith('home_'):
        from wcs.skin import navigation
        available = navigation.actions(row)
    elif kind == 'vinyl':
        from wcs.vinyl.context import actions
        available = actions(data)
    elif kind == 'channel':
        available = channel_actions(data)
    elif data.get('tmdb') and kind in ('movie', 'tv', 'season', 'episode'):
        available = video_actions(data)
        if kind == 'season' and data.get('path'):
            available['open'] = (text(32634), builtin({'action': 'mlist', 'kind': 'episodes', 'tmdb': data['tmdb'], 's': data['s']}, folder=True))
    elif kind == 'person':
        available = person_actions(data)
    else:
        from wcs.library2 import clicks
        action = 'game' if row in GAME_ROWS else 'play' if kind == 'file' else 'open'
        command = ('PlayMedia({})'.format(data['path']) if kind == 'file' and not data['path'].startswith('plugin://') else
                   'ActivateWindow(Music,{},return)'.format(data['path']) if data.get('music') == '1' and data.get('folder') == '1'
                   else clicks.exec_for(data['path'], data.get('folder') == '1')) if data.get('path') else ''
        available = {action: (text(32634 if action == 'open' else LABELS[action]), command)} if command else {}
    if row.startswith('fy_'):
        from wcs.foryou import mode
        available[mode.ACTION] = (mode.label(), mode.builtin())
    available.update(decode(data.get('extras', '')) or {})
    # Inherit the origin through navigation actions as well as through the item's default click.
    for key, (label, command) in list(available.items()):
        if command.startswith('ActivateWindow(') and ',plugin://' in command:
            prefix, path, suffix = command.split(',', 2)
            available[key] = (label, prefix + ',' + with_origin(path, data.get('ctx'), row) + ',' + suffix)
        elif command.startswith('RunPlugin(') and any('action=' + a in command for a in
                ('my_tv_dialog', 'my_cinema_dialog', 'show_wcs_details', 'display_similar_', 'display_actor_dialog', 'display_director_dialog', 'display_writer_dialog')):
            available[key] = (label, 'RunPlugin(' + with_origin(command[len('RunPlugin('):-1], data.get('ctx'), row) + ')')
    return sec, slot, widget, row, available


def parts(data):
    sec, _slot, widget, row, available = resolve(data)
    from wcs.skin import context_profiles
    profile = context_profiles.key(data)
    # A widget controls its media and descendants; unrelated kinds use their shared profile.
    kind = data.get('type')
    inherited = widget and ((kind in ('movie', 'tv', 'season', 'episode') and not row.startswith('vinyl_')) or
                            (kind in ('folder', 'action') and any(a in catalog(row) for a in available)) or
                            (kind == 'channel' and row in ('widget_mytv_channels', 'widget_mycinema_channels')) or
                            (kind == 'vinyl' and row.startswith('vinyl_')))
    if inherited:
        config = normalize(row, widget.get('menu'))
        # Special actions (e.g. editing channel programmes) have their own shared configuration.
        extra_ids = decode(data.get('extras', '')) or {}
        supplemental = context_profiles.config(profile) if any(a not in catalog(row) for a in extra_ids) else None
        if supplemental:
            for group in ('main', 'more', 'hidden'):
                config[group].extend(a for a in supplemental[group] if a in extra_ids and a not in catalog(row))
    else:
        config = context_profiles.config(profile)
    top = [available[a] for a in config['main'] if a in available]
    more = [available[a] for a in config['more'] if a in available]
    if inherited:
        from wcs.skin.slots import TITLES
        more.extend([(text(32373), builtin({'action': 'skin_slots', 'sec': sec})),
                     (text(32704).format(text(TITLES[row])),
                      builtin({'action': 'skin_context_edit', 'ctx': data['ctx']}))])
        from wcs.skin.hero_contract import SECTIONS as HERO_SECTIONS
        if sec in HERO_SECTIONS:
            more.append((text(32692), builtin({'action': 'skin_hero', 'do': 'edit', 'ctx': data['ctx']})))
    elif data.get('sec'):
        from wcs.skin.slots import SECTIONS
        if data['sec'] in SECTIONS:
            more.append((text(32373), builtin({'action': 'skin_slots', 'sec': data['sec']})))
    if not inherited or supplemental:
        more.append((text(32703).format(text(context_profiles.PROFILES[profile][0])),
                     builtin({'action': 'skin_context_edit', 'profile': profile})))
    return top, more


def edit_menu(params):
    if params.get('profile'):
        from wcs.skin import context_profiles
        context_profiles.edit(params['profile'])
    else:
        from wcs.skin import slots
        slots.edit_saved_context(params.get('ctx', ''))


def open_menu(params, channel_dialog=None, execute=True, actions=None):
    if params.get('data'):
        params = unpack(params['data'])
    callbacks = {}
    if actions is not None:
        entries = {}
        for key, (title, command) in actions.items():
            if callable(command):
                callbacks['local:' + key] = command
                command = 'local:' + key
            entries[key] = (title, command)
        params = dict(params, extras=encode(entries))
    top, more = parts(params)
    if not top and not more:
        return
    dialog = xbmcgui.Dialog()
    labels = [label for label, _command in top] + ([text(32420)] if more else [])
    chosen = None
    home = xbmcgui.Window(10000)
    home.setProperty('milionar.ctx.active', '1')
    from wcs.skin import perflog
    perflog.log('context menu open')
    from wcs.skin.menu_anchor import anchored
    try:
        with anchored():                                # a hero button's menu opens where a card's does
            while True:
                index = dialog.contextmenu(labels)
                if index < 0:
                    break
                if index < len(top):
                    chosen = top[index][1]
                    break
                index = dialog.contextmenu([label for label, _command in more])
                if index >= 0:
                    chosen = more[index][1]
                    break
    finally:
        home.clearProperty('milionar.ctx.active')
    if chosen:
        if chosen in callbacks:
            callbacks[chosen]()
            return
        if chosen.startswith('RunPlugin('):
            args = dict(parse_qsl(urlsplit(chosen[len('RunPlugin('):-1]).query))
            if args.get('action') == 'skin_context_edit':
                edit_menu(args)
                return
        if not execute:
            return chosen
        if channel_dialog and 'action=skin_channel_edit' in chosen:
            from wcs.skin.channel_actions import run
            args = dict(parse_qsl(urlsplit(chosen[len('RunPlugin('):-1]).query))
            if args.get('op') == 'edit_channel':
                channels = channel_dialog.channel_manager.get_all_channels()
                channel_dialog.getControl(2000).selectItem(next(i for i, ch in enumerate(channels) if ch.id == args['id']))
                channel_dialog._show_series_grid_dialog()
                return
            run(args)
            channel_dialog.channel_manager._load_channels()
            channel_dialog.refresh_channels_view()
        else:
            run_chosen(chosen)


def run_chosen(command):
    """Run a builtin: a quick action of ours (wcs/skin/local.py) here and now, without starting the plugin for it;
    everything else as the builtin it is."""
    if command.startswith('RunPlugin(' + BASE.rstrip('/')):
        try:
            from wcs.skin import local
            if local.run(dict(parse_qsl(urlsplit(command[len('RunPlugin('):-1]).query))):
                return
        except Exception as exc:                      # never lose the action: the plugin does it instead
            xbmc.log('[Milionář Skin] menu action failed here: {}: {}'.format(type(exc).__name__, exc), xbmc.LOGWARNING)
    xbmc.executebuiltin(command)


def dialog_context(dialog, kind=None):
    """Read the focused item; ordinary buttons keep their existing behavior."""
    try:
        item = dialog.getControl(dialog.getFocusId()).getSelectedItem()
    except (AttributeError, RuntimeError):
        return False
    if not item:
        return False
    raw = item.getProperty('milionar.ctxdata')
    if raw:
        open_menu({'data': raw})
        return True
    item_type = item.getProperty('item_type')
    if item_type in ('add', 'back', 'add_movie', 'add_series', 'back_to_series', 'back_to_seasons') or any(item.getProperty(k) == '1' for k in
        ('is_add_favorite', 'is_start_game', 'is_start_actor_game', 'is_start_director_game', 'is_start_creator_game')):
        return False
    media = item.getProperty('media_type') or item_type or kind or getattr(dialog, 'media_type', 'movie')
    media = 'tv' if media in ('series', 'tvshow') else media
    tmdb = item.getProperty('tmdb_id') or item.getProperty('id')
    if media in ('season', 'episode'):
        tmdb = str(getattr(dialog, 'current_series_id', '') or tmdb)
    if not tmdb or not str(tmdb).isdigit():
        return False
    data = {'type': media, 'tmdb': tmdb, 'title': item.getLabel(),
            's': item.getProperty('season_number'), 'e': item.getProperty('episode_number'),
            'ctx': getattr(dialog, '_context_uid', ''), 'row': getattr(dialog, '_context_row', '')}
    if media == 'person':
        department = item.getProperty('known_for_department')
        data.update(id=tmdb, role='director' if department == 'Directing' else 'writer' if department == 'Writing' else 'actor')
    elif media == 'collection':
        data.update(type='folder', path=BASE + '?' + urlencode({'action': 'show_movie_collection', 'tmdb_id': tmdb}), folder='1')
    open_menu(data)
    return True


def capture_dialog(dialog):
    if not hasattr(dialog, '_context_uid'):
        dialog._context_uid, dialog._context_row = _listing_context


def channel_dialog_context(dialog):
    """The channel dialog's lists inherit the originating widget, too; its other controls keep their behavior."""
    try:
        control = dialog.getControl(dialog.getFocusId())
        item = control.getListItem(control.getSelectedPosition())
    except (AttributeError, RuntimeError):
        return False
    cid = item.getProperty('channel_id')
    if not cid or item.getProperty('is_create_button') == 'true' or item.getProperty('is_add_button') == 'true':
        return False
    kind = dialog.channel_manager.channel_type
    data = {'ctx': dialog._context_uid, 'row': dialog._context_row, 'title': item.getLabel()}
    tmdb = item.getProperty('tmdb_id') or item.getProperty('movie_id') or item.getProperty('series_id')
    if tmdb:
        s, e = item.getProperty('season'), item.getProperty('episode')
        data.update(type='movie' if kind == 'cinema' else 'episode' if s and e else 'tv', tmdb=tmdb,
                    s=s, e=e, channel_id=cid, channel_kind=kind)
        values = {'tmdb_id': tmdb, 'poster': item.getProperty('poster'), 'year': item.getProperty('year')}
        if kind == 'cinema':
            values.update(action='play_movie_from_addon', title=item.getLabel(), mycinema_channel_id=cid)
        else:
            if not s or not e:
                from wcs.ai import ProgressManager
                s, e = ProgressManager.get_progress(cid, tmdb)
                e = int(e) + 1
            values.update(action='play_episode_from_addon', series_name=item.getProperty('series_name') or item.getLabel(),
                          season_number=s, episode_number=e, mytv_channel_id=cid)
        data['channel_play'] = builtin(values)
    else:
        data.update(type='channel', id=cid, kind=kind)
    actions = {}
    if tmdb:
        if dialog.getFocusId() == 9000:
            actions['move_program'] = (text(32732), dialog._enter_program_reorder_mode_direct)
            if kind == 'tv':
                def edit_progress():
                    dialog._play_selection_item = item
                    dialog._show_edit_progress_dialog()
                actions['edit_progress'] = (text(32730), edit_progress)
        elif dialog.getFocusId() == 9100:
            def remove_program():
                if kind == 'cinema':
                    dialog._show_movie_detail_dialog(tmdb, cid)
                else:
                    dialog._show_series_detail_dialog(tmdb, cid)
                dialog._show_delete_series_confirmation()
            actions['remove_program'] = (text(32731), remove_program)
    open_menu(data, channel_dialog=dialog, actions=actions)
    return True


def focused_control():
    """The parent window's selection, even while Kodi's context dialog is active."""
    return str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId())


def focused_data(control):
    if not control.isdigit():
        return ''
    prefix = 'Container({}).ListItem.'.format(control)
    raw = xbmc.getInfoLabel(prefix + 'Property(milionar.ctxdata)')
    if raw:
        return raw
    path = xbmc.getInfoLabel(prefix + 'FolderPath')
    if not path.startswith(BASE):
        return ''
    # Cached items from before universal tagging still go through the same descriptor builder.
    values = dict(parse_qsl(urlsplit(path).query))
    item = xbmcgui.ListItem(xbmc.getInfoLabel(prefix + 'Label'), path=path)
    for name in ('type', 'tmdb', 's', 'e', 'channel_id', 'channel_kind'):
        item.setProperty('milionar.' + name, xbmc.getInfoLabel(prefix + 'Property(milionar.' + name + ')'))
    attach(path, item, xbmc.getCondVisibility(prefix + 'IsFolder'), values.get('ctx', ''), values.get('ctxrow', ''))
    return item.getProperty('milionar.ctxdata')


def native_context():
    """Replace Kodi's generated actions before exposing the popup; our own popup is guarded."""
    home = xbmcgui.Window(10000)
    if home.getProperty('milionar.ctx.active') or home.getProperty('milionar.ctx.native'):
        return
    # Close first: unqualified ListItem info inside the modal dialog does not identify the parent selection.
    xbmc.executebuiltin('Dialog.Close(contextmenu,true)', True)
    if xbmcgui.getCurrentWindowId() == 10000:
        home_context(native=True)
        return
    data = focused_data(focused_control())
    if data:
        open_menu({'data': data})
    elif xbmcgui.getCurrentWindowId() != 11120:
        home.setProperty('milionar.ctx.native', '1')
        xbmc.executebuiltin('Action(ContextMenu)')


def home_context(native=False):
    """Home keymap dispatch: read the actual focus synchronously, including C immediately after navigation."""
    if not family.active():
        xbmc.executebuiltin('Action(ContextMenu)')
        return
    from wcs.skin import slots
    control = focused_control()
    if control == '9000':
        menu_id = xbmc.getInfoLabel('Container(9000).ListItem.Property(id)')
        if menu_id in slots.MENU_SECTIONS:
            slots.open_for_menu_item(menu_id)
        else:
            if native:
                xbmcgui.Window(10000).setProperty('milionar.ctx.native', '1')
            xbmc.executebuiltin('Action(ContextMenu)')
        return
    from wcs.skin import hero_buttons
    widget = hero_buttons.focused_widget(control)          # C / long OK on a hero button: its editor
    if widget:
        slots.edit_saved_hero(widget)
        return
    data = focused_data(control)
    if data:
        open_menu({'data': data})
    else:
        if native:
            xbmcgui.Window(10000).setProperty('milionar.ctx.native', '1')
        xbmc.executebuiltin('Action(ContextMenu)')
