// Focus timer store. Single active timer at a time — the "just-start"
// 2/5/10/15-minute countdown paired with micro-steps. Exposes:
//   - focusTimer.active: whether a timer is currently running
//   - focusTimer.progress: 0..1 fraction of elapsed time
//   - focusTimer.remainingMs: milliseconds until completion
//   - focusTimer.label / focusTimer.taskId: what this timer is for
//   - start(seconds, opts) / cancel() / extend(seconds)
//
// Survives window close + reopen via localStorage, because a timer
// that loses its clock when the user alt-tabs is a timer that nobody
// trusts. On rehydrate after expiry, fires completion then clears —
// so closing the window mid-timer still gets you the "done" signal
// on next launch.

import { migrateLocalStorageKey } from "$lib/utils/localStorageMigration";

const STORAGE_KEY = "lumotia.focusTimer.v1";
const TICK_INTERVAL_MS = 250;

// One-shot key rename from the magnotia era. Idempotent.
migrateLocalStorageKey("magnotia.focusTimer.v1", STORAGE_KEY);

export type FocusTimerPersisted = {
  startedAt: number;
  durationMs: number;
  taskId: string | null;
  label: string | null;
};

function readPersisted(): FocusTimerPersisted | null {
  if (typeof window === "undefined") return null;
  try {
    const raw = window.localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (
      typeof parsed?.startedAt !== "number" ||
      typeof parsed?.durationMs !== "number"
    ) return null;
    return {
      startedAt: parsed.startedAt,
      durationMs: parsed.durationMs,
      taskId: parsed.taskId ?? null,
      label: parsed.label ?? null,
    };
  } catch {
    return null;
  }
}

function writePersisted(state: FocusTimerPersisted | null): void {
  if (typeof window === "undefined") return;
  try {
    if (state === null) window.localStorage.removeItem(STORAGE_KEY);
    else window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
  } catch { /* storage may be disabled; non-fatal */ }
}

function createFocusTimerStore() {
  let startedAt = $state<number | null>(null);
  let durationMs = $state<number>(0);
  let taskId = $state<string | null>(null);
  let label = $state<string | null>(null);
  let now = $state<number>(Date.now());
  let completionFlashUntil = $state<number>(0);

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

  const active = $derived(startedAt !== null);
  const elapsedMs = $derived(startedAt === null ? 0 : Math.max(0, now - startedAt));
  const remainingMs = $derived(Math.max(0, durationMs - elapsedMs));
  const progress = $derived(durationMs === 0 ? 0 : Math.min(1, elapsedMs / durationMs));
  const finished = $derived(active && remainingMs === 0);
  const showingCompletionFlash = $derived(now < completionFlashUntil);

  // Internal completion + flash bookkeeping. We track whether we have
  // already fired the completion chime for the current timer so a
  // second tick does not re-fire it. Reset whenever a new timer starts.
  let completionFired = false;

  function tick() {
    now = Date.now();
    // Fire completion once, the first tick after we cross remaining=0.
    if (startedAt !== null && !completionFired && now - startedAt >= durationMs) {
      completionFired = true;
      completionFlashUntil = now + 3000;
      fireCompletion();
    }
    // After the 3 s flash window, clear everything and stop ticking.
    if (completionFlashUntil > 0 && now >= completionFlashUntil) {
      clear();
    }
  }

  function startTick() {
    if (interval !== null) return;
    interval = setInterval(tick, TICK_INTERVAL_MS);
  }

  function stopTick() {
    if (interval !== null) {
      clearInterval(interval);
      interval = null;
    }
  }

  function clear() {
    // Broadcast cancellation so downstream listeners (Phase 6 nudge bus)
    // can reset their inactivity-while-timer state. Only fires when a
    // timer was actually active — dismissing the completion flash
    // after natural completion doesn't re-fire the event.
    const wasActive = startedAt !== null && !completionFired;
    startedAt = null;
    durationMs = 0;
    taskId = null;
    label = null;
    completionFlashUntil = 0;
    completionFired = false;
    writePersisted(null);
    stopTick();
    if (wasActive && typeof window !== "undefined") {
      window.dispatchEvent(new CustomEvent("lumotia:focus-timer-cancelled"));
    }
  }

  function start(seconds: number, opts?: { taskId?: string | null; label?: string | null }): void {
    if (!Number.isFinite(seconds) || seconds <= 0) return;
    now = Date.now();
    startedAt = now;
    durationMs = Math.floor(seconds * 1000);
    taskId = opts?.taskId ?? null;
    label = opts?.label ?? null;
    completionFlashUntil = 0;
    completionFired = false;
    writePersisted({ startedAt, durationMs, taskId, label });
    startTick();
  }

  function cancel(): void {
    clear();
  }

  function extend(seconds: number): void {
    if (startedAt === null) return;
    if (!Number.isFinite(seconds) || seconds <= 0) return;
    durationMs += Math.floor(seconds * 1000);
    writePersisted({ startedAt, durationMs, taskId, label });
  }

  function dismissCompletionFlash(): void {
    completionFlashUntil = 0;
    clear();
  }

  function fireCompletion(): void {
    if (typeof window === "undefined") return;
    // Gentle chime — WebAudio so we do not ship a bundled asset.
    // A 440 Hz fall into 330 Hz over 220 ms, low volume, no sustain.
    try {
      type AudioCtx = typeof AudioContext;
      const win = window as unknown as { AudioContext?: AudioCtx; webkitAudioContext?: AudioCtx };
      const Ctx = win.AudioContext ?? win.webkitAudioContext;
      if (!Ctx) return;
      const ctx = new Ctx();
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.type = "sine";
      osc.frequency.setValueAtTime(440, ctx.currentTime);
      osc.frequency.exponentialRampToValueAtTime(330, ctx.currentTime + 0.22);
      gain.gain.setValueAtTime(0.0001, ctx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.15, ctx.currentTime + 0.02);
      gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.28);
      osc.connect(gain);
      gain.connect(ctx.destination);
      osc.start();
      osc.stop(ctx.currentTime + 0.3);
      osc.onended = () => ctx.close().catch(() => {});
    } catch { /* audio is a nicety; never fatal */ }

    window.dispatchEvent(new CustomEvent("lumotia:focus-timer-complete", {
      detail: { taskId, label },
    }));
  }

  // Rehydrate on first touch. If the persisted timer has already
  // expired, fire completion then clear so the user still gets the
  // "done" signal they missed while the window was closed.
  function rehydrate(): void {
    const persisted = readPersisted();
    if (!persisted) return;
    const age = Date.now() - persisted.startedAt;
    if (age >= persisted.durationMs) {
      // Already expired while the window was closed. Fire a completion
      // event so downstream listeners (nudges, UI flourishes) can react.
      taskId = persisted.taskId;
      label = persisted.label;
      durationMs = persisted.durationMs;
      startedAt = persisted.startedAt;
      now = persisted.startedAt + persisted.durationMs;
      // Flash briefly so the user knows it happened.
      completionFlashUntil = Date.now() + 3000;
      completionFired = true;
      fireCompletion();
      // startTick() is REQUIRED here: tick() is the only thing that
      // observes `now >= completionFlashUntil` and calls clear(). Without
      // it, the completion flash would stay visible until the user
      // interacts with the app. stopTick() runs via clear() once the
      // flash window elapses.
      startTick();
      return;
    }
    startedAt = persisted.startedAt;
    durationMs = persisted.durationMs;
    taskId = persisted.taskId;
    label = persisted.label;
    now = Date.now();
    startTick();
  }

  // Exposed as frozen object. Getters so derivations stay reactive.
  return {
    get active() { return active; },
    get progress() { return progress; },
    get elapsedMs() { return elapsedMs; },
    get remainingMs() { return remainingMs; },
    get durationMs() { return durationMs; },
    get taskId() { return taskId; },
    get label() { return label; },
    get showingCompletionFlash() { return showingCompletionFlash; },
    start,
    cancel,
    extend,
    rehydrate,
    dismissCompletionFlash,
  };
}

export const focusTimer = createFocusTimerStore();

// Preset durations surfaced in the UI. 2 / 5 / 10 / 15 minutes match
// the brief's guidance for the "just-start" timer and cover common
// Pomodoro / shorter-focus preferences.
export const FOCUS_TIMER_PRESETS_SECONDS: ReadonlyArray<{ label: string; seconds: number }> = [
  { label: "2 min", seconds: 120 },
  { label: "5 min", seconds: 300 },
  { label: "10 min", seconds: 600 },
  { label: "15 min", seconds: 900 },
];
