import { Astal, Gdk, Gtk } from "ags/gtk4";
import app from "ags/gtk4/app";
import { createBinding, 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";

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>
    );
}

function BatteryIcon(): JSX.Element {
    if (battery.get_state() == 0) return <box />;
    let batteryPercentage = createBinding(battery, "percentage");
    return (
        <button
            class="battery-item"
            onClicked={() => execAsync(["gnome-power-statistics"])}
        >
            <box>
                <With value={batteryPercentage}>
                    {(percentage) => {
                        const thresholds = [...Array(11).keys()].map((i) => i * 10);
                        const icon = thresholds.find(
                            (threshold) => threshold >= percentage * 100,
                        );
                        const charging_name =
                            battery.percentage >= 0.99 ? "charged" : "charging";
                        return (
                            <image
                                iconName={
                                    battery.charging
                                        ? `battery-level-${icon}-${charging_name}-symbolic`
                                        : `battery-level-${icon}-symbolic`
                                }
                            />
                        );
                    }}
                </With>
            </box>
        </button>
    );
}

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

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

    const audio = wirePlumber.audio;
    const icon = createBinding(audio.default_speaker, "volume").as((volume) => {
        const vol = volume * 100;
        const icon = [
            [101, "overamplified"],
            [67, "high"],
            [34, "medium"],
            [1, "low"],
            [0, "muted"],
        ].find(([threshold]) => Number(threshold) <= vol)?.[1];
        return `audio-volume-${icon}-symbolic`;
    });
    const css = createBinding(audio.default_speaker, "mute").as((mute) => {
        return mute ? "margin-left:0;" : "margin-left: 0.7em;";
    });
    let volume = createBinding(audio.default_speaker, "volume");
    let mute = createBinding(audio.default_speaker, "mute");
    return (
        <button
            class="item blue"
            onClicked={() =>
                (audio.default_speaker.mute = !audio.default_speaker.mute)
            }
        >
            <box>
                <image iconName={icon} />
                <With value={volume}>
                    {(vol) => (
                        <box>
                            <With value={mute}>
                                {(muted) => {
                                    return (
                                        <label
                                            label={muted ? "" : `${Math.floor(vol * 100)}%`}
                                            css={css}
                                        />
                                    );
                                }}
                            </With>
                        </box>
                    )}
                </With>
            </box>
        </button>
    );
}

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>
    );
}
