// Phase 6 Margot nudge bus. Frontend-owned, subscribes to in-app
// signals Lumotia already produces, applies suppression, and fans out
// to OS notification (via Rust's deliver_nudge) + optional TTS.
//
// Why frontend-only: OS-wide keyboard/window detection is fragile
// across Wayland/macOS/Windows (no sanctioned global-keyboard API on
// Wayland, accessibility permission on macOS, message-loop hook on
// Windows). The app-local signals we already have — timer state,
// task completion, micro-step creation, app focus/visibility — cover
// the Phase 6 trigger set without the platform pain.
//
// Suppression rules:
//   - nudgesEnabled && !nudgesMuted
//   - document has focus  → never nudge (user is already looking)
//   - rolling 1-hour cap of 3 nudges
//   - timer-specific: no nudge in first 60 s of a running timer
//
// Trigger set v1 (in-app only):
//   - inactivity_with_active_timer
//   - pending_morning_triage
//   - micro_step_idle

import { invoke } from "@tauri-apps/api/core";
import { hasTauriRuntime } from "$lib/utils/runtime.js";
import { settings } from "$lib/stores/page.svelte.js";

const HOUR_MS = 60 * 60 * 1000;
const INACTIVITY_TIMER_THRESHOLD_MS = 90_000;
const TIMER_WARMUP_MS = 60_000;
const MICRO_STEP_IDLE_MS = 15 * 60_000;
const MORNING_TRIAGE_CHECK_INTERVAL_MS = 5 * 60_000;
const MORNING_TRIAGE_TRIGGER_HOUR = 10;

interface TimerStartPayload {
  taskId?: string;
  seconds?: number;
}

interface MicroStepPayload {
  parentTaskId: string;
}

let started = false;
let permissionRequested = false;
let permissionGranted = false;

// Rolling timestamps of recent nudge deliveries (ms since epoch).
// Older-than-one-hour entries are pruned lazily on each check.
const recentNudges: number[] = [];

// Per-trigger state.
let timerRunning = false;
let timerStartedAt = 0;
let timerNudgeFiredThisSession = false;
let blurredAt: number | null = null;
let blurCheckHandle: ReturnType<typeof setInterval> | null = null;

let triagePollHandle: ReturnType<typeof setInterval> | null = null;

// parentTaskId → scheduled timeout handle for "idle since step created".
// Cleared when a step or parent task gets marked done in the same session.
const microStepTimers = new Map<string, ReturnType<typeof setTimeout>>();

// --- Permission ------------------------------------------------------

async function ensurePermission(): Promise<boolean> {
  if (!hasTauriRuntime()) return false;
  if (permissionRequested) return permissionGranted;
  permissionRequested = true;
  try {
    const plugin = await import("@tauri-apps/plugin-notification");
    const already = await plugin.isPermissionGranted();
    if (already) {
      permissionGranted = true;
      return true;
    }
    const result = await plugin.requestPermission();
    permissionGranted = result === "granted";
    return permissionGranted;
  } catch {
    permissionGranted = false;
    return false;
  }
}

// --- Suppression -----------------------------------------------------

function pruneRecentNudges(now: number): void {
  while (recentNudges.length > 0 && now - recentNudges[0] > HOUR_MS) {
    recentNudges.shift();
  }
}

function canNudgeNow(now: number): boolean {
  if (!settings.nudgesEnabled) return false;
  if (settings.nudgesMuted) return false;
  if (typeof document !== "undefined" && document.hasFocus()) return false;
  pruneRecentNudges(now);
  if (recentNudges.length >= 3) return false;
  return true;
}

// --- Delivery --------------------------------------------------------

async function deliver(title: string, body: string): Promise<void> {
  const now = Date.now();
  if (!canNudgeNow(now)) return;
  const ok = await ensurePermission();
  if (!ok) return;
  recentNudges.push(now);
  try {
    await invoke("deliver_nudge", { input: { title, body } });
  } catch {
    // Nudges are fire-and-forget — surfacing an error to the user
    // would defeat the "anticipatory, not push-notification" framing.
  }
  if (settings.nudgesSpeakAloud) {
    try {
      await invoke("tts_speak", {
        text: body || title,
        rate: settings.ttsRate,
        voice: settings.ttsVoice ?? null,
      });
    } catch {
      // Same reasoning — never surface TTS failures through a nudge.
    }
  }
}

// --- Trigger: inactivity with active timer ---------------------------

function onTimerStart(event: Event) {
  const detail = (event as CustomEvent<TimerStartPayload>).detail;
  timerRunning = true;
  timerStartedAt = Date.now();
  timerNudgeFiredThisSession = false;
  // If the user switched away before starting the timer, the next
  // blur check (below) will pick it up.
  void detail;
}

function resetTimerState() {
  timerRunning = false;
  timerStartedAt = 0;
  timerNudgeFiredThisSession = false;
}

function onFocus() {
  blurredAt = null;
}

function onBlur() {
  blurredAt = Date.now();
}

function checkInactivityWithActiveTimer(now: number) {
  if (!timerRunning || timerNudgeFiredThisSession) return;
  if (now - timerStartedAt < TIMER_WARMUP_MS) return;
  if (blurredAt === null) return;
  if (now - blurredAt < INACTIVITY_TIMER_THRESHOLD_MS) return;
  timerNudgeFiredThisSession = true;
  void deliver(
    "Still on that timer?",
    "It's been ticking while you've been away. Pick up where you left off, or stop it.",
  );
}

// --- Trigger: pending morning triage ---------------------------------

function todayLocalKey(): string {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}

let triageNudgeFiredOnKey: string | null = null;

async function checkPendingMorningTriage() {
  if (!settings.ritualsMorning) return;
  if (!hasTauriRuntime()) return;
  const now = new Date();
  if (now.getHours() < MORNING_TRIAGE_TRIGGER_HOUR) return;
  const today = todayLocalKey();
  if (triageNudgeFiredOnKey === today) return;
  let lastShown: string | null = null;
  try {
    lastShown = await invoke<string | null>("get_last_morning_triage");
  } catch {
    return;
  }
  if (lastShown === today) return;
  triageNudgeFiredOnKey = today;
  void deliver(
    "A few things waiting",
    "When you're ready, Lumotia has your morning list. No rush.",
  );
}

// --- Trigger: micro-step idle ----------------------------------------

function onMicroStepGenerated(event: Event) {
  const detail = (event as CustomEvent<MicroStepPayload>).detail;
  if (!detail?.parentTaskId) return;
  const parentTaskId = detail.parentTaskId;
  clearMicroStepTimer(parentTaskId);
  const handle = setTimeout(() => {
    microStepTimers.delete(parentTaskId);
    void deliver(
      "Still with that one?",
      "Your breakdown is here when you want to pick the first step.",
    );
  }, MICRO_STEP_IDLE_MS);
  microStepTimers.set(parentTaskId, handle);
}

function clearMicroStepTimer(parentTaskId: string) {
  const handle = microStepTimers.get(parentTaskId);
  if (handle !== undefined) {
    clearTimeout(handle);
    microStepTimers.delete(parentTaskId);
  }
}

function onStepOrTaskCompleted(event: Event) {
  const detail = (event as CustomEvent<{ id?: string; parentTaskId?: string }>).detail;
  // A completed subtask clears its parent's idle timer; a completed
  // parent task clears its own idle timer (the step decomposition
  // attached to that task is no longer waiting on anyone).
  if (detail?.parentTaskId) clearMicroStepTimer(detail.parentTaskId);
  if (detail?.id) clearMicroStepTimer(detail.id);
}

// --- Lifecycle -------------------------------------------------------

export function startNudgeBus(): void {
  if (started) return;
  if (typeof window === "undefined") return;
  started = true;

  window.addEventListener("lumotia:start-timer", onTimerStart);
  window.addEventListener("lumotia:focus-timer-complete", resetTimerState);
  window.addEventListener("lumotia:focus-timer-cancelled", resetTimerState);
  window.addEventListener("lumotia:microstep-generated", onMicroStepGenerated);
  window.addEventListener("lumotia:step-completed", onStepOrTaskCompleted);
  window.addEventListener("lumotia:task-completed", onStepOrTaskCompleted);
  window.addEventListener("focus", onFocus);
  window.addEventListener("blur", onBlur);

  // Inactivity check runs while a timer is live. Low frequency is fine
  // — the nudge itself only fires once the 90 s threshold is crossed.
  blurCheckHandle = setInterval(() => {
    checkInactivityWithActiveTimer(Date.now());
  }, 10_000);

  // Morning triage check runs on an interval + one immediate probe on
  // start. The Rust `get_last_morning_triage` call is cheap (one
  // SQLite read), so polling every 5 min costs nothing and means we
  // catch the 10:00 threshold without a scheduler.
  void checkPendingMorningTriage();
  triagePollHandle = setInterval(() => {
    void checkPendingMorningTriage();
  }, MORNING_TRIAGE_CHECK_INTERVAL_MS);
}

export function stopNudgeBus(): void {
  if (!started) return;
  started = false;

  window.removeEventListener("lumotia:start-timer", onTimerStart);
  window.removeEventListener("lumotia:focus-timer-complete", resetTimerState);
  window.removeEventListener("lumotia:focus-timer-cancelled", resetTimerState);
  window.removeEventListener("lumotia:microstep-generated", onMicroStepGenerated);
  window.removeEventListener("lumotia:step-completed", onStepOrTaskCompleted);
  window.removeEventListener("lumotia:task-completed", onStepOrTaskCompleted);
  window.removeEventListener("focus", onFocus);
  window.removeEventListener("blur", onBlur);

  if (blurCheckHandle !== null) {
    clearInterval(blurCheckHandle);
    blurCheckHandle = null;
  }
  if (triagePollHandle !== null) {
    clearInterval(triagePollHandle);
    triagePollHandle = null;
  }
  for (const handle of microStepTimers.values()) clearTimeout(handle);
  microStepTimers.clear();

  resetTimerState();
  blurredAt = null;
  triageNudgeFiredOnKey = null;
  permissionRequested = false;
}
