#!/usr/bin/env python
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>

# Entry point: collect_keys_data(opts) — collects all keybinding, alias, and
# mouse data into a JSON-serializable dict consumed by the Go TUI.

import sys
from functools import partial
from typing import Any

from kitty.conf.types import Definition
from kitty.fast_data_types import add_timer, get_boss
from kitty.typing_compat import BossType

from ..tui.handler import result_handler

definition = Definition(
    '!kittens.command_palette',
)

agr = definition.add_group
egr = definition.end_group
map = definition.add_map

# shortcuts {{{
agr('shortcuts', 'Keyboard shortcuts')

map('Move selection up',
    'selection_up --allow-fallback=shifted,ascii ctrl+k selection_up',
    )
map('Move selection up',
    'selection_up --allow-fallback=shifted,ascii ctrl+p selection_up',
    )
map('Move selection down',
    'selection_down --allow-fallback=shifted,ascii ctrl+j selection_down',
    )
map('Move selection down',
    'selection_down --allow-fallback=shifted,ascii ctrl+n selection_down',
    )

egr()  # }}}


def classify_action(
    definition: str, alias_map: Any, action_to_group: dict[str, str]
) -> tuple[str, str, str]:
    """Classify a keybinding definition into (action_name, category, alias).

    Returns the resolved action name, the category it belongs to, and the alias
    name if the definition uses one (empty string otherwise).
    """
    raw_action = definition.split()[0] if definition else 'no_op'
    if raw_action == 'combine':
        return 'combine', 'Combined actions', ''
    if raw_action in action_to_group or raw_action == 'no_op':
        return raw_action, action_to_group.get(raw_action, 'Miscellaneous'), ''
    # Not a known action — try alias resolution
    resolved = alias_map.resolve_aliases(definition)
    if resolved:
        action_name = resolved[0].func
        return action_name, 'Action aliases', raw_action
    return raw_action, 'Miscellaneous', ''


def build_action_lookups() -> tuple[dict[str, str], dict[str, str], dict[str, str]]:
    """Build action->group, action->help, and action->long_help lookups."""
    from kitty.actions import get_all_actions, groups

    action_to_group: dict[str, str] = {}
    action_to_help: dict[str, str] = {}
    action_to_long_help: dict[str, str] = {}
    for group_key, actions in get_all_actions().items():
        for action in actions:
            action_to_group[action.name] = groups[group_key]
            action_to_help[action.name] = action.short_help
            action_to_long_help[action.name] = action.long_help
    return action_to_group, action_to_help, action_to_long_help


def deduplicate_definitions(defns: list[Any]) -> list[Any]:
    """Return unique definitions, keeping the last occurrence of each."""
    seen: set[tuple[Any, ...]] = set()
    uniq: list[Any] = []
    for d in reversed(defns):
        uid = d.unique_identity_within_keymap
        if uid not in seen:
            seen.add(uid)
            uniq.append(d)
    return uniq


def build_binding_entry(
    key_repr: str, action_repr: str, definition: str, alias_map: Any,
    action_to_group: dict[str, str], action_to_help: dict[str, str],
    action_to_long_help: dict[str, str],
) -> tuple[dict[str, str], str]:
    """Build a single binding entry dict and return it with its category."""
    action_name, category, alias = classify_action(definition, alias_map, action_to_group)
    entry: dict[str, str] = {
        'key': key_repr,
        'action': action_name,
        'action_display': action_repr,
        'definition': definition or action_name,
        'help': action_to_help.get(action_name, ''),
        'long_help': action_to_long_help.get(action_name, ''),
    }
    if alias:
        entry['alias'] = alias
    return entry, category


def order_categories(
    categories: dict[str, list[dict[str, str]]], group_order: list[str]
) -> dict[str, list[dict[str, str]]]:
    """Order categories by the groups order, with remaining categories sorted at the end."""
    ordered: dict[str, list[dict[str, str]]] = {}
    for group_title in group_order:
        if group_title in categories:
            ordered[group_title] = categories.pop(group_title)
    for cat_name, binds in sorted(categories.items()):
        ordered[cat_name] = binds
    return ordered


def collect_keyboard_bindings(
    opts: Any,
    action_to_group: dict[str, str],
    action_to_help: dict[str, str],
    action_to_long_help: dict[str, str],
) -> dict[str, dict[str, list[dict[str, str]]]]:
    """Collect keybindings from all keyboard modes into categorized dicts."""
    from kitty.actions import groups
    from kitty.options.utils import KeyDefinition
    from kitty.types import Shortcut

    def as_sc(k: 'Any', v: KeyDefinition) -> Shortcut:
        if v.is_sequence:
            return Shortcut((v.trigger,) + v.rest)
        return Shortcut((k,))

    modes: dict[str, dict[str, list[dict[str, str]]]] = {}
    for mode_name, mode in opts.keyboard_modes.items():
        categories: dict[str, list[dict[str, str]]] = {}
        for key, defns in mode.keymap.items():
            for d in deduplicate_definitions(defns):
                sc = as_sc(key, d)
                entry, category = build_binding_entry(
                    sc.human_repr(opts.kitty_mod), d.human_repr(),
                    d.definition or '', opts.alias_map,
                    action_to_group, action_to_help, action_to_long_help)
                categories.setdefault(category, []).append(entry)
        for cat in categories:
            categories[cat].sort(key=lambda b: b['key'])
        modes[mode_name] = order_categories(categories, list(groups.values()))
    return modes


def relocate_mode_entry_bindings(
    modes: dict[str, dict[str, list[dict[str, str]]]]
) -> None:
    """Move push_keyboard_mode bindings into their target mode's section."""
    if '' not in modes:
        return
    new_default_cats: dict[str, list[dict[str, str]]] = {}
    for cat_name, bindings in modes[''].items():
        keep: list[dict[str, str]] = []
        for b in bindings:
            if b['action'] == 'push_keyboard_mode':
                parts = b['definition'].split()
                target = parts[1] if len(parts) > 1 else ''
                if target and target in modes:
                    if 'Enter mode' not in modes[target]:
                        new_target: dict[str, list[dict[str, str]]] = {'Enter mode': [b]}
                        new_target.update(modes[target])
                        modes[target] = new_target
                    else:
                        modes[target]['Enter mode'].append(b)
                    continue
            keep.append(b)
        if keep:
            new_default_cats[cat_name] = keep
    modes[''] = new_default_cats


def add_unmapped_actions(
    modes: dict[str, dict[str, list[dict[str, str]]]]
) -> None:
    """Add actions with no keyboard shortcut to the default mode."""
    from kitty.actions import get_all_actions, groups

    mapped_actions: set[str] = set()
    for mode_cats in modes.values():
        for bindings in mode_cats.values():
            for b in bindings:
                mapped_actions.add(b['action'])

    default_mode_cats = modes.setdefault('', {})
    for group_key, actions in get_all_actions().items():
        category = groups[group_key]
        for action in actions:
            if action.name not in mapped_actions:
                default_mode_cats.setdefault(category, []).append({
                    'key': '',
                    'action': action.name,
                    'action_display': action.name,
                    'definition': action.name,
                    'help': action.short_help,
                    'long_help': action.long_help,
                })

    # Re-sort: mapped entries (non-empty key) first, then unmapped by action name.
    for cat in default_mode_cats:
        default_mode_cats[cat].sort(key=lambda b: (b['key'] == '', b['key'] or b['action']))

    # Re-order by groups ordering
    modes[''] = order_categories(default_mode_cats, list(groups.values()))


def collect_bound_aliases(
    modes: dict[str, dict[str, list[dict[str, str]]]]
) -> set[str]:
    """Collect alias names that are already present from keybindings."""
    bound: set[str] = set()
    for mode_cats in modes.values():
        for bindings in mode_cats.values():
            bound.update(b['alias'] for b in bindings if 'alias' in b)
    return bound


def build_alias_entry(alias_map: Any, display: str, expansion: str) -> dict[str, str]:
    """Build a single alias section entry."""
    resolved = alias_map.resolve_aliases(display)
    resolved_action = resolved[0].func if resolved else display.split()[0]
    return {
        'key': '',
        'action': resolved_action,
        'action_display': display,
        'definition': expansion,
        'help': f'Alias for: {expansion}',
        'long_help': '',
        'alias': display,
    }


def add_alias_sections(
    opts: Any,
    modes: dict[str, dict[str, list[dict[str, str]]]]
) -> None:
    """Add Action aliases and Kitten aliases sections for unbound aliases."""
    bound_aliases = collect_bound_aliases(modes)

    action_alias_entries: list[dict[str, str]] = []
    kitten_alias_entries: list[dict[str, str]] = []
    for alias_name, alias_list in opts.alias_map.aliases.items():
        for aa in alias_list:
            if aa.replace_second_arg:
                display = f'{alias_name} {aa.name}'
                expansion = f'{alias_name} {aa.value}'
                target_list = kitten_alias_entries
            else:
                display = aa.name
                expansion = aa.value
                target_list = action_alias_entries
            if display not in bound_aliases:
                target_list.append(build_alias_entry(opts.alias_map, display, expansion))

    default_mode_cats = modes.setdefault('', {})
    def sort_key(b: dict[str, str]) -> tuple[bool, str]:
        return (b['key'] == '', b['key'] or b['action_display'])

    for section_name, entries in (('Action aliases', action_alias_entries), ('Kitten aliases', kitten_alias_entries)):
        if entries:
            existing = default_mode_cats.get(section_name, [])
            existing.extend(entries)
            existing.sort(key=sort_key)
            default_mode_cats[section_name] = existing


def collect_mouse_bindings(opts: Any) -> list[dict[str, str]]:
    """Collect mouse mappings."""
    mouse: list[dict[str, str]] = []
    for event, action in opts.mousemap.items():
        key_repr = event.human_repr(opts.kitty_mod)
        mouse.append({'key': key_repr, 'action': action, 'action_display': action, 'help': '', 'long_help': ''})
    mouse.sort(key=lambda b: b['key'])
    return mouse


def collect_keys_data(opts: Any) -> dict[str, Any]:
    """Collect all keybinding data from options into a JSON-serializable dict."""
    action_to_group, action_to_help, action_to_long_help = build_action_lookups()
    modes = collect_keyboard_bindings(opts, action_to_group, action_to_help, action_to_long_help)
    relocate_mode_entry_bindings(modes)
    add_unmapped_actions(modes)
    add_alias_sections(opts, modes)

    mode_order = list(modes.keys())
    category_order: dict[str, list[str]] = {}
    for mode_name, cats in modes.items():
        category_order[mode_name] = list(cats.keys())

    return {
        'modes': modes,
        'mouse': collect_mouse_bindings(opts),
        'mode_order': mode_order,
        'category_order': category_order,
    }


def main(args: list[str]) -> None:
    raise SystemExit('This kitten must be used only from a kitty.conf mapping')


def callback(target_window_id: int, action: str, timer_id: int | None) -> None:
    boss = get_boss()
    w = boss.window_id_map.get(target_window_id)
    boss.combine(action, w)


@result_handler(has_ready_notification=True)
def handle_result(args: list[str], data: dict[str, Any], target_window_id: int, boss: BossType) -> None:
    if data and (action := data.get('action')):
        # run action after event loop tick so command palette overlay is closed
        add_timer(partial(callback, target_window_id, action), 0, False)

help_text = 'Browse and trigger keyboard shortcuts and actions'
usage = ''
OPTIONS = r'''
'''.format


if __name__ == '__main__':
    main(sys.argv)
elif __name__ == '__doc__':
    cd = sys.cli_docs  # type: ignore
    cd['usage'] = usage
    cd['options'] = OPTIONS
    cd['help_text'] = help_text
    cd['short_desc'] = help_text
elif __name__ == '__conf__':
    sys.options_definition = definition  # type: ignore
