<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { Terminal } from '@xterm/xterm';
  import { CanvasAddon } from '@xterm/addon-canvas';
  import { FitAddon } from '@xterm/addon-fit';
  import { ImageAddon } from '@xterm/addon-image';
  import { appRpc } from './rpc.ts';
  import { fontStore } from './font-store.svelte.ts';
  import { themeStore } from './theme-store.svelte.ts';
  import { getXtermTheme } from './themes.ts';

  interface Props {
    sessionId: string;
    /** Working directory to open the shell in. */
    cwd?: string;
  }

  let { sessionId, cwd }: Props = $props();

  let termEl: HTMLDivElement;
  let term: Terminal;
  let fitAddon: FitAddon;
  let unsubFont: (() => void) | null = null;
  let ro: ResizeObserver | null = null;
  let destroyed = false;
  // Fix #5: Store listener cleanup functions to prevent leaks
  let listenerCleanups: Array<() => void> = [];

  /** Decode a base64 string from the daemon into a Uint8Array. */
  function decodeBase64(b64: string): Uint8Array {
    const binary = atob(b64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
    return bytes;
  }

  onMount(() => {
    const currentTheme = themeStore.currentTheme;
    const termFamily = fontStore.termFontFamily || 'JetBrains Mono, Fira Code, monospace';
    const termSize   = fontStore.termFontSize   || 13;

    term = new Terminal({
      theme: getXtermTheme(currentTheme),
      fontFamily: termFamily,
      fontSize: termSize,
      cursorBlink: true,
      allowProposedApi: true,
      scrollback: 5000,
    });

    fitAddon = new FitAddon();
    term.loadAddon(fitAddon);
    // NOTE: CanvasAddon and ImageAddon MUST be loaded AFTER term.open()
    // because they access _linkifier2 which is created during open()

    const openAndLoadAddons = () => {
      term.open(termEl);
      // Now safe to load addons that depend on _linkifier2
      try { term.loadAddon(new CanvasAddon()); } catch (e) {
        console.warn('[Terminal] CanvasAddon failed:', (e as Error).message);
      }
      try {
        term.loadAddon(new ImageAddon({
          enableSizeReports: true,
          sixelSupport: true,
          sixelScrolling: true,
          sixelPaletteLimit: 4096,
          showPlaceholder: true,
        }));
      } catch (e) {
        console.warn('[Terminal] ImageAddon failed:', (e as Error).message);
      }
      fitAddon.fit();
    };

    // Defer if container isn't visible yet (e.g., behind splash)
    if (termEl.offsetParent !== null) {
      openAndLoadAddons();
    } else {
      const observer = new IntersectionObserver((entries) => {
        if (entries[0]?.isIntersecting) {
          observer.disconnect();
          openAndLoadAddons();
        }
      });
      observer.observe(termEl);
      setTimeout(() => { observer.disconnect(); openAndLoadAddons(); }, 2000);
    }

    // ── Read cursor/scrollback settings ─────────────────────────────────

    void (async () => {
      try {
        const { settings } = await appRpc.request['settings.getAll']({});
        if (settings['cursor_style']) {
          const style = settings['cursor_style'];
          if (style === 'block' || style === 'underline' || style === 'bar') {
            term.options.cursorStyle = style;
          }
        }
        if (settings['cursor_blink'] === 'false') {
          term.options.cursorBlink = false;
        }
        if (settings['scrollback']) {
          const sb = parseInt(settings['scrollback'], 10);
          if (!isNaN(sb) && sb >= 0) term.options.scrollback = sb;
        }
      } catch { /* non-critical — use defaults */ }
    })();

    // ── Subscribe to terminal font changes ─────────────────────────────────

    unsubFont = fontStore.onTermFontChange((family: string, size: number) => {
      term.options.fontFamily = family || 'JetBrains Mono, Fira Code, monospace';
      term.options.fontSize   = size;
      fitAddon.fit();
      appRpc.request['pty.resize']({ sessionId, cols: term.cols, rows: term.rows }).catch(() => {});
    });

    // ── Connect to PTY daemon (fire-and-forget from onMount) ───────────────

    void (async () => {
      // Read default_shell and default_cwd from settings if not provided
      let effectiveCwd = cwd;
      try {
        const { settings } = await appRpc.request['settings.getAll']({});
        if (!effectiveCwd && settings['default_cwd']) {
          effectiveCwd = settings['default_cwd'];
        }
        // default_shell is handled by agor-ptyd, not needed in create params
      } catch { /* use provided or defaults */ }

      const { cols, rows } = term;
      const result = await appRpc.request['pty.create']({ sessionId, cols, rows, cwd: effectiveCwd });
      if (!result?.ok) {
        term.writeln(`\x1b[31m[agor] Failed to connect to PTY daemon: ${result?.error ?? 'unknown error'}\x1b[0m`);
        term.writeln('\x1b[33m[agor] Is agor-ptyd running? Start it with: agor-ptyd\x1b[0m');
      }
    })();

    // ── Receive output from daemon ─────────────────────────────────────────

    const outputHandler = ({ sessionId: sid, data }: { sessionId: string; data: string }) => {
      if (destroyed || sid !== sessionId) return;
      term.write(decodeBase64(data));
    };
    appRpc.addMessageListener('pty.output', outputHandler);

    const closedHandler = ({ sessionId: sid, exitCode }: { sessionId: string; exitCode: number | null }) => {
      if (destroyed || sid !== sessionId) return;
      term.writeln(`\r\n\x1b[90m[Process exited${exitCode !== null ? ` with code ${exitCode}` : ''}]\x1b[0m`);
    };
    appRpc.addMessageListener('pty.closed', closedHandler);

    // Fix #5: Store cleanup functions for message listeners
    listenerCleanups.push(
      () => appRpc.removeMessageListener?.('pty.output', outputHandler),
      () => appRpc.removeMessageListener?.('pty.closed', closedHandler),
    );

    // ── Send user input to daemon ──────────────────────────────────────────

    // Feature 5: Max terminal paste chunk (64KB) — truncate with warning
    const MAX_PASTE_CHUNK = 64 * 1024;
    term.onData((data: string) => {
      let payload = data;
      if (payload.length > MAX_PASTE_CHUNK) {
        console.warn(`[terminal] Paste truncated from ${payload.length} to ${MAX_PASTE_CHUNK} bytes`);
        payload = payload.slice(0, MAX_PASTE_CHUNK);
        term.writeln('\r\n\x1b[33m[agor] Paste truncated to 64KB\x1b[0m');
      }
      appRpc.request['pty.write']({ sessionId, data: payload }).catch((err: unknown) => {
        console.error('[pty.write] error:', err);
      });
    });

    // ── Sync resize events to daemon ───────────────────────────────────────

    term.onResize(({ cols: c, rows: r }) => {
      appRpc.request['pty.resize']({ sessionId, cols: c, rows: r }).catch(() => {});
    });

    ro = new ResizeObserver(() => { fitAddon.fit(); });
    ro.observe(termEl);
  });

  onDestroy(() => {
    destroyed = true;
    unsubFont?.();
    ro?.disconnect();
    // Fix #5: Clean up all message listeners to prevent leaks
    for (const cleanup of listenerCleanups) {
      try { cleanup(); } catch { /* ignore */ }
    }
    listenerCleanups = [];
    appRpc.request['pty.close']({ sessionId }).catch(() => {});
    term?.dispose();
  });
</script>

<div class="terminal-container" bind:this={termEl}></div>

<style>
  .terminal-container {
    width: 100%;
    height: 100%;
    min-height: 10rem;
  }

  :global(.xterm) {
    padding: 0.5rem;
  }
</style>
