import { Astal, Gdk, Gtk } from "ags/gtk4";
import app from "ags/gtk4/app";
import {
    createBinding,
    createComputed,
    createState,
    For,
    With,
    Accessor,
} from "ags";
import { createPoll } from "ags/time";
import { subprocess, execAsync } from "ags/process";
import Pango from "gi://Pango";
import Tray from "gi://AstalTray";
import { getIconName } from "./utils";
import Wp from "gi://AstalWp";
import Battery from "gi://AstalBattery";
import GLib from "gi://GLib";
import Gio from "gi://Gio";

const battery = Battery.get_default();
const sensorsAvailable = await execAsync(["sensors"])
    .then(() => true)
    .catch(() => false);
const wirePlumber = Wp.get_default();

// --- NIRI IPC INTEGRATION ---
const [workspaces, setWorkspaces] = createState<any[]>([]);
const [windows, setWindows] = createState<any[]>([]);

async function updateNiriState() {
    try {
        const wsOut = await execAsync(["niri", "msg", "-j", "workspaces"]);
        setWorkspaces(JSON.parse(wsOut) || []);

        const winOut = await execAsync(["niri", "msg", "-j", "windows"]);
        setWindows(JSON.parse(winOut) || []);
    } catch (e) {
        console.error("Failed to fetch Niri state:", e);
    }
}

// Initialize state immediately
updateNiriState();

// Subscribe to Niri's native event stream
subprocess(
    ["niri", "msg", "-j", "event-stream"],
    (line) => {
        // A state change happened in Niri. Re-fetching guarantees perfect sync
        updateNiriState();
    },
    (err) => console.error("Niri event stream error:", err)
);
// ----------------------------

function SysTray(): JSX.Element {
    const tray = Tray.get_default();
    let items = createBinding(tray, "items");

    const init = (btn: Gtk.MenuButton, item: Tray.TrayItem) => {
        btn.menuModel = item.menuModel;
        btn.insert_action_group("dbusmenu", item.actionGroup);
        item.connect("notify::action-group", () => {
            btn.insert_action_group("dbusmenu", item.actionGroup);
        });
    };

    return (
        <box>
            <For each={items}>
                {(item: Tray.TrayItem) => {
                    if (item.iconThemePath) app.add_icons(item.iconThemePath);
                    return (
                        <menubutton
                            $={(self) => init(self, item)}
                            class="systray"
                            tooltipMarkup={createBinding(item, "tooltipMarkup")}
                            menuModel={createBinding(item, "menuModel")}
                        >
                            <image
                                gicon={createBinding(item, "gicon")}
                                class="systray-item"
                            />
                        </menubutton>
                    );
                }}
            </For>
        </box>
    );
}

function Left({ connector }: { connector: string }): JSX.Element {
    return (
        <box hexpand halign={Gtk.Align.START}>
            <Clients connector={connector} />
        </box>
    );
}

function Center({ connector }: { connector: string }): JSX.Element {
    return (
        <box>
            <Workspaces connector={connector} />
        </box>
    );
}

function Date({ format = "%Y-%m-%d" }): JSX.Element {
    const time = createPoll<string>(
        "",
        60000,
        () => GLib.DateTime.new_now_local().format(format)!,
    );
    return (
        <button
            class="item"
            label={time}
            onClicked={() => execAsync(["gnome-calendar"])}
        />
    );
}

function Time({ format = "%H:%M:%S" }): JSX.Element {
    const time = createPoll<string>(
        "",
        1000,
        () => GLib.DateTime.new_now_local().format(format)!,
    );
    return <label class="item blue" label={time} />;
}

function Temp(): JSX.Element {
    let [label, _setlabel] = createState<string>("N/A");
    if (sensorsAvailable) {
        label = createPoll<string>("", 3000, "sensors", (out) => {
            const match = out
                .split("\n")
                .find((line) => line.includes("Tctl") || line.includes("Package"))
                ?.match(/[0-9.]*°C/);
            return match ? match[0] : "N/A";
        });
    }
    return <label class="item blue" label={label} />;
}

function Memory(): JSX.Element {
    const memory = createPoll<string>("", 2000, "free", (out) => {
        const line = out.split("\n").find((line) => line.includes("Mem:"));
        if (!line) return "N/A";
        const split = line.split(/\s+/).map(Number);
        return (split[2] / 1000000).toFixed(2) + "GB";
    });
    return <label class="item blue" label={memory} />;
}

function ClockSpeed(): JSX.Element {
    const command =
        'bash -c "cat /proc/cpuinfo | grep \\"MHz\\" | awk \'{print \\$4}\' | sort -n | tail -1 | awk \'{printf \\"%.2fGHz\\", \\$1/1000}\'"';
    const speed = createPoll<string>("", 1000, command, (out) => out);
    return <label class="item" label={speed} />;
}

function CPU(): JSX.Element {
    const usage = createPoll<string>("", 2000, "top -b -n 1", (out) => {
        const line = out.split("\n").find((line) => line.includes("Cpu(s)"));
        if (!line) return "N/A";
        return line.split(/\s+/)[1].replace(",", ".").toString() + "%";
    });
    return (
        <box class="item">
            <image iconName="speedometer" css="margin-right: 0.7em;" />
            <label label={usage} />
        </box>
    );
}

function Right() {
    return (
        <box class="right" hexpand halign={Gtk.Align.END} spacing={6}>
            <Icons />
            <Volume />
            <CPU />
            <Memory />
            <ClockSpeed />
            <Temp />
            <Date />
            <Time />
        </box>
    );
}

// Icon names to try for the current battery state, most specific first.
// Papirus does not ship every combination, so each entry needs a fallback:
// "-charged-symbolic" only exists at level 100, and "-charging-symbolic" only
// up to level 90 (above that the panel icon has to be named directly).
function batteryIconNames(): string[] {
    const percentage = battery.percentage;
    const thresholds = [...Array(11).keys()].map((i) => i * 10);
    const level = thresholds.find((threshold) => threshold >= percentage * 100);
    const plain = `battery-level-${level}-symbolic`;

    // Being full is a state, not a 100% reading: with a charge limit set the
    // battery stops well below 100 and sits there reporting FULLY_CHARGED.
    if (battery.state === Battery.State.FULLY_CHARGED) {
        return [`battery-level-${level}-charged-symbolic`, plain];
    }
    if (battery.charging) {
        const padded = String(level).padStart(3, "0");
        return [
            `battery-level-${level}-charging-symbolic`,
            `battery-${padded}-charging`,
            plain,
        ];
    }
    return [plain];
}

// Papirus encodes the battery level as a 35%-opacity overlay inside its
// *-symbolic icons. GTK4's symbolic recoloring flattens that opacity, so every
// level renders as a solid, full battery. The Papirus *-symbolic icons are just
// symlinks to non-symbolic panel SVGs which keep the opacity when rendered
// normally — so we resolve each name to that underlying file and load it
// directly (as a non-symbolic file icon), which renders the correct level.
function batteryGicon(): Gio.Icon {
    const theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!);
    // lookup_icon() never fails: for an unknown name it hands back the
    // "image-missing" paintable, so check availability before resolving.
    const names = batteryIconNames();
    const name = names.find((n) => theme.has_icon(n)) ?? names[names.length - 1];
    const symbolic = theme
        .lookup_icon(name, null, 16, 1, Gtk.TextDirection.NONE, 0)
        .get_file();
    if (!symbolic) return Gio.ThemedIcon.new(name);
    try {
        const target = symbolic
            .query_info(
                "standard::symlink-target",
                Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
                null,
            )
            .get_symlink_target();
        if (target) {
            return new Gio.FileIcon({
                file: symbolic.get_parent()!.resolve_relative_path(target),
            });
        }
    } catch (e) {}
    return new Gio.FileIcon({ file: symbolic });
}

function batteryTooltip(): string {
    return `${Math.round(battery.percentage * 100)}%`;
}

function BatteryIcon(): JSX.Element {
    if (battery.get_state() == 0) return <box />;
    // AstalBattery does not emit notify::percentage/charging/state on this
    // system, so a signal binding stays frozen at its initial value (e.g.
    // showing "charged" forever). Poll the live values instead.
    const gicon = createPoll<Gio.Icon>(batteryGicon(), 10000, batteryGicon);
    const tooltip = createPoll<string>(batteryTooltip(), 10000, batteryTooltip);
    return (
        <button
            class="battery-item"
            tooltipText={tooltip}
            onClicked={() => execAsync(["gnome-power-statistics"])}
        >
            <box>
                <image gicon={gicon} />
            </box>
        </button>
    );
}

function Icons() {
    return (
        <box class="item icon-group">
            <SysTray />
            <BatteryIcon />
        </box>
    );
}

function SpeakerButton({ speaker }: { speaker: Wp.Endpoint }): JSX.Element {
    const volume = createBinding(speaker, "volume");
    const mute = createBinding(speaker, "mute");

    const icon = volume.as((vol) => {
        const percentage = vol * 100;
        const level = [
            [101, "overamplified"],
            [67, "high"],
            [34, "medium"],
            [1, "low"],
            [0, "muted"],
        ].find(([threshold]) => Number(threshold) <= percentage)?.[1];
        return `audio-volume-${level}-symbolic`;
    });
    const label = createComputed(() =>
        mute() ? "" : `${Math.floor(volume() * 100)}%`,
    );
    const css = mute.as((muted) =>
        muted ? "margin-left: 0;" : "margin-left: 0.7em;",
    );

    return (
        <button class="item blue" onClicked={() => (speaker.mute = !speaker.mute)}>
            <box>
                <image iconName={icon} />
                <label label={label} css={css} />
            </box>
        </button>
    );
}

function Volume(): JSX.Element {
    if (!wirePlumber) return <box />;

    // WirePlumber swaps out the whole endpoint object whenever the default
    // sink changes, e.g. when a Bluetooth headset connects. Binding to the
    // endpoint that happened to be default at startup leaves the widget stuck
    // on that device forever, so track the property and rebuild on every swap.
    const speaker = createBinding(wirePlumber.audio, "defaultSpeaker");
    return (
        <With value={speaker}>
            {(endpoint: Wp.Endpoint | null) =>
                endpoint ? <SpeakerButton speaker={endpoint} /> : <box />
            }
        </With>
    );
}

function Workspaces({ connector }: { connector: string }): JSX.Element {
    return (
        <box class="workspaces">
            {/* Generate a derived binding by passing a transformation function into the `workspaces` state */}
            <For each={workspaces((wss: any[]) =>
                wss.filter((ws) => ws.output === connector)
                   .filter((ws) => ws.is_active || windows().some((w: any) => w.workspace_id === ws.id))
                   .sort((a, b) => a.idx - b.idx)
            )}>
                {(ws: any) => (
                    <button
                        class={ws.is_active ? "focused" : ""}
                        onClicked={() => execAsync(["niri", "msg", "action", "focus-workspace", ws.idx.toString()])}
                    >
                        {ws.name || ws.idx.toString()}
                    </button>
                )}
            </For>
        </box>
    );
}

const MAX_TOTAL_CHARS = 120;
const MIN_TITLE_CHARS = 10;
const OVERHEAD_PER_WINDOW = 5; // icon + padding + margin in character-width equivalents

function computeTitleBudgets(wins: any[]): any[] {
    const totalLength = wins.reduce((sum, w) => sum + (w.title || "").length, 0);
    const count = wins.length;
    const totalWithOverhead = totalLength + count * OVERHEAD_PER_WINDOW;

    let maxChars: number;
    if (count === 0 || totalWithOverhead <= MAX_TOTAL_CHARS) {
        maxChars = -1;
    } else {
        const charsAvailable = MAX_TOTAL_CHARS - count * OVERHEAD_PER_WINDOW;
        maxChars = Math.max(MIN_TITLE_CHARS, Math.floor(charsAvailable / count));
    }

    return wins.map(w => ({ ...w, maxChars }));
}

function Clients({ connector }: { connector: string }): JSX.Element {
    return (
        <box class="clients">
            {/* Use With to react to workspace changes, so we know which WS is active */}
            <With value={workspaces}>
                {(wss: any[]) => {
                    let active_ws_for_monitor = wss.find((ws: any) => ws.is_active && ws.output === connector)?.id;

                    return (
                        <box>
                            {/* Generate a derived binding for windows, depending on the active workspace */}
                            <For each={windows((wins: any[]) =>
                                computeTitleBudgets(
                                    wins.filter((w: any) => !w.title?.includes("rofi"))
                                        .filter((w: any) => w.workspace_id === active_ws_for_monitor)
                                        .sort((a: any, b: any) => {
                                            const posA = a.layout?.pos_in_scrolling_layout?.[0] ?? 0;
                                            const posB = b.layout?.pos_in_scrolling_layout?.[0] ?? 0;
                                            return posA - posB;
                                        })
                                )
                            )}>
                                {(win: any) => (
                                    <box class={win.is_focused ? "focused" : "unfocused"}>
                                        <image iconName={getIconName(win.app_id, win.title)} class="app-icon" />
                                        <label
                                            label={win.title || ""}
                                            maxWidthChars={win.maxChars}
                                            ellipsize={Pango.EllipsizeMode.END}
                                        />
                                    </box>
                                )}
                            </For>
                        </box>
                    );
                }}
            </With>
        </box>
    );
}

export default function Bar(gdkmonitor: Gdk.Monitor, scaleFactor: number = 1) {
    console.log("Creating Bar on monitor:", gdkmonitor.get_connector());
    const connector = gdkmonitor.get_connector() || "";

    return (
        <window
            visible
            gdkmonitor={gdkmonitor}
            css={"font-size: " + scaleFactor + "em;"}
            exclusivity={Astal.Exclusivity.EXCLUSIVE}
            anchor={
                Astal.WindowAnchor.TOP |
                Astal.WindowAnchor.LEFT |
                Astal.WindowAnchor.RIGHT
            }
            application={app}
            class="Bar"
            name={`top-bar-${connector}`}
        >
            <centerbox class="window-box">
                <Left $type="start" connector={connector} />
                <Center $type="center" connector={connector} />
                <Right $type="end" />
            </centerbox>
        </window>
    );
}
