import app from "ags/gtk4/app";
import { Astal, Gtk } from "ags/gtk4";
import { execAsync, subprocess, Process } from "ags/process";
import GLib from "gi://GLib";
import css from "./package-manager.css";

const WINDOW_MARGIN_TOP = 48;
const ESC_KEYVAL = 65307;
const RUNNER = `${GLib.get_home_dir()}/.config/hypr/Scripts/ags-package-runner.py`;

type Helper = "pacman" | "paru";

type PackageResult = {
    repo: string;
    name: string;
    version: string;
    installed: boolean;
    description: string;
};

let helper: Helper = "paru";
let query = "";
let results: PackageResult[] = [];
let busy = false;
let statusMessage = "Suchbegriff eingeben und Enter druecken.";
let errorMessage = "";
let activeProcess: Process | null = null;
let operationTitle = "";
let operationOutput = "";
let operationInput = "";
let secretInput = false;
let lastOutput = "";

function shQuote(value: string) {
    return `'${value.replace(/'/g, `'\\''`)}'`;
}

function notify(message: string) {
    execAsync(["notify-send", "Pakete", message]).catch(console.error);
}

function runShell(command: string) {
    return execAsync(["bash", "-lc", command]);
}

function setBusy(value: boolean) {
    busy = value;
    rebuild();
}

function appendOperationOutput(data: string) {
    operationOutput = `${operationOutput}${data}`.slice(-30000);
    lastOutput = data;
    secretInput = /(\[sudo\].*password|passwort|password).*:/i.test(data);
    rebuild();
}

function commandLabel(command: string[]) {
    return command.map(shQuote).join(" ");
}

function helperAvailable(nextHelper: Helper) {
    return runShell(`command -v ${nextHelper} >/dev/null 2>&1`);
}

function parseSearch(output: string) {
    const parsed: PackageResult[] = [];
    const lines = output.split("\n");

    for (let index = 0; index < lines.length; index += 1) {
        const header = lines[index];
        const match = header.match(/^([^/\s]+)\/([^\s]+)\s+([^\s]+)(?:\s+\[(installed[^\]]*)\])?/);

        if (!match) {
            continue;
        }

        const descriptionLines: string[] = [];
        let next = index + 1;
        while (next < lines.length && /^\s+/.test(lines[next])) {
            descriptionLines.push(lines[next].trim());
            next += 1;
        }

        parsed.push({
            repo: match[1],
            name: match[2],
            version: match[3],
            installed: Boolean(match[4]),
            description: descriptionLines.join(" ") || "Keine Beschreibung.",
        });

        index = next - 1;
    }

    return parsed.slice(0, 80);
}

function searchPackages() {
    const term = query.trim();
    if (!term) {
        results = [];
        statusMessage = "Suchbegriff fehlt.";
        rebuild();
        return;
    }

    setBusy(true);
    errorMessage = "";
    statusMessage = `Suche mit ${helper} nach "${term}"...`;

    helperAvailable(helper)
        .then(() => runShell(`${helper} -Ss ${shQuote(term)} 2>/dev/null || true`))
        .then(output => {
            results = parseSearch(output);
            statusMessage = results.length
                ? `${results.length} Treffer fuer "${term}" mit ${helper}.`
                : `Keine Treffer fuer "${term}".`;
        })
        .catch(error => {
            console.error(error);
            results = [];
            errorMessage = `${helper} ist nicht verfuegbar oder die Suche ist fehlgeschlagen.`;
            statusMessage = "Suche fehlgeschlagen.";
        })
        .finally(() => {
            setBusy(false);
        });
}

function startOperation(title: string, command: string[]) {
    if (activeProcess) {
        statusMessage = "Es laeuft bereits ein Paketprozess.";
        rebuild();
        return;
    }

    operationTitle = title;
    operationOutput = `$ ${commandLabel(command)}\n\n`;
    operationInput = "";
    errorMessage = "";
    statusMessage = `${title} laeuft in AGS.`;
    busy = true;

    const process = subprocess(
        [RUNNER, "--", ...command],
        line => {
            try {
                const event = JSON.parse(line);
                if (event.type === "out") {
                    appendOperationOutput(event.data || "");
                } else if (event.type === "exit") {
                    const suffix = event.signaled ? `Signal ${event.code}` : `Exit ${event.code}`;
                    appendOperationOutput(`\n[${suffix}]\n`);
                }
            } catch {
                appendOperationOutput(`${line}\n`);
            }
        },
        line => appendOperationOutput(`${line}\n`),
    );

    activeProcess = process;
    process.connect("exit", (_, code: number, signaled: boolean) => {
        activeProcess = null;
        busy = false;
        secretInput = false;
        statusMessage = signaled
            ? `${title} wurde beendet.`
            : code === 0
                ? `${title} abgeschlossen.`
                : `${title} fehlgeschlagen (Exit ${code}).`;
        notify(statusMessage);
        rebuild();
    });

    rebuild();
}

function sendOperationInput(value = operationInput, allowEmpty = false) {
    if (!activeProcess || (!allowEmpty && value.length === 0)) {
        return;
    }

    activeProcess.writeAsync(`${JSON.stringify({ type: "input", data: `${value}\n` })}\n`).catch(error => {
        console.error(error);
        errorMessage = "Eingabe konnte nicht gesendet werden.";
        rebuild();
    });
    operationInput = "";
    secretInput = false;
    rebuild();
}

function yesAnswer() {
    return /[\[(][YyJj]\/[Nn][\])]/.test(lastOutput) ? "" : "y";
}

function sendYesAnswer() {
    const answer = yesAnswer();
    appendOperationOutput(answer ? `\n> ${answer}\n` : "\n> Enter\n");
    sendOperationInput(answer, true);
}

function sendNoAnswer() {
    appendOperationOutput("\n> n\n");
    sendOperationInput("n");
}

function cancelOperation() {
    if (!activeProcess) {
        return;
    }

    activeProcess.writeAsync(`${JSON.stringify({ type: "signal", signal: 15 })}\n`).catch(console.error);
    statusMessage = "Abbruch angefordert.";
    rebuild();
}

function installPackage(pkg: PackageResult) {
    const command = helper === "pacman"
        ? ["sudo", "pacman", "-S", pkg.name]
        : ["paru", "-S", pkg.name];

    notify(`Installation gestartet: ${pkg.name}`);
    startOperation(`Installation: ${pkg.name}`, command);
}

function updateSystem() {
    const command = helper === "pacman"
        ? ["sudo", "pacman", "-Syu"]
        : ["paru", "-Syu"];

    notify(`Update gestartet mit ${helper}.`);
    startOperation(`Systemupdate mit ${helper}`, command);
}

function HelperButton({ id, label }: { id: Helper; label: string }) {
    return (
        <button
            class={helper === id ? "tool-button active" : "tool-button"}
            onClicked={() => {
                helper = id;
                statusMessage = `Aktiver Helper: ${helper}`;
                rebuild();
            }}
            label={label}
        />
    );
}

function PackageRow({ pkg }: { pkg: PackageResult }) {
    return (
        <box class="package-row" spacing={10}>
            <box orientation={Gtk.Orientation.VERTICAL} spacing={4} hexpand>
                <box spacing={8}>
                    <label class="package-name" xalign={0} label={pkg.name} />
                    <label class="repo-pill" label={pkg.repo} />
                    {pkg.installed ? <label class="installed-pill" label="installed" /> : <box />}
                </box>
                <label class="package-meta" xalign={0} label={pkg.version} />
                <label class="package-desc" xalign={0} wrap label={pkg.description} />
            </box>
            <button class="button primary" sensitive={!busy} onClicked={() => installPackage(pkg)} label="Installieren" />
        </box>
    );
}

function OperationPanel() {
    if (!operationTitle && !operationOutput) {
        return <box />;
    }

    return (
        <box class="operation-panel" orientation={Gtk.Orientation.VERTICAL} spacing={8}>
            <box class="operation-header" spacing={8}>
                <label class="operation-title" xalign={0} hexpand label={operationTitle || "Paketprozess"} />
                <button class="button danger" sensitive={Boolean(activeProcess)} onClicked={cancelOperation} label="Abbrechen" />
            </box>
            <scrolledwindow
                class="operation-scroll"
                hexpand
                vexpand
                hscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
                vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
            >
                <label class="operation-output" xalign={0} yalign={0} wrap selectable label={operationOutput || "Warte auf Ausgabe..."} />
            </scrolledwindow>
            <box class="operation-input-row" spacing={8}>
                <entry
                    hexpand
                    sensitive={Boolean(activeProcess)}
                    visibility={!secretInput}
                    text={operationInput}
                    placeholderText={secretInput ? "Passwort eingeben..." : "Antwort eingeben..."}
                    onChanged={entry => {
                        operationInput = entry.get_text();
                    }}
                    onActivate={entry => {
                        operationInput = entry.get_text();
                        sendOperationInput();
                    }}
                />
                <button class="button" sensitive={Boolean(activeProcess)} onClicked={sendYesAnswer} label="Ja" />
                <button class="button" sensitive={Boolean(activeProcess)} onClicked={sendNoAnswer} label="Nein" />
                <button class="button primary" sensitive={Boolean(activeProcess)} onClicked={() => sendOperationInput()} label="Senden" />
            </box>
        </box>
    );
}

function PackageManagerWindow() {
    return (
        <window
            name="package-manager"
            namespace="package-manager"
            class="package-window"
            visible
            keymode={Astal.Keymode.EXCLUSIVE}
            anchor={Astal.WindowAnchor.TOP}
            application={app}
        >
            <Gtk.EventControllerKey onKeyPressed={(_, keyval) => {
                if (keyval === ESC_KEYVAL) {
                    app.quit();
                    return true;
                }
                return false;
            }} />
            <box class="package-panel" orientation={Gtk.Orientation.VERTICAL} spacing={14} marginTop={WINDOW_MARGIN_TOP}>
                <box class="header">
                    <box orientation={Gtk.Orientation.VERTICAL} hexpand>
                        <label class="title" xalign={0} label="Paket Installation / Updates" />
                        <label class="subtitle" xalign={0} label="Pacman und AUR Suche, Installation direkt in AGS." />
                    </box>
                    <button class="icon-button close" tooltipText="Schliessen" onClicked={() => app.quit()}>
                        <label label="" />
                    </button>
                </box>
                <box class="toolbar" spacing={8}>
                    <entry
                        hexpand
                        placeholderText="Paket suchen, z.B. firefox, docker, obsidian..."
                        onChanged={entry => {
                            query = entry.get_text();
                        }}
                        onActivate={entry => {
                            query = entry.get_text();
                            searchPackages();
                        }}
                    />
                    <button class="button primary" sensitive={!busy} onClicked={searchPackages} label={busy ? "Suche..." : "Suchen"} />
                </box>
                <box class="toolbar" spacing={8}>
                    <HelperButton id="paru" label="paru" />
                    <HelperButton id="pacman" label="pacman" />
                    <button class="button" sensitive={!busy} onClicked={updateSystem} label="System updaten" />
                </box>
                <box class="status-strip">
                    <label class={errorMessage ? "error" : "muted"} xalign={0} hexpand label={errorMessage || statusMessage} />
                </box>
                <OperationPanel />
                <scrolledwindow
                    class="results-scroll"
                    hexpand
                    vexpand
                    hscrollbarPolicy={Gtk.PolicyType.NEVER}
                    vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
                >
                    {results.length
                        ? (
                            <box orientation={Gtk.Orientation.VERTICAL} spacing={8}>
                                {results.map(pkg => <PackageRow pkg={pkg} />)}
                            </box>
                        )
                        : (
                            <box class="empty">
                                <label hexpand xalign={0.5} label="Noch keine Suchergebnisse." />
                            </box>
                        )}
                </scrolledwindow>
            </box>
        </window>
    );
}

function rebuild() {
    const win = app.get_window("package-manager");
    if (!win) {
        return;
    }
    win.set_child(
        <box class="package-panel" orientation={Gtk.Orientation.VERTICAL} spacing={14} marginTop={WINDOW_MARGIN_TOP}>
            <box class="header">
                <box orientation={Gtk.Orientation.VERTICAL} hexpand>
                    <label class="title" xalign={0} label="Paket Installation / Updates" />
                    <label class="subtitle" xalign={0} label="Pacman und AUR Suche, Installation direkt in AGS." />
                </box>
                <button class="icon-button close" tooltipText="Schliessen" onClicked={() => app.quit()}>
                    <label label="" />
                </button>
            </box>
            <box class="toolbar" spacing={8}>
                <entry
                    hexpand
                    text={query}
                    placeholderText="Paket suchen, z.B. firefox, docker, obsidian..."
                    onChanged={entry => {
                        query = entry.get_text();
                    }}
                    onActivate={entry => {
                        query = entry.get_text();
                        searchPackages();
                    }}
                />
                <button class="button primary" sensitive={!busy} onClicked={searchPackages} label={busy ? "Suche..." : "Suchen"} />
            </box>
            <box class="toolbar" spacing={8}>
                <HelperButton id="paru" label="paru" />
                <HelperButton id="pacman" label="pacman" />
                <button class="button" sensitive={!busy} onClicked={updateSystem} label="System updaten" />
            </box>
            <box class="status-strip">
                <label class={errorMessage ? "error" : "muted"} xalign={0} hexpand label={errorMessage || statusMessage} />
            </box>
            <OperationPanel />
            <scrolledwindow
                class="results-scroll"
                hexpand
                vexpand
                hscrollbarPolicy={Gtk.PolicyType.NEVER}
                vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
            >
                {results.length
                    ? (
                        <box orientation={Gtk.Orientation.VERTICAL} spacing={8}>
                            {results.map(pkg => <PackageRow pkg={pkg} />)}
                        </box>
                    )
                    : (
                        <box class="empty">
                            <label hexpand xalign={0.5} label="Noch keine Suchergebnisse." />
                        </box>
                    )}
            </scrolledwindow>
        </box> as Gtk.Widget
    );
}

app.start({
    css,
    instanceName: "package-manager",
    main() {
        PackageManagerWindow();
    },
});
