<script lang="ts">
  // @ts-nocheck
  // Phase 5 morning triage. Surfaces a calm "pick up to three for today"
  // modal on the first launch-of-day once the user-set trigger time has
  // passed. Evidence-based "rule of 3" per Life Skills Advocate / Aspire
  // Therapy ADHD routine literature: externalise the daily choice and
  // cap it at 3 to protect working memory (Sweller cognitive-load theory,
  // Barkley's point-of-performance principle).
  //
  // Copy audit: no "overdue", no "failed", no subtractive framing (RSD).
  //
  // Triggering: runs a lightweight check on mount and when the page
  // regains focus. Only one modal per calendar day regardless of how
  // many times the app is restarted.

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

  interface TriageTask {
    id: string;
    text: string;
    bucket: string;
    done: boolean;
    createdAt: string;
  }

  let open = $state(false);
  let loading = $state(false);
  let tasks = $state<TriageTask[]>([]);
  let selected = $state<Set<string>>(new Set());
  let tooManyFlash = $state(false);
  let applying = $state(false);
  let focusHandler: (() => void) | null = null;
  // Phase 10a a11y G4: focus-trap state. The modal must keep keyboard
  // focus inside its bounds while open and restore focus to the
  // invoking element on close.
  let dialogEl = $state<HTMLDivElement | null>(null);
  let invokerEl: HTMLElement | null = null;

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

  // Parse a HH:MM string into minutes-since-midnight. Falls back to
  // 08:00 on malformed input rather than throwing — rituals must never
  // break the app shell.
  function parseTriggerMinutes(hhmm: string | undefined): number {
    if (typeof hhmm !== 'string') return 8 * 60;
    const match = /^(\d{1,2}):(\d{2})$/.exec(hhmm.trim());
    if (!match) return 8 * 60;
    const h = Math.max(0, Math.min(23, parseInt(match[1], 10)));
    const m = Math.max(0, Math.min(59, parseInt(match[2], 10)));
    return h * 60 + m;
  }

  function currentMinutes(): number {
    const d = new Date();
    return d.getHours() * 60 + d.getMinutes();
  }

  function dispatchTriageFinished(mode: 'empty' | 'skipped' | 'picked') {
    if (typeof window === 'undefined') return;
    window.dispatchEvent(new CustomEvent('lumotia:morning-triage-finished', {
      detail: { date: todayKey(), mode },
    }));
  }

  function isBeforeToday(createdAt: string): boolean {
    // Task `createdAt` comes from SQLite as ISO-8601 UTC. Compare the
    // local-time date portion so a task made last night locally counts
    // as "yesterday" regardless of the UTC offset.
    const d = new Date(createdAt);
    if (Number.isNaN(d.getTime())) return false;
    const local = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
    return local < todayKey();
  }

  async function maybeShow() {
    if (open || applying) return;
    if (!settings.ritualsMorning) return;
    if (!hasTauriRuntime()) return;
    if (currentMinutes() < parseTriggerMinutes(settings.ritualsMorningTime)) return;

    let lastShown: string | null = null;
    try {
      lastShown = await invoke<string | null>('get_last_morning_triage');
    } catch {
      // Non-fatal: if we can't read the sentinel, treat as never-shown.
    }
    if (lastShown === todayKey()) return;

    loading = true;
    try {
      const all = await invoke<TriageTask[]>('list_tasks_cmd');
      tasks = all.filter(
        (t) => !t.done && t.bucket !== 'today' && isBeforeToday(t.createdAt),
      );
    } catch {
      tasks = [];
    } finally {
      loading = false;
    }

    if (tasks.length === 0) {
      // Nothing to triage — record the shown sentinel anyway so we
      // don't re-check the DB every focus event today.
      try { await invoke('mark_morning_triage_shown', { date: todayKey() }); } catch {}
      dispatchTriageFinished('empty');
      return;
    }

    selected = new Set();
    open = true;
  }

  function toggle(taskId: string) {
    if (selected.has(taskId)) {
      const next = new Set(selected);
      next.delete(taskId);
      selected = next;
      return;
    }
    if (selected.size >= 3) {
      tooManyFlash = true;
      setTimeout(() => { tooManyFlash = false; }, 1800);
      return;
    }
    const next = new Set(selected);
    next.add(taskId);
    selected = next;
  }

  async function skipForToday() {
    applying = true;
    try {
      await invoke('mark_morning_triage_shown', { date: todayKey() });
    } catch (err) {
      toasts.warn('Could not save triage state', String(err));
    } finally {
      applying = false;
      open = false;
      dispatchTriageFinished('skipped');
    }
  }

  async function startTheDay() {
    if (selected.size === 0) return;
    applying = true;
    try {
      for (const id of selected) {
        try {
          await invoke('update_task_cmd', {
            id,
            patch: { bucket: 'today' },
          });
        } catch (err) {
          // Continue the loop — surface a single toast at the end rather
          // than one per failure, so the user isn't drowned in errors.
          console.warn('Triage: failed to move task', id, err);
        }
      }
      await invoke('mark_morning_triage_shown', { date: todayKey() });
    } catch (err) {
      toasts.warn('Could not save triage state', String(err));
    } finally {
      applying = false;
      open = false;
      dispatchTriageFinished('picked');
    }
  }

  // Phase 10a a11y G4: collect every focusable element inside the dialog
  // for the focus trap. We re-query on every Tab so dynamic content
  // (loading → tasks → buttons) stays in sync.
  function focusableInDialog(): HTMLElement[] {
    if (!dialogEl) return [];
    const sel = [
      'a[href]',
      'button:not([disabled])',
      'input:not([disabled])',
      'select:not([disabled])',
      'textarea:not([disabled])',
      '[tabindex]:not([tabindex="-1"])',
    ].join(',');
    return Array.from(dialogEl.querySelectorAll<HTMLElement>(sel)).filter(
      (el) => !el.hasAttribute('aria-hidden') && el.offsetParent !== null,
    );
  }

  function handleKeydown(e: KeyboardEvent) {
    if (!open) return;
    if (e.key === 'Escape') {
      e.preventDefault();
      skipForToday();
      return;
    }
    if (e.key === 'Tab') {
      const focusables = focusableInDialog();
      if (focusables.length === 0) {
        // Nothing focusable inside — keep focus on the dialog container.
        e.preventDefault();
        dialogEl?.focus();
        return;
      }
      const first = focusables[0];
      const last = focusables[focusables.length - 1];
      const active = document.activeElement as HTMLElement | null;
      if (e.shiftKey) {
        if (active === first || !dialogEl?.contains(active)) {
          e.preventDefault();
          last.focus();
        }
      } else {
        if (active === last || !dialogEl?.contains(active)) {
          e.preventDefault();
          first.focus();
        }
      }
    }
  }

  // Phase 10a a11y G4: when the dialog opens, remember the invoker so
  // we can return focus on close, and move focus into the dialog.
  $effect(() => {
    if (open) {
      invokerEl = (document.activeElement as HTMLElement) ?? null;
      // Wait one tick for the dialog to mount before focusing.
      queueMicrotask(() => {
        const focusables = focusableInDialog();
        (focusables[0] ?? dialogEl)?.focus();
      });
    } else if (invokerEl) {
      // Restore focus to the element that opened the modal.
      try { invokerEl.focus(); } catch {}
      invokerEl = null;
    }
  });

  onMount(() => {
    maybeShow();
    focusHandler = () => maybeShow();
    window.addEventListener('focus', focusHandler);
  });

  onDestroy(() => {
    if (focusHandler) window.removeEventListener('focus', focusHandler);
  });
</script>

<svelte:window onkeydown={handleKeydown} />

{#if open}
  <!--
    Overlay sits on `--color-overlay-dim` (defined in app.css @theme), warm
    deep-neutral derived from --color-bg. No backdrop-blur: the brand opposes
    glassmorphism as a default. z-[60] sits above the .grain overlay (z-50).
  -->
  <div
    bind:this={dialogEl}
    class="fixed inset-0 z-[60] flex items-center justify-center animate-fade-in"
    style="background: var(--color-overlay-dim)"
    role="dialog"
    aria-modal="true"
    aria-labelledby="triage-title"
    tabindex="-1"
  >
    <div class="bg-bg-elevated border border-border rounded-2xl shadow-2xl max-w-[480px] w-[90vw] max-h-[80vh] flex flex-col">
      <div class="px-6 pt-6 pb-3">
        <h2 id="triage-title" class="font-display text-[22px] italic text-text">Pick up to three for today</h2>
        <p class="text-[12px] text-text-secondary mt-1">
          Yesterday's open items. The rest can wait.
        </p>
      </div>

      <div class="flex-1 overflow-y-auto px-6 pb-3 min-h-0">
        {#if loading}
          <p class="text-[12px] text-text-secondary py-6 text-center">Loading your list…</p>
        {:else}
          <ul class="flex flex-col gap-1.5">
            {#each tasks as task (task.id)}
              {@const picked = selected.has(task.id)}
              <li>
                <button
                  type="button"
                  class="w-full text-left flex items-start gap-3 px-3 py-2 rounded-lg border transition-colors
                    {picked
                      ? 'bg-accent/10 border-accent text-text'
                      : 'bg-bg-input border-border-subtle text-text-secondary hover:border-border'}"
                  onclick={() => toggle(task.id)}
                  aria-pressed={picked}
                >
                  <span
                    class="mt-0.5 w-4 h-4 rounded-sm border flex items-center justify-center flex-shrink-0
                      {picked ? 'bg-accent border-accent text-white' : 'border-border'}"
                    aria-hidden="true"
                  >
                    {#if picked}
                      <svg viewBox="0 0 24 24" class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="3">
                        <path d="M5 12l5 5L20 7" stroke-linecap="round" stroke-linejoin="round" />
                      </svg>
                    {/if}
                  </span>
                  <span class="text-[13px] leading-snug">{task.text}</span>
                </button>
              </li>
            {/each}
          </ul>
        {/if}
      </div>

      <div class="px-6 pb-5 pt-2">
        <p
          class="text-[12px] mb-3 min-h-[16px] transition-colors
            {tooManyFlash ? 'text-warning' : 'text-text-tertiary'}"
          aria-live="polite"
        >
          {#if tooManyFlash}
            Just three for today. Unpick one to swap.
          {:else if selected.size > 0}
            {selected.size} picked · room for {3 - selected.size} more
          {:else}
            Pick 1, 2, or 3.
          {/if}
        </p>
        <div class="flex items-center justify-between gap-3">
          <button
            type="button"
            class="px-3 py-2 text-[12px] text-text-secondary hover:text-text"
            onclick={skipForToday}
            disabled={applying}
          >
            Skip for today
          </button>
          <button
            type="button"
            class="px-4 py-2 rounded-lg text-[12px] font-medium transition-colors
              {selected.size >= 1 && !applying
                ? 'bg-accent text-white hover:bg-accent-hover'
                : 'bg-bg-input text-text-tertiary cursor-not-allowed'}"
            onclick={startTheDay}
            disabled={selected.size === 0 || applying}
          >
            {applying ? 'Saving…' : 'Start the day'}
          </button>
        </div>
      </div>
    </div>
  </div>
{/if}
