// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';

import Plugin from '../plugin.js';


export const Metadata = {
    label: _('Run Commands'),
    id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.RunCommand',
    description: _('Run commands on your paired device or let the device run predefined commands on this PC'),
    incomingCapabilities: [
        'kdeconnect.runcommand',
        'kdeconnect.runcommand.request',
    ],
    outgoingCapabilities: [
        'kdeconnect.runcommand',
        'kdeconnect.runcommand.request',
    ],
    actions: {
        commands: {
            label: _('Commands'),
            icon_name: 'system-run-symbolic',

            parameter_type: new GLib.VariantType('s'),
            incoming: ['kdeconnect.runcommand'],
            outgoing: ['kdeconnect.runcommand.request'],
        },
        executeCommand: {
            label: _('Commands'),
            icon_name: 'system-run-symbolic',

            parameter_type: new GLib.VariantType('s'),
            incoming: ['kdeconnect.runcommand'],
            outgoing: ['kdeconnect.runcommand.request'],
        },
    },
};


/**
 * RunCommand Plugin
 * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/remotecommands
 * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/runcommand
 */
const RunCommandPlugin = GObject.registerClass({
    GTypeName: 'GSConnectRunCommandPlugin',
    Properties: {
        'remote-commands': GObject.param_spec_variant(
            'remote-commands',
            'Remote Command List',
            'A list of the device\'s remote commands',
            new GLib.VariantType('a{sv}'),
            null,
            GObject.ParamFlags.READABLE
        ),
    },
}, class RunCommandPlugin extends Plugin {

    _init(device) {
        super._init(device, 'runcommand');

        // Local Commands
        this._commandListChangedId = this.settings.connect(
            'changed::command-list',
            this._sendCommandList.bind(this)
        );

        // We cache remote commands so they can be used in the settings even
        // when the device is offline.
        this._remote_commands = {};
        this.cacheProperties(['_remote_commands']);
    }

    get remote_commands() {
        return this._remote_commands;
    }

    connected() {
        super.connected();

        this._sendCommandList();
        this._requestCommandList();
        this._handleCommandList(this.remote_commands);
    }

    clearCache() {
        this._remote_commands = {};
        this.notify('remote-commands');
    }

    cacheLoaded() {
        if (!this.device.connected)
            return;

        this._sendCommandList();
        this._requestCommandList();
        this._handleCommandList(this.remote_commands);
    }

    handlePacket(packet) {
        switch (packet.type) {
            case 'kdeconnect.runcommand':
                this._handleCommandList(packet.body.commandList);
                break;

            case 'kdeconnect.runcommand.request':
                if (packet.body.hasOwnProperty('key'))
                    this._handleCommand(packet.body.key);

                else if (packet.body.hasOwnProperty('requestCommandList'))
                    this._sendCommandList();

                break;
        }
    }

    /**
     * Handle a request to execute the local command with the UUID @key
     *
     * @param {string} key - The UUID of the local command
     */
    _handleCommand(key) {
        try {
            const commands = this.settings.get_value('command-list');
            const commandList = commands.recursiveUnpack();

            if (!commandList.hasOwnProperty(key)) {
                throw new Gio.IOErrorEnum({
                    code: Gio.IOErrorEnum.PERMISSION_DENIED,
                    message: `Unknown command: ${key}`,
                });
            }

            this.device.launchProcess([
                '/bin/sh',
                '-c',
                commandList[key].command,
            ]);
        } catch (e) {
            logError(e, this.device.name);
        }
    }

    /**
     * Parse the response to a request for the remote command list. Remove the
     * command menu if there are no commands, otherwise amend the menu.
     *
     * @param {string | object[]} commandList - A list of remote commands
     */
    _handleCommandList(commandList) {
        // See: https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1051
        if (typeof commandList === 'string') {
            try {
                commandList = JSON.parse(commandList);
            } catch {
                commandList = {};
            }
        }

        this._remote_commands = commandList;
        this.notify('remote-commands');

        const commandEntries = Object.entries(this.remote_commands);

        // If there are no commands, hide the menu by disabling the action
        this.device.lookup_action('commands').enabled = (commandEntries.length > 0);

        // Commands Submenu
        const submenu = new Gio.Menu();

        for (const [uuid, info] of commandEntries) {
            const item = new Gio.MenuItem();
            item.set_label(info.name);
            item.set_icon(
                new Gio.ThemedIcon({name: 'application-x-executable-symbolic'})
            );
            item.set_detailed_action(`device.executeCommand::${uuid}`);
            submenu.append_item(item);
        }

        // Commands Item
        const item = new Gio.MenuItem();
        item.set_detailed_action('device.commands::menu');
        item.set_attribute_value(
            'hidden-when',
            new GLib.Variant('s', 'action-disabled')
        );
        item.set_icon(new Gio.ThemedIcon({name: 'system-run-symbolic'}));
        item.set_label(_('Commands'));
        item.set_submenu(submenu);

        // If the submenu item is already present it will be replaced
        const menuActions = this.device.settings.get_strv('menu-actions');
        const index = menuActions.indexOf('commands');

        if (index > -1) {
            this.device.removeMenuAction('device.commands');
            this.device.addMenuItem(item, index);
        }
    }

    /**
     * Send a request for the remote command list
     */
    _requestCommandList() {
        this.device.sendPacket({
            type: 'kdeconnect.runcommand.request',
            body: {requestCommandList: true},
        });
    }

    /**
     * Send the local command list
     */
    _sendCommandList() {
        const commands = this.settings.get_value('command-list').recursiveUnpack();
        const commandList = JSON.stringify(commands);

        this.device.sendPacket({
            type: 'kdeconnect.runcommand',
            body: {commandList: commandList},
        });
    }

    /**
     * Placeholder function for command action
     */
    commands() {}

    /**
     * Send a request to execute the remote command with the UUID @key
     *
     * @param {string} key - The UUID of the remote command
     */
    executeCommand(key) {
        this.device.sendPacket({
            type: 'kdeconnect.runcommand.request',
            body: {key: key},
        });
    }

    destroy() {
        this.settings.disconnect(this._commandListChangedId);

        super.destroy();
    }
});

export default RunCommandPlugin;
