pragma Singleton

// A single focus session bound to a Hyprland workspace. The deadline and the
// few user decisions are persisted; the once-per-second display value is not,
// so an active session does not turn into a constant disk writer.

import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import QtQuick
import qs.config

Singleton {
    id: root

    readonly property alias active: state.active
    readonly property alias paused: state.paused
    readonly property alias capsuleVisible: state.capsuleVisible
    readonly property alias workspaceId: state.workspaceId
    readonly property alias workspaceLabel: state.workspaceLabel
    readonly property alias monitorName: state.monitorName

    // Kept in memory only. The persisted deadline is enough to reconstruct it
    // after a reload, and paused sessions persist their fixed remainder.
    property double nowMs: Date.now()

    readonly property int remainingSeconds: {
        if (!state.active)
            return 0;
        if (state.paused)
            return Math.max(0, state.pausedRemainingSeconds);
        return Math.max(0, Math.ceil((state.deadlineMs - root.nowMs) / 1000));
    }

    readonly property string remainingText: {
        const total = root.remainingSeconds;
        const hours = Math.floor(total / 3600);
        const minutes = Math.floor((total % 3600) / 60);
        const seconds = total % 60;
        const pad = value => String(value).padStart(2, "0");
        return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;
    }

    readonly property string statusText: state.paused ? "Paused" : "Do Not Disturb · Caffeine";

    FileView {
        id: stateFile

        path: Quickshell.stateDir + "/focus-session.json"
        blockLoading: true
        printErrors: false
        atomicWrites: true
        onLoaded: restoreTimer.restart()

        JsonAdapter {
            id: state

            property bool active: false
            property bool paused: false
            property bool capsuleVisible: false
            property int workspaceId: 0
            property string workspaceLabel: ""
            property string monitorName: ""
            property double deadlineMs: 0
            property int pausedRemainingSeconds: 0
            property bool previousDnd: false
            property bool previousCaffeine: false
        }
    }

    Timer {
        interval: 1000
        repeat: true
        running: state.active && !state.paused
        triggeredOnStart: true
        onTriggered: {
            root.nowMs = Date.now();
            if (state.deadlineMs > 0 && root.nowMs >= state.deadlineMs)
                root.end(true);
        }
    }

    // Defer restoration until the other service singletons have completed
    // construction. An active focus session owns these states across reloads.
    Timer {
        id: restoreTimer
        interval: 0
        onTriggered: root.restore()
    }

    Component.onCompleted: {
        // `blockLoading` usually makes this true immediately. Keep the loaded
        // signal above as the authoritative path for slower storage.
        if (stateFile.loaded)
            restoreTimer.restart();
    }

    function start(minutes: int): void {
        const duration = Math.max(1, minutes);
        const workspace = root.currentPositiveWorkspace();

        // Replacing an active session first releases exactly the state it owns.
        if (state.active)
            root.end(false);

        state.previousDnd = Notifs.doNotDisturb;
        state.previousCaffeine = Caffeine.enabled;
        Notifs.doNotDisturb = true;
        Caffeine.enabled = true;

        state.workspaceId = workspace ? workspace.id : 1;
        state.workspaceLabel = root.displayWorkspaceName(workspace);
        state.monitorName = Hyprland.focusedMonitor?.name ?? "";
        state.pausedRemainingSeconds = duration * 60;
        state.deadlineMs = Date.now() + duration * 60 * 1000;
        state.paused = false;
        state.capsuleVisible = true;
        state.active = true;
        root.nowMs = Date.now();
        stateFile.writeAdapter();
    }

    function startDefault(): void {
        root.start(Settings.focusDurationMinutes);
    }

    // The discoverable "start or show" behavior used by quick settings and
    // Super+Shift+F. A second invocation never destroys work accidentally.
    function reveal(): void {
        if (!state.active) {
            root.startDefault();
            return;
        }
        state.capsuleVisible = true;
        stateFile.writeAdapter();
    }

    function dismiss(): void {
        state.capsuleVisible = false;
        stateFile.writeAdapter();
    }

    function pauseOrResume(): void {
        if (!state.active)
            return;

        root.nowMs = Date.now();
        if (state.paused) {
            state.deadlineMs = root.nowMs + state.pausedRemainingSeconds * 1000;
            state.paused = false;
        } else {
            state.pausedRemainingSeconds = root.remainingSeconds;
            state.paused = true;
        }
        stateFile.writeAdapter();
    }

    function activateWorkspace(): void {
        if (!state.active || state.workspaceId <= 0)
            return;

        const all = Hyprland.workspaces?.values ?? [];
        for (const workspace of all) {
            if (workspace && workspace.id === state.workspaceId) {
                workspace.activate();
                return;
            }
        }

        // Dynamic workspaces can disappear while the session is running.
        // Focusing the stored positive id recreates the destination cleanly.
        Hyprland.dispatch(`hl.dsp.focus({ workspace = ${state.workspaceId} })`);
    }

    function end(completed: bool): void {
        if (!state.active)
            return;

        const previousDnd = state.previousDnd;
        const previousCaffeine = state.previousCaffeine;
        const finishedWorkspaceId = state.workspaceId;
        const finishedWorkspace = state.workspaceLabel;

        state.active = false;
        state.paused = false;
        state.capsuleVisible = false;
        state.workspaceId = 0;
        state.workspaceLabel = "";
        state.monitorName = "";
        state.deadlineMs = 0;
        state.pausedRemainingSeconds = 0;
        stateFile.writeAdapter();

        Notifs.doNotDisturb = previousDnd;
        Caffeine.enabled = previousCaffeine;

        if (completed) {
            StatusEvents.publish({
                key: "focus-complete",
                glyph: "\u{F051F}",
                title: "Focus complete",
                detail: finishedWorkspace ? `${finishedWorkspace} session finished` : "Your focus session finished",
                tone: "ok",
                priority: StatusEvents.importantPriority,
                actionId: "open-workspace",
                actionData: String(finishedWorkspaceId)
            });
            Quickshell.execDetached([
                "notify-send",
                "-a", "Panama",
                "-i", "appointment-soon-symbolic",
                "Focus complete",
                finishedWorkspace ? `${finishedWorkspace} session finished` : "Your focus session finished"
            ]);
        }
    }

    function restore(): void {
        root.nowMs = Date.now();
        if (!state.active)
            return;
        if (!state.paused && state.deadlineMs <= root.nowMs) {
            root.end(true);
            return;
        }

        // Caffeine and the in-process notification server restart on a shell
        // reload. The persisted session therefore has to reclaim both states.
        Notifs.doNotDisturb = true;
        Caffeine.enabled = true;
    }

    function currentPositiveWorkspace(): var {
        const focused = Hyprland.focusedWorkspace;
        if (focused && focused.id > 0)
            return focused;

        const all = Hyprland.workspaces?.values ?? [];
        for (const workspace of all) {
            if (workspace && workspace.id > 0)
                return workspace;
        }
        return null;
    }

    function displayWorkspaceName(workspace: var): string {
        if (!workspace)
            return "Workspace 1";
        const name = workspace.name || String(workspace.id);
        return /^\d+$/.test(name) ? `Workspace ${name}` : name;
    }
}
