<script lang="ts">
  import { settings, saveSettings } from "$lib/stores/page.svelte.js";
  import { osInfo } from "$lib/utils/osInfo";
  import { validateHotkey, type HotkeyOs } from "$lib/utils/hotkeyValidity";

  let recording = $state(false);
  let captured = $state(false);
  let rejectionReason = $state<string | null>(null);

  function currentOs(): HotkeyOs {
    const info = osInfo();
    if (!info) return "unknown";
    const name = (info.os || "").toLowerCase();
    if (name.startsWith("mac")) return "macos";
    if (name.startsWith("win")) return "windows";
    if (name.startsWith("linux")) return "linux";
    return "unknown";
  }

  // Linux webkit2gtk fires e.key === "Super" for the Windows/Super key;
  // Firefox/older Chrome may fire "OS" or "Hyper". All of these should be
  // treated as modifiers, not as the trigger key.
  const modifierKeys = new Set([
    "Control", "Shift", "Alt", "Meta",
    "Super", "OS", "Hyper",
  ]);

  // Map KeyboardEvent.code (physical key, layout-independent, shift-independent)
  // to the unshifted name the lumotia-hotkey evdev parser understands. Browser
  // e.key returns "+" for Shift+Equal — parser wants "=". Same for | → \, etc.
  const codeToParserKey: Record<string, string> = {
    Equal: "=", Minus: "-",
    BracketLeft: "[", BracketRight: "]",
    Backslash: "\\", IntlBackslash: "\\",
    Semicolon: ";", Quote: "'",
    Comma: ",", Period: ".", Slash: "/",
    Backquote: "`",
    Space: "Space",
    // Numpad keys intentionally NOT mapped here. They are physically distinct
    // evdev codes from their main-keyboard counterparts; mapping them would
    // silently bind the wrong key. Let resolveTriggerKey fall through so the
    // backend parser rejects and the toast surfaces the failure.
  };

  function resolveTriggerKey(e: KeyboardEvent): string {
    // Letters and digits: e.key is reliable (always A-Z / 0-9 for those codes).
    if (/^Key[A-Z]$/.test(e.code)) return e.code.slice(3); // KeyA → A
    if (/^Digit[0-9]$/.test(e.code)) return e.code.slice(5); // Digit1 → 1
    if (/^Numpad[0-9]$/.test(e.code)) return e.code.slice(6); // Numpad1 → 1
    if (/^F\d{1,2}$/.test(e.code)) return e.code; // F1..F12

    // Shifted punctuation / numpad operators: use the physical-key mapping.
    if (codeToParserKey[e.code]) return codeToParserKey[e.code];

    // Named keys (Escape, Enter, Tab, arrows, etc.): e.key is the right choice
    // — but only if it's multi-char (single chars here are usually dead-keys
    // or IME output we shouldn't save).
    if (e.key.length > 1) return e.key;

    // Fallback: uppercase single char. Better than nothing.
    return e.key.toUpperCase();
  }

  // Manual modifier-state tracker. webkit2gtk on KDE Wayland does not
  // populate e.metaKey when the compositor intercepts Super — the Super
  // keydown arrives as e.key === "Super" with e.metaKey still false on the
  // next event. We track modifier state from raw keydown/keyup and OR it
  // with the webkit booleans when building the combo so Super+letter can
  // be recorded.
  let manualMods = {
    ctrl: false,
    shift: false,
    alt: false,
    meta: false,
  };

  function updateManualMods(e: KeyboardEvent, down: boolean) {
    switch (e.key) {
      case "Control":
        manualMods.ctrl = down;
        break;
      case "Shift":
        manualMods.shift = down;
        break;
      case "Alt":
        manualMods.alt = down;
        break;
      case "Meta":
      case "Super":
      case "OS":
      case "Hyper":
        manualMods.meta = down;
        break;
    }
  }

  function handleKeyDown(e: KeyboardEvent) {
    // Capture-phase listener; guard still belts-and-braces.
    if (!recording) return;
    updateManualMods(e, true);
    e.preventDefault();
    e.stopPropagation();

    if (e.key === "Escape") {
      recording = false;
      return;
    }

    // Wait for a non-modifier key
    if (modifierKeys.has(e.key)) return;

    const parts: string[] = [];
    if (e.ctrlKey || manualMods.ctrl) parts.push("Ctrl");
    if (e.shiftKey || manualMods.shift) parts.push("Shift");
    if (e.altKey || manualMods.alt) parts.push("Alt");
    if (e.metaKey || manualMods.meta) parts.push("Super");

    // Must have at least one modifier
    if (parts.length === 0) {
      recording = false;
      return;
    }

    parts.push(resolveTriggerKey(e));
    const candidate = parts.join("+");

    // Per-OS capability matrix — reject combos the backend can't
    // reliably bind before they land in settings.globalHotkey (brief
    // item #5). Leaves the previous, known-good hotkey intact.
    const check = validateHotkey(candidate, currentOs());
    if (!check.valid) {
      rejectionReason = check.reason;
      recording = false;
      return;
    }

    rejectionReason = null;
    settings.globalHotkey = candidate;
    saveSettings();
    recording = false;
    captured = true;
    setTimeout(() => { captured = false; }, 1500);
  }

  function handleKeyUp(e: KeyboardEvent) {
    if (!recording) return;
    updateManualMods(e, false);
  }

  // Register capture-phase keydown + keyup listeners only while recording.
  // Document-level capture so no descendant (or the parent layout's
  // svelte:window keydown) can swallow the event first.
  $effect(() => {
    if (!recording) return;
    manualMods = { ctrl: false, shift: false, alt: false, meta: false };
    document.addEventListener("keydown", handleKeyDown, { capture: true });
    document.addEventListener("keyup", handleKeyUp, { capture: true });
    return () => {
      document.removeEventListener("keydown", handleKeyDown, { capture: true });
      document.removeEventListener("keyup", handleKeyUp, { capture: true });
    };
  });

  function startRecording() {
    recording = true;
    captured = false;
    rejectionReason = null;
  }

  let chips = $derived(settings.globalHotkey.split("+"));
  let ariaLabel = $derived(
    recording
      ? "Press a new global hotkey. Press Escape to cancel."
      : `Record hotkey. Current shortcut: ${settings.globalHotkey}`,
  );
  let srStatus = $derived(
    recording
      ? "Listening for a new shortcut. Press Escape to cancel."
      : captured
        ? `Shortcut saved as ${settings.globalHotkey}.`
        : `Current shortcut: ${settings.globalHotkey}. Click to change.`,
  );
</script>

<div class="inline-flex flex-col items-start gap-1.5">
  <button
    type="button"
    class="inline-flex items-center gap-1.5 rounded-xl px-4 py-2.5
      {recording
        ? 'bg-bg-input border-accent shadow-[0_0_0_3px_rgba(214,132,80,0.1)]'
        : captured
          ? 'bg-success/10 border-success/40'
          : 'bg-bg-input border-border hover:border-border'}
      border transition-colors"
    onclick={startRecording}
    aria-label={ariaLabel}
    aria-pressed={recording}
  >
    <span class="sr-only" aria-live="polite">{srStatus}</span>
    {#if recording}
      <span class="text-[12px] text-text-secondary italic">Hold Ctrl/Alt/Super + key. Esc to cancel.</span>
    {:else}
      {#each chips as chip, i}
        {#if i > 0}
          <span class="text-[12px] text-text-secondary" aria-hidden="true">+</span>
        {/if}
        <span class="px-2 py-0.5 rounded-md bg-bg-elevated border border-border text-[12px] font-medium text-text
          shadow-[inset_0_1px_2px_rgba(0,0,0,0.3)]">{chip}</span>
      {/each}
    {/if}
  </button>
  {#if rejectionReason}
    <span class="text-[12px] text-warning" role="alert">
      Couldn't bind that combo. {rejectionReason}.
    </span>
  {/if}
</div>
