import app from "ags/gtk4/app";
import { Astal, Gtk } from "ags/gtk4";
import { readFile } from "ags/file";
import { execAsync } from "ags/process";
import { createRoot } from "gnim";
import css from "./widget-panel.css";
import GLib from "gi://GLib";

const HYPR_DIR = GLib.getenv("HYPR_DIR") || `${GLib.get_home_dir()}/.config/hypr`;
const THEME_DIR = `${HYPR_DIR}/Themes`;
const CURRENT_WALLPAPER = `${HYPR_DIR}/current-wallpaper`;
const REFRESH_SECONDS = 2;
const WEATHER_SECONDS = 20 * 60;
const HISTORY_LIMIT = 22;
const ESC_KEYVAL = 65307;
const START_HIDDEN = GLib.getenv("WIDGET_PANEL_START_HIDDEN") === "1";

type UiTheme = {
    accent: string;
    accent2: string;
    background: string;
    backgroundSoft: string;
    foreground: string;
    muted: string;
    panelHex: string;
};

type SystemSnapshot = {
    cpu: number;
    memory: number;
    disk: number;
    temp: string;
    uptime: string;
};

let lastCpuTotal = 0;
let lastCpuIdle = 0;
let system: SystemSnapshot = {
    cpu: 0,
    memory: 0,
    disk: 0,
    temp: "n/a",
    uptime: "n/a",
};
let cpuHistory: number[] = [];
let weather = "Wetter wird geladen...";
let weatherUpdated = 0;
let timerStarted = false;
let disposeRebuild: (() => void) | null = null;
let panelWindow: Gtk.Window | null = null;

function readText(path: string) {
    try {
        return readFile(path);
    } catch {
        return "";
    }
}

function listFiles(dir: string, predicate: (path: string, name: string) => boolean) {
    try {
        const directory = GLib.Dir.open(dir, 0);
        const files: string[] = [];
        let name = directory.read_name();

        while (name !== null) {
            const path = `${dir}/${name}`;
            if (predicate(path, name)) {
                files.push(path);
            }
            name = directory.read_name();
        }

        return files.sort((a, b) => a.localeCompare(b));
    } catch {
        return [];
    }
}

function shellValue(contents: string, key: string) {
    const regex = new RegExp(`^${key}=(["']?)(.*?)\\1$`, "m");
    return contents.match(regex)?.[2] || "";
}

function currentWallpaper() {
    return readText(CURRENT_WALLPAPER).trim();
}

function activeTheme(): UiTheme {
    const fallback = {
        accent: "#f38ba8",
        accent2: "#cba6f7",
        background: "rgba(24, 20, 31, 0.96)",
        backgroundSoft: "rgba(49, 50, 68, 0.82)",
        foreground: "#f5e0dc",
        muted: "#cdd6f4",
        panelHex: "#313244",
    };

    const activeWallpaper = currentWallpaper();
    const themeFile = listFiles(THEME_DIR, (_path, name) => name.endsWith(".theme"))
        .find(path => shellValue(readText(path), "WALLPAPER") === activeWallpaper);

    if (!themeFile) {
        return fallback;
    }

    const contents = readText(themeFile);
    return {
        accent: shellValue(contents, "ACCENT") || fallback.accent,
        accent2: shellValue(contents, "ACCENT_2") || fallback.accent2,
        background: shellValue(contents, "BACKGROUND") || fallback.background,
        backgroundSoft: shellValue(contents, "BACKGROUND_SOFT") || fallback.backgroundSoft,
        foreground: shellValue(contents, "FOREGROUND") || fallback.foreground,
        muted: shellValue(contents, "MUTED") || fallback.muted,
        panelHex: shellValue(contents, "PANEL_HEX") || fallback.panelHex,
    };
}

function themeCss(theme: UiTheme) {
    return [
        `@define-color ags_accent ${theme.accent};`,
        `@define-color ags_accent_2 ${theme.accent2};`,
        `@define-color ags_bg ${theme.background};`,
        `@define-color ags_bg_soft ${theme.backgroundSoft};`,
        `@define-color ags_fg ${theme.foreground};`,
        `@define-color ags_muted ${theme.muted};`,
        `@define-color ags_panel ${theme.panelHex};`,
        css,
    ].join("\n");
}

function clamp(value: number, min = 0, max = 100) {
    return Math.max(min, Math.min(max, value));
}

function percentLabel(value: number) {
    return `${Math.round(clamp(value))}%`;
}

function updateSystem() {
    const cpuLine = readText("/proc/stat").split("\n")[0] || "";
    const cpuValues = cpuLine.trim().split(/\s+/).slice(1).map(value => Number(value) || 0);
    const idle = (cpuValues[3] || 0) + (cpuValues[4] || 0);
    const total = cpuValues.reduce((sum, value) => sum + value, 0);
    const totalDelta = total - lastCpuTotal;
    const idleDelta = idle - lastCpuIdle;

    if (lastCpuTotal > 0 && totalDelta > 0) {
        system.cpu = clamp(((totalDelta - idleDelta) / totalDelta) * 100);
    }

    lastCpuTotal = total;
    lastCpuIdle = idle;

    const meminfo = readText("/proc/meminfo");
    const memTotal = Number(meminfo.match(/^MemTotal:\s+(\d+)/m)?.[1] || 0);
    const memAvailable = Number(meminfo.match(/^MemAvailable:\s+(\d+)/m)?.[1] || 0);
    if (memTotal > 0) {
        system.memory = clamp(((memTotal - memAvailable) / memTotal) * 100);
    }

    const uptimeSeconds = Number(readText("/proc/uptime").split(" ")[0] || 0);
    const hours = Math.floor(uptimeSeconds / 3600);
    const minutes = Math.floor((uptimeSeconds % 3600) / 60);
    system.uptime = `${hours}h ${minutes}m`;

    const temps = listFiles("/sys/class/thermal", (_path, name) => name.startsWith("thermal_zone"))
        .map(path => Number(readText(`${path}/temp`).trim()) / 1000)
        .filter(value => Number.isFinite(value) && value > 0);
    system.temp = temps.length ? `${Math.round(Math.max(...temps))} C` : "n/a";

    execAsync(["bash", "-lc", "df -P / | awk 'NR==2 {gsub(/%/, \"\", $5); print $5}'"])
        .then(output => {
            system.disk = clamp(Number(output.trim()) || system.disk);
        })
        .catch(console.error);

    cpuHistory = [...cpuHistory, system.cpu].slice(-HISTORY_LIMIT);
}

function updateWeather(force = false) {
    if (!force && Date.now() - weatherUpdated < WEATHER_SECONDS * 1000) {
        return;
    }

    weatherUpdated = Date.now();
    execAsync(["bash", "-lc", "curl -fsS --max-time 5 'https://wttr.in/?format=%l:+%c+%t+%w' 2>/dev/null || true"])
        .then(output => {
            weather = output.trim() || "Wetter nicht erreichbar";
        })
        .catch(() => {
            weather = "Wetter nicht erreichbar";
        });
}

function sparkline(values: number[]) {
    const blocks = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
    if (!values.length) {
        return "·".repeat(HISTORY_LIMIT);
    }

    return values
        .map(value => blocks[Math.round((clamp(value) / 100) * (blocks.length - 1))])
        .join("");
}

function monthDays() {
    const now = new Date();
    const year = now.getFullYear();
    const month = now.getMonth();
    const first = new Date(year, month, 1);
    const last = new Date(year, month + 1, 0);
    const startOffset = (first.getDay() + 6) % 7;
    const cells: { day: string; today: boolean; muted: boolean }[] = [];

    for (let i = 0; i < startOffset; i += 1) {
        cells.push({ day: "", today: false, muted: true });
    }

    for (let day = 1; day <= last.getDate(); day += 1) {
        cells.push({ day: String(day), today: day === now.getDate(), muted: false });
    }

    while (cells.length % 7 !== 0) {
        cells.push({ day: "", today: false, muted: true });
    }

    return cells;
}

function monthTitle() {
    return new Intl.DateTimeFormat("de-DE", { month: "long", year: "numeric" }).format(new Date());
}

function Metric({ icon, label, value }: { icon: string; label: string; value: number }) {
    return (
        <box class="metric" orientation={Gtk.Orientation.VERTICAL} spacing={8}>
            <box>
                <label class="metric-name" hexpand xalign={0} label={`${icon}  ${label}`} />
                <label class="metric-value" label={percentLabel(value)} />
            </box>
            <box class="progress">
                <box class="progress-fill" css={`min-width: ${Math.round(clamp(value) * 2.3)}px;`} />
            </box>
        </box>
    );
}

function SystemMonitor() {
    return (
        <box class="card" orientation={Gtk.Orientation.VERTICAL} spacing={12}>
            <label class="card-title" xalign={0} label="System Monitoring" />
            <Metric icon="" label="CPU" value={system.cpu} />
            <Metric icon="" label="RAM" value={system.memory} />
            <Metric icon="" label="Disk /" value={system.disk} />
            <box class="meta-row">
                <label hexpand xalign={0} label={`  ${system.temp}`} />
                <label xalign={1} label={`  ${system.uptime}`} />
            </box>
            <label class="sparkline" xalign={0} label={sparkline(cpuHistory)} />
        </box>
    );
}

function Calendar() {
    const cells = monthDays();
    const weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];

    return (
        <box class="card" orientation={Gtk.Orientation.VERTICAL} spacing={10}>
            <label class="card-title" xalign={0} label={monthTitle()} />
            <box class="calendar-grid" orientation={Gtk.Orientation.VERTICAL} spacing={5}>
                <box spacing={5}>
                    {weekdays.map(day => <label class="weekday" label={day} />)}
                </box>
                {Array.from({ length: Math.ceil(cells.length / 7) }, (_unused, row) => (
                    <box spacing={5}>
                        {cells.slice(row * 7, row * 7 + 7).map(cell => (
                            <label
                                class={`day ${cell.today ? "today" : ""} ${cell.muted ? "muted" : ""}`}
                                label={cell.day}
                            />
                        ))}
                    </box>
                ))}
            </box>
        </box>
    );
}

function Weather() {
    return (
        <box class="card weather" orientation={Gtk.Orientation.VERTICAL} spacing={10}>
            <box>
                <label class="card-title" hexpand xalign={0} label="Wetter" />
                <button class="icon-button" tooltipText="Aktualisieren" onClicked={() => updateWeather(true)}>
                    <label label="" />
                </button>
            </box>
            <label class="weather-main" xalign={0} wrap label={weather} />
            <label class="card-subtitle" xalign={0} label="Quelle: wttr.in" />
        </box>
    );
}

function hidePanel() {
    const win = panelWindow || app.get_window("widget-panel");
    win?.set_visible(false);
}

function PanelContent() {
    return (
        <box class="panel" orientation={Gtk.Orientation.VERTICAL} spacing={14}>
            <box class="header">
                <box orientation={Gtk.Orientation.VERTICAL} hexpand>
                    <label class="title" xalign={0} label="Widgetbereich" />
                    <label class="subtitle" xalign={0} label="System, Kalender und Wetter" />
                </box>
                <button class="icon-button close" tooltipText="Schliessen" onClicked={hidePanel}>
                    <label label="" />
                </button>
            </box>
            <SystemMonitor />
            <Calendar />
            <Weather />
        </box>
    );
}

function panelLayout() {
    const geometry = app.monitors[0]?.get_geometry();
    const screenWidth = geometry?.width || 1280;
    const screenHeight = geometry?.height || 720;

    return {
        width: Math.round(clamp(screenWidth * 0.28, 360, 460)),
        height: Math.round(clamp(screenHeight - 96, 560, 900)),
    };
}

function WidgetPanelWindow() {
    const layout = panelLayout();

    return (
        <window
            name="widget-panel"
            namespace="widget-panel"
            class="widget-window"
            visible={!START_HIDDEN}
            keymode={Astal.Keymode.EXCLUSIVE}
            anchor={Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM}
            application={app}
        >
            <Gtk.EventControllerKey onKeyPressed={(_, keyval) => {
                if (keyval === ESC_KEYVAL) {
                    hidePanel();
                    return true;
                }
                return false;
            }} />
            <box marginStart={18} marginTop={48} marginBottom={48}>
                <scrolledwindow
                    class="panel-scroll"
                    widthRequest={layout.width}
                    heightRequest={layout.height}
                    hscrollbarPolicy={Gtk.PolicyType.NEVER}
                    vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
                >
                    <PanelContent />
                </scrolledwindow>
            </box>
        </window>
    );
}

function rebuild() {
    const win = app.get_window("widget-panel");
    if (!win) {
        return;
    }

    disposeRebuild?.();
    const layout = panelLayout();
    createRoot(dispose => {
        disposeRebuild = dispose;
        win.set_child(
            <box marginStart={18} marginTop={48} marginBottom={48}>
                <scrolledwindow
                    class="panel-scroll"
                    widthRequest={layout.width}
                    heightRequest={layout.height}
                    hscrollbarPolicy={Gtk.PolicyType.NEVER}
                    vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
                >
                    <PanelContent />
                </scrolledwindow>
            </box> as Gtk.Widget,
        );
    });
}

function startTimer() {
    if (timerStarted) {
        return;
    }

    timerStarted = true;
    updateSystem();
    updateWeather(true);

    GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, REFRESH_SECONDS, () => {
        updateSystem();
        updateWeather();
        rebuild();
        return GLib.SOURCE_CONTINUE;
    });
}

app.start({
    instanceName: "widget-panel",
    css: themeCss(activeTheme()),
    main() {
        panelWindow = WidgetPanelWindow() as Gtk.Window;
        app.add_window(panelWindow);
        if (!START_HIDDEN) {
            panelWindow.present();
        }
        GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
            startTimer();
            rebuild();
            return GLib.SOURCE_REMOVE;
        });
    },
});
