// Keyboard capture: translate browser KeyboardEvent into protobuf
// Message{key_event: ...}.
//
// We use Legacy mode (KeyboardMode = 0). Critically, Translate mode at the
// host explicitly drops Unicode and ControlKey payloads
// (src/server/input_service.rs:2022-2024 has `// Do not handle unicode`),
// so a Translate-mode submission with our payload shape silently no-ops.
// Legacy mode's legacy_keyboard_mode() at :1862 dispatches all four
// payload types (ControlKey/Chr/Unicode/Seq).
//
// Per-keystroke wire shape for Legacy mode:
//   - Special keys (Enter, F1, Ctrl, etc.):
//       { control_key: ControlKey, down, modifiers, mode: Legacy }
//     Sent on both keydown and keyup (host's process_control_key honors `down`).
//   - Printable chars:
//       { unicode: codepoint, down: true, modifiers, mode: Legacy }
//     Sent ONLY on keydown — host's process_unicode does a single key_click
//     so a second event on keyup would re-type the char.

import { hbb } from "../proto/generated.js";
import type { Session } from "../transport/session.js";
import type { CanvasView } from "../ui/canvas.js";
import { pushClipboardText } from "./clipboard.js";

/**
 * KeyboardEvent.code → ControlKey enum value. Covers the named keys; any
 * not in this map is treated as a printable Unicode key (using event.key).
 */
const CODE_TO_CONTROL: Record<string, hbb.ControlKey> = {
  // Modifiers
  AltLeft: hbb.ControlKey.Alt,
  AltRight: hbb.ControlKey.RAlt,
  ControlLeft: hbb.ControlKey.Control,
  ControlRight: hbb.ControlKey.RControl,
  ShiftLeft: hbb.ControlKey.Shift,
  ShiftRight: hbb.ControlKey.RShift,
  MetaLeft: hbb.ControlKey.Meta,
  MetaRight: hbb.ControlKey.RWin,
  OSLeft: hbb.ControlKey.Meta,
  OSRight: hbb.ControlKey.RWin,
  CapsLock: hbb.ControlKey.CapsLock,
  // Editing / navigation
  Backspace: hbb.ControlKey.Backspace,
  Delete: hbb.ControlKey.Delete,
  Tab: hbb.ControlKey.Tab,
  Enter: hbb.ControlKey.Return,
  NumpadEnter: hbb.ControlKey.NumpadEnter,
  Escape: hbb.ControlKey.Escape,
  Space: hbb.ControlKey.Space,
  Insert: hbb.ControlKey.Insert,
  Home: hbb.ControlKey.Home,
  End: hbb.ControlKey.End,
  PageUp: hbb.ControlKey.PageUp,
  PageDown: hbb.ControlKey.PageDown,
  ArrowUp: hbb.ControlKey.UpArrow,
  ArrowDown: hbb.ControlKey.DownArrow,
  ArrowLeft: hbb.ControlKey.LeftArrow,
  ArrowRight: hbb.ControlKey.RightArrow,
  // Function keys
  F1: hbb.ControlKey.F1, F2: hbb.ControlKey.F2, F3: hbb.ControlKey.F3,
  F4: hbb.ControlKey.F4, F5: hbb.ControlKey.F5, F6: hbb.ControlKey.F6,
  F7: hbb.ControlKey.F7, F8: hbb.ControlKey.F8, F9: hbb.ControlKey.F9,
  F10: hbb.ControlKey.F10, F11: hbb.ControlKey.F11, F12: hbb.ControlKey.F12,
  // System
  PrintScreen: hbb.ControlKey.Snapshot,
  ScrollLock: hbb.ControlKey.Scroll,
  Pause: hbb.ControlKey.Pause,
  NumLock: hbb.ControlKey.NumLock,
  ContextMenu: hbb.ControlKey.Apps,
  // Numpad
  Numpad0: hbb.ControlKey.Numpad0, Numpad1: hbb.ControlKey.Numpad1,
  Numpad2: hbb.ControlKey.Numpad2, Numpad3: hbb.ControlKey.Numpad3,
  Numpad4: hbb.ControlKey.Numpad4, Numpad5: hbb.ControlKey.Numpad5,
  Numpad6: hbb.ControlKey.Numpad6, Numpad7: hbb.ControlKey.Numpad7,
  Numpad8: hbb.ControlKey.Numpad8, Numpad9: hbb.ControlKey.Numpad9,
  NumpadAdd: hbb.ControlKey.Add,
  NumpadSubtract: hbb.ControlKey.Subtract,
  NumpadMultiply: hbb.ControlKey.Multiply,
  NumpadDivide: hbb.ControlKey.Divide,
  NumpadDecimal: hbb.ControlKey.Decimal,
  NumpadEqual: hbb.ControlKey.Equals,
};

function modifierList(e: KeyboardEvent): hbb.ControlKey[] {
  const mods: hbb.ControlKey[] = [];
  if (e.altKey) mods.push(hbb.ControlKey.Alt);
  if (e.ctrlKey) mods.push(hbb.ControlKey.Control);
  if (e.shiftKey) mods.push(hbb.ControlKey.Shift);
  if (e.metaKey) mods.push(hbb.ControlKey.Meta);
  return mods;
}

export function bindKeyboard(_canvas: CanvasView, session: Session): () => void {
  // Window-level listeners — canvas focus is unreliable across browsers
  // and can be lost without warning. Forwarding everything that arrives
  // at the window is closer to the desktop client's behavior anyway.
  // The trade-off: browser-level shortcuts (Cmd-W, Cmd-T, etc.) need to
  // be allowlisted so they keep working.

  const sendKey = (e: KeyboardEvent, down: boolean): void => {
    // Don't intercept keystrokes when the user is typing into a real form
    // input (the password prompt, dev tools fields rendered by extensions,
    // etc.). Once we're in session, there are no real inputs in our UI.
    const tgt = e.target as HTMLElement | null;
    if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) {
      return;
    }

    const ck = CODE_TO_CONTROL[e.code];
    const hotkeyModifier = e.ctrlKey || e.altKey || e.metaKey;
    let payload: hbb.IKeyEvent;
    if (ck !== undefined) {
      // Special key — fire on both keydown and keyup.
      payload = { control_key: ck, down };
    } else if (e.key.length === 1) {
      const codepoint = e.key.codePointAt(0)!;
      if (hotkeyModifier) {
        // Printable + Ctrl/Alt/Cmd → must use Chr (scancode-style) so the
        // host's process_chr respects the held modifiers. Unicode payloads
        // go through process_unicode which ignores modifiers (just types
        // the char), breaking Ctrl+C, Ctrl+V, etc. Fire on both edges.
        payload = { chr: codepoint, down };
      } else {
        // Plain typing — Unicode keydown only (host does a single
        // key_click; a keyup would double-type the char).
        if (!down) return;
        payload = { unicode: codepoint, down: true };
      }
    } else {
      // Unmapped key — silently drop. Rare in normal use.
      return;
    }
    payload.modifiers = modifierList(e);
    payload.mode = hbb.KeyboardMode.Legacy;

    // Cmd+V / Ctrl+V → read the local clipboard and push it to the host
    // BEFORE the V keystroke so the host's paste hotkey lands on the
    // freshly-synced text. Non-blocking — the keystroke goes through
    // either way; if the clipboard read fails (permission, empty), the
    // host pastes whatever it had.
    if (down && hotkeyModifier && e.code === "KeyV") {
      pushClipboardText(session).catch(() => { /* swallowed */ });
    }

    session.send(hbb.Message.create({ key_event: hbb.KeyEvent.create(payload) }))
      .catch(() => { /* relay closed */ });

    // Browser-shortcut allowlist: don't preventDefault on combos the user
    // needs to keep using to manage their browser tab/window.
    const allowBrowserDefault =
      (e.metaKey || e.ctrlKey) && ["KeyT", "KeyN", "KeyW", "KeyR", "Tab"].includes(e.code);
    if (!allowBrowserDefault) {
      e.preventDefault();
    }
  };

  const onDown = (e: KeyboardEvent): void => sendKey(e, true);
  const onUp = (e: KeyboardEvent): void => sendKey(e, false);

  window.addEventListener("keydown", onDown);
  window.addEventListener("keyup", onUp);

  return (): void => {
    window.removeEventListener("keydown", onDown);
    window.removeEventListener("keyup", onUp);
  };
}

/** Send Ctrl+Alt+Del — the host has a special handler for this combo at
 *  src/server/input_service.rs:1788, which only fires on Windows hosts.
 *  On macOS/Linux this is a no-op server-side (there's no equivalent SAS).
 *  We use Legacy mode because Translate mode drops ControlKey payloads. */
export function sendCtrlAltDel(session: Session): void {
  session.send(hbb.Message.create({
    key_event: hbb.KeyEvent.create({
      control_key: hbb.ControlKey.CtrlAltDel,
      down: true,
      mode: hbb.KeyboardMode.Legacy,
    }),
  })).catch(() => { /* ignore */ });
}
