import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk';
import Adw from 'gi://Adw';
import Soup from 'gi://Soup';

import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';

const ALL_COLORS = [
    'blue', 'brown', 'green', 'orange', 'pink',
    'purple', 'red', 'slate', 'teal', 'yellow',
];

const COLOR_META = {
    blue:   { label: 'Blue',   hex: '#3584e4' },
    brown:  { label: 'Brown',  hex: '#986a44' },
    green:  { label: 'Green',  hex: '#3a944a' },
    orange: { label: 'Orange', hex: '#e66100' },
    pink:   { label: 'Pink',   hex: '#d56199' },
    purple: { label: 'Purple', hex: '#9141ac' },
    red:    { label: 'Red',    hex: '#e01b24' },
    slate:  { label: 'Slate',  hex: '#6f8396' },
    maia:   { label: 'Maia',   hex: '#16a085' },
    teal:   { label: 'Teal',   hex: '#2190a4' },
    yellow: { label: 'Yellow', hex: '#e5a50a' },
};

const ACCENT_TO_COLOR = {
    blue: 'blue', brown: 'brown', teal: 'teal', green: 'green', yellow: 'yellow',
    orange: 'orange', red: 'red', pink: 'pink', purple: 'purple',
    slate: 'slate', maia: 'maia', default: 'maia',
};

const ICON_PATHS = [
    GLib.get_home_dir() + '/.local/share/icons',
    GLib.get_home_dir() + '/.icons',
    '/usr/local/share/icons',
    '/var/usr/local/share/icons',
    '/usr/share/icons',
];

const MOREWAITA_PATHS = [
    GLib.get_home_dir() + '/.local/share/icons/MoreWaita',
    GLib.get_home_dir() + '/.icons/MoreWaita',
    '/usr/local/share/icons/MoreWaita',
    '/var/usr/local/share/icons/MoreWaita',
    '/usr/share/icons/MoreWaita',
];

const GITHUB_API_URL         = 'https://api.github.com/repos/dpejoh/Adwaita-colors/releases/latest';
const GITHUB_RELEASES_URL    = 'https://github.com/dpejoh/Adwaita-colors/releases/latest';
const GITHUB_PAGE_URL        = 'https://github.com/dpejoh/Adwaita-colors';
const GITHUB_ISSUES_URL      = 'https://github.com/dpejoh/Adwaita-colors/issues';
const GITHUB_HOME_ISSUES_URL = 'https://github.com/dpejoh/Adwaita-Colors-Home/issues';
const MOREWAITA_URL          = 'https://github.com/somepaulo/MoreWaita';
const MOREWAITA_API_URL      = 'https://api.github.com/repos/somepaulo/MoreWaita/releases/latest';

function detectInstallation() {
    const installedColors = [];
    let firstPath = null;

    for (const base of ICON_PATHS) {
        for (const color of ALL_COLORS) {
            if (GLib.file_test(`${base}/Adwaita-${color}/index.theme`, GLib.FileTest.EXISTS)) {
                if (!firstPath) firstPath = base;
                if (!installedColors.includes(color)) installedColors.push(color);
            }
        }
    }

    return { installed: installedColors.length > 0, path: firstPath, installedColors };
}

function detectMoreWaita() {
    for (const base of MOREWAITA_PATHS) {
        if (GLib.file_test(`${base}/index.theme`, GLib.FileTest.EXISTS)) {
            const installBase = base.slice(0, base.lastIndexOf('/'));
            return { found: true, path: base, installBase };
        }
    }
    return { found: false, path: null, installBase: null };
}

function detectDistroType() {
    if (GLib.file_test('/run/ostree-booted', GLib.FileTest.EXISTS))
        return 'atomic';
    try {
        const [, data] = GLib.file_get_contents('/etc/os-release');
        if (/VARIANT_ID=(silverblue|kinoite|sericea|onyx)/i.test(new TextDecoder().decode(data)))
            return 'atomic';
    } catch (_) {}
    return 'standard';
}

function parseSemver(v) {
    return (v ?? '').replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
}

function isNewer(candidate, installed) {
    const [ca, cb, cc] = parseSemver(candidate);
    const [ia, ib, ic] = parseSemver(installed);
    if (ca !== ia) return ca > ia;
    if (cb !== ib) return cb > ib;
    return cc > ic;
}

function openUri(uri) {
    Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null);
}

function makeColorDot(hex, size = 16) {
    const area = new Gtk.DrawingArea({ width_request: size, height_request: size });
    area.set_draw_func((_w, cr) => {
        cr.arc(size / 2, size / 2, size / 2 - 1, 0, 2 * Math.PI);
        cr.setSourceRGB(
            parseInt(hex.slice(1, 3), 16) / 255,
            parseInt(hex.slice(3, 5), 16) / 255,
            parseInt(hex.slice(5, 7), 16) / 255
        );
        cr.fillPreserve();
        cr.setSourceRGBA(0, 0, 0, 0.25);
        cr.setLineWidth(1);
        cr.stroke();
    });
    return area;
}

function needsPrivileges(path) {
    return path.startsWith('/usr/') ||
           path.startsWith('/var/usr/local/') ||
           path.startsWith('/usr/local/');
}

export default class AdwaitaColorsPreferences extends ExtensionPreferences {
    fillPreferencesWindow(window) {
        this._settings        = this.getSettings();
        this._desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
        this._window          = window;
        this._distroType      = detectDistroType();
        this._installation    = detectInstallation();
        this._morewaita       = detectMoreWaita();
        this._hasAccentColor  = this._desktopSettings.settings_schema.has_key('accent-color');

        window.set_default_size(680, 680);
        window.set_title('Adwaita Colors Home');

        this._installPage = this._buildInstallationPage();

        window.add(this._buildGeneralPage());
        window.add(this._installPage);
        window.add(this._buildAboutPage());

        window.connect('close-request', () => {
            this._settings        = null;
            this._desktopSettings = null;
            this._window          = null;
            this._distroType      = null;
            this._installation    = null;
            this._morewaita       = null;
            this._hasAccentColor  = null;
            this._installPage     = null;
            this._activeColorDot  = null;
            this._activeColorLabel = null;
            this._updateStatusRow  = null;
            this._progressRow      = null;
            this._progressBar      = null;
            this._installStatusRow = null;
            this._manualCheckLabel = null;
            this._mwStatusRow      = null;
        });
    }

    _buildGeneralPage() {
        const page = new Adw.PreferencesPage({
            title: 'General',
            icon_name: 'preferences-system-symbolic',
        });

        if (!this._installation.installed) {
            const notInstalledGroup = new Adw.PreferencesGroup();
            const bannerRow = new Adw.ActionRow({
                title: 'Adwaita Colors is not installed',
                subtitle: 'Open the Installation tab to download and install it.',
                css_classes: ['warning'],
            });
            bannerRow.add_prefix(new Gtk.Image({ icon_name: 'dialog-warning-symbolic', pixel_size: 32 }));
            const installBtn = new Gtk.Button({
                label: 'Install Now',
                css_classes: ['suggested-action'],
                valign: Gtk.Align.CENTER,
            });
            installBtn.connect('clicked', () => {
                if (this._installPage)
                    this._window.set_visible_page(this._installPage);
            });
            bannerRow.add_suffix(installBtn);
            notInstalledGroup.add(bannerRow);
            page.add(notInstalledGroup);
        }

        const themeGroup = new Adw.PreferencesGroup({
            title: 'Icon Theme',
            description: 'Control how Adwaita Colors syncs with your desktop accent color.',
        });

        const activeRow = new Adw.ActionRow({ title: 'Active Variant' });
        this._activeColorDot   = makeColorDot('#3584e4', 18);
        this._activeColorLabel = new Gtk.Label({ label: 'Blue' });
        const activeBox = new Gtk.Box({ spacing: 8, valign: Gtk.Align.CENTER });
        activeBox.append(this._activeColorDot);
        activeBox.append(this._activeColorLabel);
        activeRow.add_suffix(activeBox);
        themeGroup.add(activeRow);

        if (!this._hasAccentColor) {
            const noAccentRow = new Adw.ActionRow({
                title: 'Auto-sync unavailable',
                subtitle: 'GNOME 47 or newer is required for accent color support.',
            });
            noAccentRow.add_prefix(new Gtk.Image({ icon_name: 'dialog-warning-symbolic' }));
            themeGroup.add(noAccentRow);
        }

        const syncRow = new Adw.SwitchRow({
            title: 'Auto-sync with GNOME Accent Color',
            subtitle: 'Automatically switch the icon theme when you change your accent color.',
            sensitive: this._hasAccentColor,
        });
        this._settings.bind('auto-sync', syncRow, 'active', Gio.SettingsBindFlags.DEFAULT);
        themeGroup.add(syncRow);

        const colorRow = new Adw.ComboRow({
            title: 'Icon Color',
            subtitle: 'Only active when auto-sync is off.',
        });
        const colorModel = new Gtk.StringList();
        ALL_COLORS.forEach(c => colorModel.append(COLOR_META[c].label));
        colorRow.model = colorModel;

        const syncSelection = () => {
            const idx = ALL_COLORS.indexOf(this._settings.get_string('manual-color'));
            if (idx >= 0 && colorRow.selected !== idx) colorRow.selected = idx;
        };
        syncSelection();

        colorRow.connect('notify::selected', () => {
            const color = ALL_COLORS[colorRow.selected];
            if (color) this._settings.set_string('manual-color', color);
        });

        const updateSensitivity = () => {
            colorRow.sensitive = !this._settings.get_boolean('auto-sync');
            this._refreshActiveColorRow();
        };
        this._settings.connect('changed::auto-sync', updateSensitivity);
        this._settings.connect('changed::manual-color', () => {
            syncSelection();
            this._refreshActiveColorRow();
        });
        updateSensitivity();
        themeGroup.add(colorRow);

        const carryOverRow = new Adw.SwitchRow({
            title: 'Carry Over Custom Folder Icons',
            subtitle: 'Update custom folder icons when switching between Adwaita Colors variants so they always match the active theme.',
        });
        this._settings.bind('carry-over-custom-icons', carryOverRow, 'active', Gio.SettingsBindFlags.DEFAULT);
        themeGroup.add(carryOverRow);

        const indicatorRow = new Adw.SwitchRow({
            title: 'Show Panel Color Indicator',
            subtitle: 'Display a colored circle in the top bar for quick color switching.',
        });
        this._settings.bind('show-panel-indicator', indicatorRow, 'active', Gio.SettingsBindFlags.DEFAULT);
        themeGroup.add(indicatorRow);

        page.add(themeGroup);

        const statusGroup = new Adw.PreferencesGroup({ title: 'Status' });

        const installRow = new Adw.ActionRow({ title: 'Installation' });
        if (this._installation.installed) {
            const version = this._settings.get_string('installed-version') || 'unknown';
            installRow.subtitle =
                `Installed at ${this._installation.path} · ` +
                `${this._installation.installedColors.length} variants · version ${version}`;
            installRow.add_prefix(
                new Gtk.Image({ icon_name: 'emblem-ok-symbolic', css_classes: ['success'] }));
        } else {
            installRow.subtitle = 'Not installed, go to the Installation tab.';
            installRow.add_prefix(
                new Gtk.Image({ icon_name: 'dialog-warning-symbolic', css_classes: ['warning'] }));
        }
        statusGroup.add(installRow);

        this._updateStatusRow = new Adw.ActionRow({ title: 'Updates', subtitle: 'Checking…' });
        this._updateStatusRow.add_prefix(
            new Gtk.Image({ icon_name: 'software-update-available-symbolic' }));
        statusGroup.add(this._updateStatusRow);
        page.add(statusGroup);

        GLib.idle_add(GLib.PRIORITY_LOW, () => {
            this._refreshUpdateStatusRow();
            return GLib.SOURCE_REMOVE;
        });
        GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
            this._refreshActiveColorRow();
            return GLib.SOURCE_REMOVE;
        });

        return page;
    }

    _refreshActiveColorRow() {
        let color;
        if (this._settings.get_boolean('auto-sync') && this._hasAccentColor) {
            const accent = this._desktopSettings.get_string('accent-color');
            color = ACCENT_TO_COLOR[accent] ?? 'blue';
        } else {
            color = this._settings.get_string('manual-color');
        }
        const meta = COLOR_META[color] ?? COLOR_META.blue;
        this._activeColorLabel.label = meta.label;

        const parent = this._activeColorDot?.get_parent();
        if (parent) {
            const newDot = makeColorDot(meta.hex, 18);
            parent.remove(this._activeColorDot);
            parent.prepend(newDot);
            this._activeColorDot = newDot;
        }
    }

    _refreshUpdateStatusRow() {
        const installed = this._settings.get_string('installed-version');
        const session = new Soup.Session();
        const msg = Soup.Message.new('GET', GITHUB_API_URL);
        msg.request_headers.append('User-Agent', 'adwaita-colors-home/1');

        session.send_and_read_async(msg, GLib.PRIORITY_LOW, null, (sess, result) => {
            try {
                const bytes  = sess.send_and_read_finish(result);
                const json   = JSON.parse(new TextDecoder().decode(bytes.get_data()));
                const latest = json.tag_name;

                if (!installed) {
                    this._updateStatusRow.subtitle = `Latest release: ${latest}`;
                    return;
                }

                if (isNewer(latest, installed)) {
                    this._updateStatusRow.subtitle =
                        `Update available: ${latest} (installed: ${installed})`;
                    const btn = new Gtk.Button({
                        label: `Update to ${latest}`,
                        css_classes: ['suggested-action'],
                        valign: Gtk.Align.CENTER,
                    });
                    btn.connect('clicked', () => {
                        this._startInstall(this._settings.get_string('install-scope'), latest);
                        this._window.set_visible_page(this._installPage);
                    });
                    this._updateStatusRow.add_suffix(btn);
                } else {
                    this._updateStatusRow.subtitle = `Up to date (${installed})`;
                }
            } catch (_) {
                this._updateStatusRow.subtitle = 'Could not check for updates';
            }
        });
    }

    _buildInstallationPage() {
        const page = new Adw.PreferencesPage({
            name: 'installation',
            title: 'Installation',
            icon_name: 'system-software-install-symbolic',
        });

        const isAtomic = this._distroType === 'atomic';

        const locationGroup = new Adw.PreferencesGroup({
            title: 'Install Location',
            description: isAtomic
                ? 'Running on an atomic/ostree desktop. System installs use /var/usr/local/share/icons (persists across OS updates). User installs are recommended.'
                : 'System installs require an administrator password (pkexec). User installs are always available without extra permissions.',
        });

        if (this._installation.installed) {
            const locRow = new Adw.ActionRow({
                title: 'Detected Install Location',
                subtitle: this._installation.path ?? 'Unknown',
            });
            locRow.add_prefix(new Gtk.Image({ icon_name: 'folder-symbolic' }));
            locationGroup.add(locRow);
        }

        const scopeRow = new Adw.ComboRow({ title: 'Default Install Scope' });
        const scopeModel = new Gtk.StringList();
        scopeModel.append('User  (~/.local/share/icons)');
        scopeModel.append(isAtomic
            ? 'System  (/var/usr/local/share/icons)'
            : 'System  (/usr/share/icons, needs sudo)');
        scopeRow.model = scopeModel;
        scopeRow.selected = this._settings.get_string('install-scope') === 'system' ? 1 : 0;
        scopeRow.connect('notify::selected', () => {
            this._settings.set_string(
                'install-scope', scopeRow.selected === 1 ? 'system' : 'user');
        });
        locationGroup.add(scopeRow);
        page.add(locationGroup);

        const actionsGroup = new Adw.PreferencesGroup({ title: 'Actions' });

        const foldersRow = new Adw.SwitchRow({
            title: 'Custom Folder Icons',
            subtitle: 'Include branded folder icons (GitHub, GitLab, Bitwig, etc.)',
        });
        this._settings.bind('install-custom-folders', foldersRow, 'active', Gio.SettingsBindFlags.DEFAULT);
        actionsGroup.add(foldersRow);

        this._progressRow = new Adw.ActionRow({ title: 'Progress', visible: false });
        this._progressBar = new Gtk.ProgressBar({
            valign: Gtk.Align.CENTER,
            hexpand: true,
            show_text: true,
        });
        this._progressRow.add_suffix(this._progressBar);
        actionsGroup.add(this._progressRow);

        this._installStatusRow = new Adw.ActionRow({
            title: 'Status',
            subtitle: this._installation.installed ? 'Adwaita Colors is installed.' : 'Not installed.',
        });
        actionsGroup.add(this._installStatusRow);

        const installBtn = new Gtk.Button({
            label: this._installation.installed ? 'Reinstall' : 'Install for me',
            css_classes: ['suggested-action'],
            valign: Gtk.Align.CENTER,
        });
        installBtn.connect('clicked', () => {
            this._startInstall(this._settings.get_string('install-scope'), null);
        });
        const installBtnRow = new Adw.ActionRow({ title: 'Install / Reinstall Adwaita Colors' });
        installBtnRow.add_suffix(installBtn);
        actionsGroup.add(installBtnRow);

        const githubBtn = new Gtk.Button({ label: 'Open on GitHub', valign: Gtk.Align.CENTER });
        githubBtn.connect('clicked', () => openUri(GITHUB_PAGE_URL));
        const githubRow = new Adw.ActionRow({ title: 'Manual Download' });
        githubRow.add_suffix(githubBtn);
        actionsGroup.add(githubRow);

        const uninstallBtn = new Gtk.Button({
            label: 'Uninstall',
            css_classes: ['destructive-action'],
            valign: Gtk.Align.CENTER,
            sensitive: this._installation.installed,
        });
        uninstallBtn.connect('clicked', () => this._confirmUninstall());
        const uninstallRow = new Adw.ActionRow({ title: 'Remove Adwaita Colors' });
        uninstallRow.add_suffix(uninstallBtn);
        actionsGroup.add(uninstallRow);

        page.add(actionsGroup);

        // MoreWaita integration group, always visible
        page.add(this._buildMoreWaitaGroup());

        return page;
    }

    _buildMoreWaitaGroup() {
        const mw = this._morewaita;

        const group = new Adw.PreferencesGroup({
            title: 'MoreWaita Integration',
            description: mw.found
                ? 'MoreWaita is installed. Patching adds it to the icon inheritance chain and copies additional mimetype icons into each color variant.'
                : 'MoreWaita extends Adwaita with many additional icons. Download and install it here.',
        });

        this._mwStatusRow = new Adw.ActionRow({ title: 'MoreWaita Status' });
        if (mw.found) {
            this._mwStatusRow.subtitle = `Installed at ${mw.path}`;
            const icon = new Gtk.Image({ icon_name: 'emblem-ok-symbolic' });
            icon.css_classes = ['success'];
            this._mwStatusRow.add_prefix(icon);
        } else {
            this._mwStatusRow.subtitle = 'Not installed';
            this._mwStatusRow.add_prefix(
                new Gtk.Image({ icon_name: 'dialog-information-symbolic' }));
            this._mwInstallBtn = new Gtk.Button({
                label: 'Install MoreWaita',
                css_classes: ['suggested-action'],
                valign: Gtk.Align.CENTER,
            });
            this._mwInstallBtn.connect('clicked', () => this._installMoreWaita());
            this._mwStatusRow.add_suffix(this._mwInstallBtn);
        }
        group.add(this._mwStatusRow);

        if (mw.found) {
            const patchStatus = this._getMoreWaitaPatchStatus();
            this._mwStatusRow = new Adw.ActionRow({
                title: 'Integration Status',
                subtitle: patchStatus,
            });
            group.add(this._mwStatusRow);

            const isPatched = patchStatus.startsWith('Patched, all');
            this._mwIntegrateBtn = new Gtk.Button({
                label: 'Integrate Now',
                css_classes: ['suggested-action'],
                valign: Gtk.Align.CENTER,
            });
            this._mwIntegrateBtn.connect('clicked', () => this._integrateMoreWaita());
            this._mwIntegrateRow = new Adw.ActionRow({ title: 'Patch Inherits chain' });
            this._mwIntegrateRow.add_suffix(this._mwIntegrateBtn);
            this._mwIntegrateRow.visible = !isPatched;
            group.add(this._mwIntegrateRow);

            const unpatchBtn = new Gtk.Button({
                label: 'Remove patch',
                valign: Gtk.Align.CENTER,
            });
            unpatchBtn.connect('clicked', () => {
                this._removeMoreWaitaPatch();
                this._refreshMoreWaitaIntegrationUI();
            });
            this._mwUnpatchRow = new Adw.ActionRow({ title: 'Remove MoreWaita from Inherits chain' });
            this._mwUnpatchRow.add_suffix(unpatchBtn);
            this._mwUnpatchRow.visible = isPatched;
            group.add(this._mwUnpatchRow);
        }

        return group;
    }

    _installMoreWaita() {
        this._progressRow.visible = true;
        this._progressBar.set_fraction(0);
        this._progressBar.set_text('Fetching release info…');
        this._mwInstallBtn.sensitive = false;

        const scope = this._settings.get_string('install-scope');
        const iconBase = scope === 'user'
            ? `${GLib.get_home_dir()}/.local/share/icons`
            : (this._distroType === 'atomic' ? '/var/usr/local/share/icons' : '/usr/share/icons');
        const mwInstallDir = `${iconBase}/MoreWaita`;

        const session = new Soup.Session();
        const msg = Soup.Message.new('GET', MOREWAITA_API_URL);
        msg.request_headers.append('User-Agent', 'adwaita-colors-home/1');

        session.send_and_read_async(msg, GLib.PRIORITY_DEFAULT, null, (sess, result) => {
            let zipUrl, tag;
            try {
                const bytes = sess.send_and_read_finish(result);
                const json  = JSON.parse(new TextDecoder().decode(bytes.get_data()));
                tag = json.tag_name ?? 'master';
                zipUrl = `https://github.com/somepaulo/MoreWaita/archive/refs/tags/${tag}.tar.gz`;
            } catch (_) {
                // Fallback to master branch
                tag = 'master';
                zipUrl = 'https://github.com/somepaulo/MoreWaita/archive/refs/heads/master.tar.gz';
            }

            this._progressBar.set_fraction(0.1);
            this._progressBar.set_text('Downloading MoreWaita…');
            this._downloadAndExtractMoreWaita(zipUrl, tag, mwInstallDir);
        });
    }

    _downloadAndExtractMoreWaita(url, tag, installDir) {
        const tmpDir      = GLib.Dir.make_tmp('morewaita-XXXXXX');
        const archivePath = `${tmpDir}/archive.zip`;

        const downloader = this._findDownloader();
        if (!downloader) {
            this._setInstallStatus('Error: neither curl nor wget found.');
            this._progressRow.visible = false;
            this._mwInstallBtn.sensitive = true;
            return;
        }

        const downloadArgs = downloader === 'curl'
            ? ['curl', '-L', '--fail', '-o', archivePath, url]
            : ['wget', '-q', '-O', archivePath, url];

        this._runSubprocess(downloadArgs, 'Downloading…', 0.1, 0.4)
            .then(() => {
                this._progressBar.set_fraction(0.42);
                this._progressBar.set_text('Extracting…');

                // Use tar.gz to preserve symlinks (GitHub's ZIP doesn't preserve them)
                const extractArgs = ['tar', '-xf', archivePath, '-C', tmpDir];
                return this._runSubprocess(extractArgs, 'Extracting…', 0.42, 0.55);
            })
            .then(() => {
                this._progressBar.set_fraction(0.57);
                this._progressBar.set_text('Finding install.sh…');

                const finderScript = `
import os, sys
base = sys.argv[1]
for root, dirs, files in os.walk(base):
    if 'install.sh' in files:
        print(root)
        sys.exit(0)
print(base)
`;
                return this._runSubprocessWithOutput(['python3', '-c', finderScript, tmpDir]);
            })
            .then(srcDir => {
                srcDir = srcDir.trim();
                this._progressBar.set_fraction(0.6);
                this._progressBar.set_text('Installing with MoreWaita install.sh…');

                const envBin = GLib.find_program_in_path('env');
                const bashBin = GLib.find_program_in_path('bash');
                if (!bashBin || !envBin)
                    throw new Error('bash or env not found');

                // MoreWaita's install.sh uses THEMEDIR env var and cp -a to preserve symlinks
                const argv = [envBin, `THEMEDIR=${installDir}`, bashBin, `${srcDir}/install.sh`];
                const escalated = this._escalate(argv, installDir);
                return this._runSubprocess(escalated, 'Installing…', 0.6, 0.95);
            })
            .then(() => {
                GLib.spawn_command_line_async(`rm -rf ${tmpDir}`);
                this._progressBar.set_fraction(1.0);
                this._progressBar.set_text('Done!');
                this._mwInstallBtn.sensitive = true;
                this._morewaita = detectMoreWaita();

                this._refreshMoreWaitaUI();

                this._setInstallStatus(`MoreWaita ${tag} installed at ${installDir}`);
                GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
                    this._progressRow.visible = false;
                    this._showIntegrateDialog();
                    return GLib.SOURCE_REMOVE;
                });
            })
            .catch(err => {
                GLib.spawn_command_line_async(`rm -rf ${tmpDir}`);
                this._setInstallStatus(`Installation failed: ${err.message ?? err}`);
                this._progressRow.visible = false;
                this._mwInstallBtn.sensitive = true;
            });
    }

    _showIntegrateDialog() {
        const dialog = new Adw.MessageDialog({
            transient_for: this._window,
            heading: 'MoreWaita Installed',
            body: 'Patch the icon inheritance chain to include MoreWaita?\n\nThis ensures icons fall through to MoreWaita before Adwaita for broader coverage.\n\nYou can also run the morewaita.sh script from the terminal for full SVG integration.',
        });
        dialog.add_response('no', "No, I'll do it later");
        dialog.add_response('integrate', 'Patch Now');
        dialog.set_response_appearance('integrate', Adw.ResponseAppearance.SUGGESTED);
        dialog.connect('response', (_d, id) => {
            if (id === 'integrate') {
                this._applyMoreWaitaPatch();
                if (this._mwStatusRow)
                    this._mwStatusRow.subtitle = this._getMoreWaitaPatchStatus();
                this._setInstallStatus('MoreWaita integrated — Inherits chain patched.');
            } else {
                this._setInstallStatus('MoreWaita installed but not integrated.');
            }
        });
        dialog.present();
    }

    _integrateMoreWaita() {
        this._applyMoreWaitaPatch();

        // Clear icon cache for each variant so the new Inherits chain takes effect
        const base = this._installation?.path;
        if (base && GLib.file_test(base, GLib.FileTest.IS_DIR)) {
            for (const color of ALL_COLORS) {
                const variantDir = `${base}/Adwaita-${color}`;
                if (GLib.file_test(variantDir, GLib.FileTest.IS_DIR)) {
                    const cacheArgs = this._escalate(
                        ['gtk-update-icon-cache', '-f', variantDir], variantDir);
                    this._runSubprocess(cacheArgs, '', 0, 0).catch(() => {});
                }
            }
        }

        this._refreshMoreWaitaIntegrationUI();
        this._setInstallStatus('MoreWaita integrated — Inherits chain patched.');
    }

    _refreshMoreWaitaIntegrationUI() {
        if (this._mwStatusRow)
            this._mwStatusRow.subtitle = this._getMoreWaitaPatchStatus();

        const isPatched = this._getMoreWaitaPatchStatus().startsWith('Patched, all');
        if (this._mwIntegrateRow)
            this._mwIntegrateRow.visible = !isPatched;
        if (this._mwUnpatchRow)
            this._mwUnpatchRow.visible = isPatched;
    }

    _refreshMoreWaitaUI() {
        this._morewaita = detectMoreWaita();
        if (this._mwStatusRow) {
            this._mwStatusRow.subtitle = this._morewaita.found
                ? `Installed at ${this._morewaita.path}`
                : 'Not installed';
        }
    }

    _getMoreWaitaPatchStatus() {
        if (!this._installation.installed)
            return 'Adwaita Colors not installed, nothing to patch yet';

        const base = this._installation.path;
        let patched = 0;
        let total = 0;

        for (const color of ALL_COLORS) {
            const indexPath = `${base}/Adwaita-${color}/index.theme`;
            if (!GLib.file_test(indexPath, GLib.FileTest.EXISTS)) continue;
            total++;
            try {
                const [, data] = GLib.file_get_contents(indexPath);
                if (new TextDecoder().decode(data).includes('MoreWaita')) patched++;
            } catch (_) {}
        }

        if (total === 0) return 'No Adwaita Colors variants found at install path';
        if (patched === 0) return `Not patched (${total} variants available)`;
        if (patched === total) return `Patched, all ${total} variants`;
        return `Partially patched (${patched} of ${total} variants)`;
    }

    _applyMoreWaitaPatch() {
        const base = this._installation.path;
        if (!base) {
            this._setInstallStatus('Cannot patch: Adwaita Colors is not installed.');
            return;
        }

        const getPatchLine = (color) => color === 'blue'
            ? 'Inherits=MoreWaita,Adwaita,AdwaitaLegacy,hicolor'
            : 'Inherits=MoreWaita,Adwaita,Adwaita-blue,AdwaitaLegacy,hicolor';

        if (needsPrivileges(base)) {
            const patchScript = `
import sys, re
from pathlib import Path

base = Path(sys.argv[1])
patched = 0
# Args come in pairs: variant_name, patch_line
args = sys.argv[2:]
for i in range(0, len(args), 2):
    name, patch = args[i], args[i+1]
    index = base / name / 'index.theme'
    if not index.exists():
        continue
    text = index.read_text()
    if 'MoreWaita' in text:
        continue
    text = re.sub(r'^Inherits=.*', patch, text, count=1, flags=re.MULTILINE)
    index.write_text(text)
    patched += 1
print(patched)
`;
            const pythonBin = GLib.find_program_in_path('python3');
            if (!pythonBin) {
                this._setInstallStatus('Cannot patch: python3 not found.');
                return;
            }
            // Build variant name + patch line pairs
            const variantArgs = [];
            for (const color of ALL_COLORS) {
                if (!GLib.file_test(`${base}/Adwaita-${color}/index.theme`, GLib.FileTest.EXISTS))
                    continue;
                variantArgs.push(`Adwaita-${color}`, getPatchLine(color));
            }
            const args = this._escalate(['python3', '-c', patchScript, base, ...variantArgs], base);
            this._runSubprocessWithOutput(args)
                .then(out => {
                    const n = parseInt(out.trim(), 10);
                    this._setInstallStatus(
                        n > 0
                            ? `MoreWaita patch applied to ${n} variant(s).`
                            : 'All variants were already patched.');
                })
                .catch(err => {
                    this._setInstallStatus(`Patch failed: ${err.message ?? err}`);
                });
            return;
        }

        let patched = 0;
        for (const color of ALL_COLORS) {
            const indexPath = `${base}/Adwaita-${color}/index.theme`;
            if (!GLib.file_test(indexPath, GLib.FileTest.EXISTS)) continue;
            try {
                const [, data] = GLib.file_get_contents(indexPath);
                let text = new TextDecoder().decode(data);
                if (text.includes('MoreWaita')) continue;
                text = text.replace(/^Inherits=.*/m, getPatchLine(color));
                GLib.file_set_contents(indexPath, new TextEncoder().encode(text));
                patched++;
            } catch (_) {}
        }

        this._setInstallStatus(
            patched > 0
                ? `MoreWaita patch applied to ${patched} variant(s).`
                : 'All variants were already patched.');
    }

    _removeMoreWaitaPatch() {
        const base = this._installation.path;
        if (!base) {
            this._setInstallStatus('Cannot unpatch: Adwaita Colors is not installed.');
            return;
        }

        if (needsPrivileges(base)) {
            const unpatchScript = `
import sys, re
from pathlib import Path

base = Path(sys.argv[1])
variants_info = sys.argv[2:]
removed = 0
for i in range(0, len(variants_info), 2):
    name, restore = variants_info[i], variants_info[i+1]
    variant_dir = base / name
    index = variant_dir / 'index.theme'
    if not index.exists():
        continue
    text = index.read_text()
    if 'MoreWaita' not in text:
        continue
    text = re.sub(r'^Inherits=.*', restore, text, count=1, flags=re.MULTILINE)
    index.write_text(text)
    removed += 1
print(removed)
`;
            const pythonBin = GLib.find_program_in_path('python3');
            if (!pythonBin) {
                this._setInstallStatus('Cannot unpatch: python3 not found.');
                return;
            }
            // Build variant name + restore line pairs
            const variantInfo = [];
            for (const color of ALL_COLORS) {
                if (!GLib.file_test(`${base}/Adwaita-${color}/index.theme`, GLib.FileTest.EXISTS))
                    continue;
                variantInfo.push(`Adwaita-${color}`);
                variantInfo.push(color === 'blue'
                    ? 'Inherits=Adwaita,AdwaitaLegacy,hicolor'
                    : 'Inherits=Adwaita,Adwaita-blue,AdwaitaLegacy,hicolor');
            }
            const args = this._escalate(['python3', '-c', unpatchScript, base, ...variantInfo], base);
            this._runSubprocessWithOutput(args)
                .then(out => {
                    const n = parseInt(out.trim(), 10);
                    this._setInstallStatus(
                        n > 0
                            ? `MoreWaita patch removed from ${n} variant(s).`
                            : 'No variants had the patch applied.');
                })
                .catch(err => {
                    this._setInstallStatus(`Unpatch failed: ${err.message ?? err}`);
                });
            return;
        }

        let removed = 0;
        for (const color of ALL_COLORS) {
            const indexPath = `${base}/Adwaita-${color}/index.theme`;
            if (!GLib.file_test(indexPath, GLib.FileTest.EXISTS)) continue;
            try {
                const [, data] = GLib.file_get_contents(indexPath);
                let text = new TextDecoder().decode(data);
                if (!text.includes('MoreWaita')) continue;
                const restoreLine = color === 'blue'
                    ? 'Inherits=Adwaita,AdwaitaLegacy,hicolor'
                    : 'Inherits=Adwaita,Adwaita-blue,AdwaitaLegacy,hicolor';
                text = text.replace(/^Inherits=.*/m, restoreLine);
                GLib.file_set_contents(indexPath, new TextEncoder().encode(text));
                removed++;
            } catch (_) {}
        }

        this._setInstallStatus(
            removed > 0
                ? `MoreWaita patch removed from ${removed} variant(s).`
                : 'No variants had the patch applied.');
    }

    _startInstall(scope, versionOverride) {
        const localRepo = `${GLib.get_home_dir()}/code/Adwaita-colors`;

        this._progressRow.visible = true;
        this._progressBar.set_fraction(0);
        this._progressBar.set_text('Copying from local repo…');
        this._setInstallStatus('Copying Adwaita Colors from local repository…');

        const installPath = scope === 'user'
            ? `${GLib.get_home_dir()}/.local/share/icons`
            : (this._distroType === 'atomic' ? '/var/usr/local/share/icons' : '/usr/share/icons');
        const withFolders = this._settings.get_boolean('install-custom-folders');
        const tmpDir = GLib.Dir.make_tmp('adwaita-colors-XXXXXX');

        const copyArgs = ['cp', '-r', '--dereference', localRepo, tmpDir];
        this._runSubprocess(copyArgs, 'Copying…', 0, 0.3)
            .then(() => {
                this._progressBar.set_fraction(0.35);
                this._progressBar.set_text('Installing…');

                const bashBin = GLib.find_program_in_path('bash');
                if (!bashBin)
                    throw new Error('bash not found');

                const repoDir = `${tmpDir}/Adwaita-colors`;
                const setupArgs = [bashBin, `${repoDir}/setup`, '-i'];
                if (withFolders)
                    setupArgs.push('-f');
                if (scope === 'system')
                    setupArgs.push('-g');
                else
                    setupArgs.push('-u');

                const escalated = this._escalate(setupArgs, installPath);
                return this._runSubprocess(escalated, 'Installing…', 0.35, 0.95);
            })
            .then(() => {
                GLib.spawn_command_line_async(`rm -rf ${tmpDir}`);
                this._progressBar.set_fraction(1.0);
                this._progressBar.set_text('Done!');
                this._settings.set_string('installed-version', 'local');
                this._setInstallStatus(`Adwaita Colors installed at ${installPath}`);
                this._installation = detectInstallation();

                // Apply the matching icon theme for the current accent color
                if (this._hasAccentColor) {
                    const accent = this._desktopSettings.get_string('accent-color');
                    const color = accent === 'default' ? 'blue' : accent;
                    const themeName = `Adwaita-${color}`;
                    if (this._desktopSettings.get_string('icon-theme') !== themeName)
                        this._desktopSettings.set_string('icon-theme', themeName);
                }

                GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2500, () => {
                    this._progressRow.visible = false;
                    return GLib.SOURCE_REMOVE;
                });
            })
            .catch(err => {
                GLib.spawn_command_line_async(`rm -rf ${tmpDir}`);
                this._setInstallStatus(`Installation failed: ${err.message ?? err}`);
                this._progressRow.visible = false;
            });
    }
    _escalate(argv, targetPath) {
        if (!needsPrivileges(targetPath))
            return argv;
        const bin = GLib.find_program_in_path(argv[0]);
        if (!bin)
            return argv;
        return ['pkexec', bin, ...argv.slice(1)];
    }

    _runSubprocess(argv, label, fromFraction, toFraction) {
        return new Promise((resolve, reject) => {
            let proc;
            try {
                proc = new Gio.Subprocess({
                    argv,
                    flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE,
                });
                proc.init(null);
            } catch (e) {
                reject(new Error(`Failed to start ${argv[0]}: ${e.message}`));
                return;
            }

            let frac = fromFraction;
            const step = (toFraction - fromFraction) * 0.08;
            const pulseId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => {
                frac = Math.min(frac + step, toFraction - 0.01);
                this._progressBar.set_fraction(frac);
                this._progressBar.set_text(label);
                return GLib.SOURCE_CONTINUE;
            });

            proc.communicate_utf8_async(null, null, (_proc, result) => {
                GLib.source_remove(pulseId);
                try {
                    const [, stdout, stderr] = proc.communicate_utf8_finish(result);
                    if (!proc.get_if_exited() || proc.get_exit_status() !== 0) {
                        const msg = (stderr || stdout || '').trim();
                        reject(new Error(`${argv[0]} failed: ${msg || 'exit code ' + proc.get_exit_status()}`));
                    } else {
                        this._progressBar.set_fraction(toFraction);
                        resolve();
                    }
                } catch (e) {
                    reject(new Error(`${argv[0]} failed: ${e.message}`));
                }
            });
        });
    }

    _runSubprocessWithOutput(argv) {
        return new Promise((resolve, reject) => {
            let proc;
            try {
                proc = new Gio.Subprocess({
                    argv,
                    flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE,
                });
                proc.init(null);
            } catch (e) {
                reject(e);
                return;
            }

            proc.communicate_utf8_async(null, null, (_proc, result) => {
                try {
                    const [, stdout, stderr] = proc.communicate_utf8_finish(result);
                    if (!proc.get_if_exited() || proc.get_exit_status() !== 0) {
                        const msg = (stderr || stdout || '').trim();
                        reject(new Error(`${argv[0]} failed: ${msg || 'exit code ' + proc.get_exit_status()}`));
                    } else {
                        resolve(stdout ?? '');
                    }
                } catch (e) {
                    reject(e);
                }
            });
        });
    }

    _findDownloader() {
        for (const tool of ['curl', 'wget']) {
            if (GLib.find_program_in_path(tool)) return tool;
        }
        return null;
    }

    _setInstallStatus(msg) {
        if (this._installStatusRow) this._installStatusRow.subtitle = msg;
    }

    _confirmUninstall() {
        const dialog = new Adw.MessageDialog({
            transient_for: this._window,
            heading: 'Uninstall Adwaita Colors?',
            body: `This will remove all Adwaita-* directories from ${this._installation.path}.`,
        });
        dialog.add_response('cancel', 'Cancel');
        dialog.add_response('uninstall', 'Uninstall');
        dialog.set_response_appearance('uninstall', Adw.ResponseAppearance.DESTRUCTIVE);
        dialog.connect('response', (_d, id) => {
            if (id === 'uninstall') this._performUninstall();
        });
        dialog.present();
    }

    _performUninstall() {
        const base = this._installation.path;
        if (!base) return;

        this._progressRow.visible = true;

        const rmScript = `
import os, shutil, sys
for entry in os.scandir(sys.argv[1]):
    if entry.is_dir() and entry.name.startswith('Adwaita-'):
        shutil.rmtree(entry.path)
`;
        const args = this._escalate(['python3', '-c', rmScript, base], base);
        this._runSubprocess(args, 'Removing…', 0, 1)
            .then(() => {
                this._settings.set_string('installed-version', '');
                this._installation = detectInstallation();

                const currentTheme = this._desktopSettings.get_string('icon-theme');
                if (currentTheme.startsWith('Adwaita-'))
                    this._desktopSettings.set_string('icon-theme', 'Adwaita');

                this._setInstallStatus('Adwaita Colors removed. Icon theme reset to Adwaita.');
                this._progressRow.visible = false;
            })
            .catch(err => {
                this._setInstallStatus(`Uninstall failed: ${err}`);
                this._progressRow.visible = false;
            });
    }

    _buildAboutPage() {
        const page = new Adw.PreferencesPage({
            title: 'About',
            icon_name: 'help-about-symbolic',
        });

        const infoGroup = new Adw.PreferencesGroup({ title: 'Adwaita Colors Home' });
        infoGroup.add(new Adw.ActionRow({ title: 'Extension Version', subtitle: '1.0' }));
        infoGroup.add(new Adw.ActionRow({ title: 'GNOME Shell Compatibility', subtitle: '47, 48, 49, 50' }));

        const accentRow = new Adw.ActionRow({
            title: 'Accent Color Support',
            subtitle: this._hasAccentColor
                ? 'Available (GNOME 47+)'
                : 'Unavailable, upgrade to GNOME 47 or newer',
        });
        accentRow.add_prefix(new Gtk.Image({
            icon_name: this._hasAccentColor ? 'emblem-ok-symbolic' : 'dialog-warning-symbolic',
            css_classes: [this._hasAccentColor ? 'success' : 'warning'],
        }));
        infoGroup.add(accentRow);
        page.add(infoGroup);

        const linksGroup = new Adw.PreferencesGroup({ title: 'Links' });
        for (const { title, url } of [
            { title: 'Adwaita Colors on GitHub',         url: GITHUB_PAGE_URL },
            { title: 'Report an issue, icon theme',     url: GITHUB_ISSUES_URL },
            { title: 'Report an issue, this extension', url: GITHUB_HOME_ISSUES_URL },
            { title: 'Latest Release',                   url: GITHUB_RELEASES_URL },
        ]) {
            const row = new Adw.ActionRow({ title, activatable: true });
            row.add_suffix(new Gtk.Image({ icon_name: 'adw-external-link-symbolic' }));
            row.connect('activated', () => openUri(url));
            linksGroup.add(row);
        }
        page.add(linksGroup);

        const updateGroup = new Adw.PreferencesGroup({ title: 'Updates' });
        this._manualCheckLabel = new Gtk.Label({ label: '', valign: Gtk.Align.CENTER });
        const checkBtn = new Gtk.Button({ label: 'Check Now', valign: Gtk.Align.CENTER });
        checkBtn.connect('clicked', () => this._manualUpdateCheck());
        const checkRow = new Adw.ActionRow({ title: 'Check for Updates Manually' });
        checkRow.add_suffix(this._manualCheckLabel);
        checkRow.add_suffix(checkBtn);
        updateGroup.add(checkRow);
        page.add(updateGroup);

        const creditsGroup = new Adw.PreferencesGroup({ title: 'Credits' });
        for (const [title, subtitle] of [
            ['Icon Theme', 'Adwaita Colors by dpejoh'],
            ['Extension',  'Adwaita Colors Home, official companion extension'],
            ['License',    'GPL-3.0-or-later'],
        ]) {
            creditsGroup.add(new Adw.ActionRow({ title, subtitle }));
        }
        page.add(creditsGroup);

        return page;
    }

    _manualUpdateCheck() {
        this._manualCheckLabel.label = 'Checking…';
        const session = new Soup.Session();
        const msg = Soup.Message.new('GET', GITHUB_API_URL);
        msg.request_headers.append('User-Agent', 'adwaita-colors-home/1');

        session.send_and_read_async(msg, GLib.PRIORITY_DEFAULT, null, (sess, result) => {
            try {
                const bytes     = sess.send_and_read_finish(result);
                const json      = JSON.parse(new TextDecoder().decode(bytes.get_data()));
                const latest    = json.tag_name;
                const installed = this._settings.get_string('installed-version');

                if (!installed)
                    this._manualCheckLabel.label = `Latest: ${latest}`;
                else if (isNewer(latest, installed))
                    this._manualCheckLabel.label = `Update available: ${latest}`;
                else
                    this._manualCheckLabel.label = 'Up to date';

                this._settings.set_int64('last-update-check', Math.floor(Date.now() / 1000));
            } catch (_) {
                this._manualCheckLabel.label = 'Network error';
            }
        });
    }
}