/**
 * KeybindingsManager — loads and merges keyboard shortcut configuration.
 *
 * Default bindings are hardcoded here. Users can override any binding by
 * creating ~/.goodvibes/goodvibes/keybindings.json.
 *
 * Config file format example:
 * {
 *   "search": { "key": "g", "ctrl": true },
 *   "block-copy": { "key": "c", "ctrl": true, "alt": true }
 * }
 *
 * Each value is a KeyCombo or an array of KeyCombos for multi-binding support.
 */
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
/** Human-readable description for each action (used in /keybindings display). */
export const ACTION_DESCRIPTIONS = {
    'copy-selection': 'Copy selected text to clipboard',
    'clear-cancel': 'Clear input / cancel generation / exit (double)',
    'screen-clear': 'Repaint the screen',
    'panel-picker': 'Open, focus, or hide the panel workspace',
    'panel-close': 'Close the currently active panel',
    'panel-close-all': 'Close all open panels',
    'panel-tab-next': 'Next workspace panel tab',
    'panel-tab-prev': 'Previous workspace panel tab',
    'history-search': 'Reverse input history search',
    'search': 'Toggle conversation search',
    'block-copy': 'Copy nearest block to clipboard',
    'bookmark': 'Bookmark / unbookmark nearest block',
    'block-save': 'Save nearest block to file',
    'delete-word': 'Delete word backward',
    'apply-diff-line-start': 'Apply nearest diff / move to line start',
    'next-error-line-end': 'Navigate to next error / move to line end',
    'kill-line': 'Kill to end of line',
    'clear-prompt': 'Clear the prompt',
    'undo': 'Undo last prompt edit',
    'redo': 'Redo last undone edit',
    'paste': 'Paste from clipboard (image priority)',
    'replay-panel': 'Open / close the Replay panel',
};
/** Default key bindings for all actions. */
export const DEFAULT_KEYBINDINGS = {
    'copy-selection': [{ key: 'c', ctrl: true, shift: true }],
    'clear-cancel': [{ key: 'c', ctrl: true }],
    'screen-clear': [{ key: 'l', ctrl: true }],
    'panel-picker': [{ key: 'p', ctrl: true }],
    'panel-close': [{ key: 'x', ctrl: true }],
    'panel-close-all': [{ key: 'x', ctrl: true, shift: true }],
    'panel-tab-next': [{ key: ']', ctrl: true }],
    'panel-tab-prev': [{ key: '[', ctrl: true }],
    'history-search': [{ key: 'r', ctrl: true }],
    'search': [{ key: 'f', ctrl: true }],
    'block-copy': [{ key: 'y', ctrl: true }],
    'bookmark': [{ key: 'b', ctrl: true }],
    'block-save': [{ key: 's', ctrl: true }],
    'delete-word': [{ key: 'w', ctrl: true }],
    'apply-diff-line-start': [{ key: 'a', ctrl: true }],
    'next-error-line-end': [{ key: 'e', ctrl: true }],
    'kill-line': [{ key: 'k', ctrl: true }],
    'clear-prompt': [{ key: 'u', ctrl: true }],
    'undo': [{ key: 'z', ctrl: true }],
    'redo': [{ key: 'z', ctrl: true, shift: true }],
    'paste': [{ key: 'v', ctrl: true }],
    'replay-panel': [{ key: 'r', ctrl: true, shift: true }],
};
function resolveKeybindingsPath(options) {
    if (options?.configPath) {
        return options.configPath;
    }
    const userRoot = options?.userRoot ?? options?.homeDirectory;
    if (!userRoot) {
        throw new Error('KeybindingsManager requires configPath or an explicit userRoot/homeDirectory.');
    }
    return join(userRoot, '.goodvibes', 'goodvibes', 'keybindings.json');
}
/**
 * KeybindingsManager — owns the resolved keybinding table.
 *
 * Call loadFromDisk() once at startup (in main.ts) to merge user config.
 * Then use matches() anywhere a key token is being evaluated.
 */
export class KeybindingsManager {
    bindings;
    configPath;
    constructor(options) {
        this.configPath = resolveKeybindingsPath(options);
        // Start with deep copy of defaults
        this.bindings = this.cloneDefaults();
    }
    cloneDefaults() {
        const result = {};
        for (const [action, combos] of Object.entries(DEFAULT_KEYBINDINGS)) {
            result[action] = combos.map(c => ({ ...c }));
        }
        return result;
    }
    /**
     * Load user overrides from disk and merge into the binding table.
     * Unknown actions are ignored with a debug log. Malformed entries are skipped.
     * Safe to call multiple times (reloads on each call).
     */
    loadFromDisk() {
        if (!existsSync(this.configPath))
            return;
        try {
            const raw = readFileSync(this.configPath, 'utf-8');
            const parsed = JSON.parse(raw);
            const validActions = new Set(Object.keys(DEFAULT_KEYBINDINGS));
            // Reset to defaults before applying overrides
            this.bindings = this.cloneDefaults();
            for (const [action, combo] of Object.entries(parsed)) {
                if (!validActions.has(action)) {
                    logger.debug('keybindings: unknown action, skipping', { action });
                    continue;
                }
                const normalized = Array.isArray(combo) ? combo : [combo];
                if (!this.validateCombos(normalized)) {
                    logger.debug('keybindings: invalid combo for action, skipping', { action, combo });
                    continue;
                }
                this.bindings[action] = normalized;
            }
            logger.debug('keybindings: loaded overrides from disk', { path: this.configPath });
        }
        catch (err) {
            logger.debug('keybindings: failed to load config file', { path: this.configPath, err: summarizeError(err) });
        }
    }
    validateCombos(combos) {
        return combos.every((c) => {
            if (typeof c !== 'object' || c === null)
                return false;
            const combo = c;
            return typeof combo['key'] === 'string' && combo['key'].length > 0;
        });
    }
    /**
     * matches — Check whether a keyboard token matches the given action.
     *
     * @param action  The action to test.
     * @param token   The parsed keyboard token from InputTokenizer.
     *                Expects: { logicalName: string; ctrl?: boolean; shift?: boolean; alt?: boolean }
     */
    matches(action, token) {
        const combos = this.bindings[action];
        if (!combos)
            return false;
        return combos.some((combo) => this.comboMatches(combo, token));
    }
    comboMatches(combo, token) {
        if (token.logicalName !== combo.key)
            return false;
        if (!!combo.ctrl !== !!token.ctrl)
            return false;
        if (!!combo.shift !== !!token.shift)
            return false;
        if (!!combo.alt !== !!token.alt)
            return false;
        return true;
    }
    /**
     * getAll — Return the full resolved binding table for display purposes.
     */
    getAll() {
        return Object.keys(this.bindings).map((action) => ({
            action,
            combos: this.bindings[action],
            description: ACTION_DESCRIPTIONS[action],
        }));
    }
    /**
     * getComboLabel — Return a human-readable label for the first combo of an action.
     * Example: { key: 'f', ctrl: true } → "Ctrl+F"
     */
    getComboLabel(action) {
        const combos = this.bindings[action];
        if (!combos?.length)
            return '(unbound)';
        return this.formatCombo(combos[0]);
    }
    /**
     * formatCombo — Format a KeyCombo as a human-readable string.
     */
    formatCombo(combo) {
        const parts = [];
        if (combo.ctrl)
            parts.push('Ctrl');
        if (combo.alt)
            parts.push('Alt');
        if (combo.shift)
            parts.push('Shift');
        parts.push(combo.key.length === 1 ? combo.key.toUpperCase() : combo.key);
        return parts.join('+');
    }
    /** Return the config file path. */
    getConfigPath() {
        return this.configPath;
    }
}
