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

import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';

import {getOrientationProp} from './shell-compat.js';

const SAM_CONSTS = {
    displayName: 'still App Manager',
    panelIconName: 'package-x-generic-symbolic',
    emptyStateLabel: 'Nothing is queued',
    logPrefix: 'SAM Extension',
    queueChangedSignal: {
        sender: 'io.stillhq.SamService',
        interfaceName: 'io.stillhq.SamService',
        signalName: 'queue_changed',
        objectPath: '/io/stillhq/SamService',
    },
    actionCompletedSignal: {
        sender: 'io.stillhq.SamService',
        interfaceName: 'io.stillhq.SamService',
        signalName: 'action_completed',
        objectPath: '/io/stillhq/SamService',
    },
};

function getSamActionIcon(action) {
    switch (action.task) {
        case 'Installing':
        case 'INSTALL':
            return 'system-software-install-symbolic';
        case 'Updating':
        case 'UPDATE':
            return 'software-update-available-symbolic';
        case 'Removing':
        case 'REMOVE':
            return 'edit-delete-symbolic';
        default:
            return 'application-x-executable-symbolic';
    }
}

function getSamActionLabel(action) {
    if (action.isRunning) {
        switch (action.task) {
            case 'Installing':
            case 'INSTALL':
                return 'Installing...';
            case 'Updating':
            case 'UPDATE':
                return 'Updating...';
            case 'Removing':
            case 'REMOVE':
                return 'Removing...';
            default:
                return 'Running Action...';
        }
    }

    switch (action.task) {
        case 'Installing':
        case 'INSTALL':
            return 'Installation Queued...';
        case 'Updating':
        case 'UPDATE':
            return 'Update Queued...';
        case 'Removing':
        case 'REMOVE':
            return 'Removal Queued...';
        default:
            return 'Action Queued...';
    }
}

class Action {
    constructor({package_id, app_name, manager_id, task, progress, error, background, status, tags}) {
        this.package_id = package_id;
        this.app_name = app_name;
        this.manager_id = manager_id;
        this.task = task;
        this.progress = parseInt(progress) || 0;
        this.error = error || '';
        this.background = background === 'true' || background === true;
        this.status = status || '';
        this.isRunning = false;
        this.tags = Array.isArray(tags) ? tags : [];
    }

    static fromDict(dict) {
        return new Action(dict);
    }
}

const BUS_NAME = 'io.stillhq.SamService';
const OBJECT_PATH = '/io/stillhq/SamService';
const IFACE = 'io.stillhq.SamService';

const MANAGER_PRIORITY = {
    system: 0,
    flathub: 1,
    fedora: 2,
    swai: 3,
};

function getManagerPriority(managerId) {
    return MANAGER_PRIORITY[managerId] ?? 999;
}

let _samProxy = null;

function getSamProxy() {
    if (!_samProxy) {
        try {
            _samProxy = Gio.DBusProxy.new_sync(
                Gio.DBus.system,
                Gio.DBusProxyFlags.NONE,
                null,
                BUS_NAME,
                OBJECT_PATH,
                IFACE,
                null
            );
            log('SAM Extension: Created DBus proxy successfully');
        } catch (e) {
            log('SAM Extension: Failed to create DBus proxy: ' + e);
            return null;
        }
    }
    return _samProxy;
}

function getSamQueue() {
    let proxy;
    try {
        proxy = getSamProxy();
        if (!proxy) {
            log('SAM Extension: No proxy available, returning empty array');
            return [];
        }
        let [actions] = proxy.call_sync(
            'get_queue_actions_dict',
            null,
            Gio.DBusCallFlags.NONE,
            -1,
            null
        ).deep_unpack();
        if (!actions)
            return [];

        let parsedActions = actions.map(dict => {
            try {
                return Action.fromDict(dict);
            } catch (e) {
                log('SAM Extension: Error creating Action from dict: ' + e + ' for dict: ' + JSON.stringify(dict));
                return null;
            }
        }).filter(action => action !== null);

        let managerQueues = {};
        for (let action of parsedActions) {
            let managerId = action.manager_id || 'unknown';
            if (!managerQueues[managerId])
                managerQueues[managerId] = [];
            managerQueues[managerId].push(action);
        }

        for (let managerId in managerQueues) {
            if (managerQueues[managerId].length > 0)
                managerQueues[managerId][0].isRunning = true;
        }

        let result = [];
        let managerIds = Object.keys(managerQueues).sort((a, b) => getManagerPriority(a) - getManagerPriority(b));
        for (let managerId of managerIds) {
            for (let action of managerQueues[managerId])
                result.push(action);
        }

        return result;
    } catch (e) {
        log('SAM Extension: Failed to get SAM queue: ' + e);
        return [];
    }
}

function getActionKey(action) {
    return `${action.manager_id}:${action.package_id}`;
}

const Indicator = GObject.registerClass(
class Indicator extends PanelMenu.Button {
    _init() {
        super._init(0.0, _(SAM_CONSTS.displayName));
        this._icon = new St.Icon({
            icon_name: SAM_CONSTS.panelIconName,
            style_class: 'system-status-icon',
        });
        this.add_child(this._icon);
        this._queueChangedSignal = null;
        this._actionCompletedSignal = null;
        this._notificationSource = null;
        this._queueWidgets = new Map();
        this._lastProgressMap = new Map();
        this._emptyItem = null;
        this._refreshTimer = null;

        this._createScrollableMenu();

        this.menu.connect('open-state-changed', (menu, open) => {
            if (open) {
                this._updateMenuAlignment();
                this._refreshQueue();
            }
        });
        this._refreshQueue();

        try {
            const signal = SAM_CONSTS.queueChangedSignal;
            this._queueChangedSignal = Gio.DBus.system.signal_subscribe(
                signal.sender,
                signal.interfaceName,
                signal.signalName,
                signal.objectPath,
                null,
                Gio.DBusSignalFlags.NONE,
                () => {
                    this._refreshQueue();
                }
            );
        } catch (e) {
            log(`${SAM_CONSTS.logPrefix}: Failed to subscribe to queue_changed signal: ${e}`);
        }

        try {
            const signal = SAM_CONSTS.actionCompletedSignal;
            this._actionCompletedSignal = Gio.DBus.system.signal_subscribe(
                signal.sender,
                signal.interfaceName,
                signal.signalName,
                signal.objectPath,
                null,
                Gio.DBusSignalFlags.NONE,
                (_connection, _sender, _path, _interface, _signal, parameters) => {
                    const [dict] = parameters.deep_unpack();
                    this._notifySystemUpdate(Action.fromDict(dict));
                }
            );
        } catch (e) {
            log(`${SAM_CONSTS.logPrefix}: Failed to subscribe to action_completed signal: ${e}`);
        }

        this._refreshTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
            this._refreshQueue();
            return GLib.SOURCE_CONTINUE;
        });
    }

    _createScrollableMenu() {
        let monitor = Main.layoutManager.primaryMonitor;
        let maxHeight = monitor ? Math.floor(monitor.height * 0.6) : 600;

        this._scrollView = new St.ScrollView({
            style_class: 'sam-scroll-view',
            hscrollbar_policy: St.PolicyType.NEVER,
            vscrollbar_policy: St.PolicyType.AUTOMATIC,
            overlay_scrollbars: true,
            style: `max-height: ${maxHeight}px;`,
        });

        this._scrollBox = new St.BoxLayout({
            ...getOrientationProp(true),
            style_class: 'sam-scroll-box',
        });

        this._scrollView.set_child(this._scrollBox);
        this.menu.box.add_child(this._scrollView);
    }

    _notifySystemUpdate(action) {
        if (action.manager_id !== 'system' || !action.tags.includes('system'))
            return;

        let body = _('A system update will be applied on reboot.');
        if (action.tags.includes('nvidia'))
            body += ' ' + _('Reboot to apply NVIDIA drivers.');

        try {
            const source = this._getNotificationSource();
            const notification = new MessageTray.Notification({
                source,
                title: _('System update ready'),
                body,
            });
            notification.iconName = 'software-update-available-symbolic';
            source.addNotification(notification);
        } catch (e) {
            log(`${SAM_CONSTS.logPrefix}: Failed to show system update notification: ${e}`);
        }
    }

    _getNotificationSource() {
        if (MessageTray.getSystemSource)
            return MessageTray.getSystemSource();
        if (!this._notificationSource) {
            this._notificationSource = new MessageTray.Source({
                title: 'stillOS',
                iconName: 'software-update-available-symbolic',
            });
            Main.messageTray.add(this._notificationSource);
        }
        return this._notificationSource;
    }

    _updateMenuAlignment() {
        let boxPointer = this.menu._boxPointer;

        // BoxPointer uses source alignment to position the popup. Updating only
        // PopupMenu's arrow field changes the arrow but leaves the popup itself
        // centered, which is especially visible in a bottom Dash to Panel.
        boxPointer.setSourceAlignment(0.0);
        this.menu._arrowAlignment = 0.0;
    }

    _addQueueItem(item) {
        this._scrollBox.add_child(item);
    }

    _clearQueueItems() {
        this._scrollBox.destroy_all_children();
    }

    _refreshQueue() {
        let queue = getSamQueue();

        if (!queue || queue.length === 0) {
            this.hide();
            if (this.menu.isOpen) {
                this._clearQueueItems();
                this._emptyItem = new QueueItemWidget({
                    icon_name: 'dialog-information-symbolic',
                    app_name: _(SAM_CONSTS.emptyStateLabel),
                    action_text: '',
                    progress: 0,
                });
                this._addQueueItem(this._emptyItem);
            }
            this._queueWidgets.clear();
            this._lastProgressMap.clear();
            return;
        }

        this.show();
        let currentKeys = new Set();
        for (let action of queue)
            currentKeys.add(getActionKey(action));

        let existingKeys = new Set(this._queueWidgets.keys());
        let structureChanged = false;

        if (existingKeys.size !== currentKeys.size) {
            structureChanged = true;
        } else {
            for (let key of currentKeys) {
                if (!existingKeys.has(key)) {
                    structureChanged = true;
                    break;
                }
            }
        }

        if (structureChanged) {
            this._clearQueueItems();
            this._queueWidgets.clear();

            for (let action of queue) {
                try {
                    let key = getActionKey(action);
                    let item = new QueueItemWidget({
                        icon_name: getSamActionIcon(action),
                        app_name: action.app_name,
                        action_text: getSamActionLabel(action),
                        progress: action.progress,
                    });
                    this._addQueueItem(item);
                    this._queueWidgets.set(key, item);
                    this._lastProgressMap.set(key, action.progress);
                } catch (e) {
                    log(`${SAM_CONSTS.logPrefix}: Error creating menu item for ${action.app_name}: ${e}`);
                }
            }
        } else {
            for (let action of queue) {
                let key = getActionKey(action);
                let widget = this._queueWidgets.get(key);
                let lastProgress = this._lastProgressMap.get(key);

                if (widget && lastProgress !== action.progress) {
                    widget.setProgress(getSamActionLabel(action), action.progress);
                    this._lastProgressMap.set(key, action.progress);
                }
            }
        }
    }

    destroy() {
        if (this._refreshTimer) {
            GLib.source_remove(this._refreshTimer);
            this._refreshTimer = null;
        }
        if (this._queueChangedSignal) {
            Gio.DBus.system.signal_unsubscribe(this._queueChangedSignal);
            this._queueChangedSignal = null;
        }
        if (this._actionCompletedSignal) {
            Gio.DBus.system.signal_unsubscribe(this._actionCompletedSignal);
            this._actionCompletedSignal = null;
        }
        if (this._notificationSource) {
            this._notificationSource.destroy();
            this._notificationSource = null;
        }
        super.destroy();
    }
});

const QueueItemWidget = GObject.registerClass(
class QueueItemWidget extends St.BoxLayout {
    _init({icon_name, app_name, action_text, progress}) {
        super._init({
            style_class: 'queue-item popup-menu-item',
            reactive: true,
            can_focus: true,
            track_hover: true,
            x_expand: true,
        });

        this._icon = new St.Icon({
            icon_name: icon_name || 'application-x-executable-symbolic',
            style_class: 'queue-item-icon',
            icon_size: 32,
        });
        this.add_child(this._icon);

        let textBox = new St.BoxLayout({
            ...getOrientationProp(true),
            x_expand: true,
            y_expand: false,
            style_class: 'queue-item-textbox',
        });
        this.add_child(textBox);

        this._label = new St.Label({
            text: app_name || '',
            style_class: 'queue-item-label-bold',
            x_expand: true,
        });
        textBox.add_child(this._label);

        let progressRow = new St.BoxLayout({
            ...getOrientationProp(false),
            x_expand: true,
            style_class: 'queue-item-progress-row',
        });
        textBox.add_child(progressRow);

        this._statusLabel = new St.Label({
            text: action_text ?? '',
            style_class: 'queue-item-progress-subtitle',
            x_expand: true,
        });
        progressRow.add_child(this._statusLabel);

        this._percentLabel = new St.Label({
            text: (progress !== undefined && progress !== null && !isNaN(progress) && progress !== 0) ? `${progress}%` : '',
            style_class: 'queue-item-progress-subtitle',
            x_expand: true,
            x_align: Clutter.ActorAlign.END,
        });
        progressRow.add_child(this._percentLabel);
    }

    setProgress(action_text, progress) {
        try {
            if (this._statusLabel && !this._statusLabel.destroyed)
                this._statusLabel.text = action_text ?? '';
            if (this._percentLabel && !this._percentLabel.destroyed)
                this._percentLabel.text = (progress !== undefined && progress !== null && !isNaN(progress) && progress !== 0) ? `${progress}%` : '';
        } catch (e) {
            log(`${SAM_CONSTS.logPrefix}: Error in setProgress: ${e}`);
        }
    }
});

export default class SamIndicator {
    enable(uuid) {
        this._indicator = new Indicator();
        Main.panel.addToStatusArea(uuid, this._indicator);
    }

    disable() {
        if (this._indicator) {
            this._indicator.destroy();
            this._indicator = null;
        }
    }
}
