<script lang="ts">
  // @ts-nocheck
  import { onDestroy, onMount, tick } from "svelte";
  import { invoke } from "@tauri-apps/api/core";
  import { listen } from "@tauri-apps/api/event";
  import { getCurrentWindow } from "@tauri-apps/api/window";
  import { Copy, Check, X, RotateCcw } from "lucide-svelte";
  import { settings } from "$lib/stores/page.svelte.js";

  // Phase state machine:
  //   listening → live → cleanup → final → (auto-hide)
  type Phase = "listening" | "live" | "cleanup" | "final";
  let phase = $state<Phase>("listening");
  let rawText = $state("");
  let finalText = $state("");
  let copied = $state(false);

  let scrollEl: HTMLDivElement | null = null;
  const unlisteners: Array<() => void> = [];
  let autoHideTimer: number | null = null;
  let copyResetTimer: number | null = null;

  const AUTO_HIDE_MS = 4000;
  const COPY_RESET_MS = 1400;

  function clearAutoHide() {
    if (autoHideTimer !== null) {
      window.clearTimeout(autoHideTimer);
      autoHideTimer = null;
    }
  }

  function scheduleAutoHide() {
    clearAutoHide();
    autoHideTimer = window.setTimeout(() => {
      getCurrentWindow().hide().catch(() => {});
    }, AUTO_HIDE_MS);
  }

  async function scrollToBottom() {
    await tick();
    if (scrollEl) scrollEl.scrollTop = scrollEl.scrollHeight;
  }

  async function copyActiveText() {
    const text = phase === "final" ? finalText : rawText;
    if (!text.trim()) return;
    let ok = false;
    try {
      await navigator.clipboard.writeText(text);
      ok = true;
    } catch {
      ok = await invoke("copy_to_clipboard", { text }).then(() => true).catch(() => false);
    }
    if (!ok) return;
    copied = true;
    if (copyResetTimer !== null) window.clearTimeout(copyResetTimer);
    copyResetTimer = window.setTimeout(() => { copied = false; }, COPY_RESET_MS);
    // Re-arm auto-hide so the user's copy action doesn't get cut short.
    if (phase === "final") scheduleAutoHide();
  }

  function dismiss() {
    clearAutoHide();
    getCurrentWindow().hide().catch(() => {});
  }

  let revertBusy = $state(false);
  let reverted = $state(false);

  // Brief item #17: the raw Whisper transcript is the source of truth.
  // In final phase the user sees both raw (in rawText) and formatted
  // (in finalText). If the LLM cleanup changed their meaning, one
  // click here replaces the pasted output with raw instead.
  //
  // Two paths depending on how the transcript was delivered to the
  // target app:
  //   autoPaste on  → the LLM output was pasted into whatever app had
  //                   focus. paste_text_replacing sends undo, waits a
  //                   beat, then pastes the raw text.
  //   autoPaste off → nothing was pasted; the LLM output is only on
  //                   the clipboard. We just overwrite the clipboard
  //                   with raw so the user's own paste yields raw.
  async function revertToRaw() {
    if (!rawText.trim() || revertBusy) return;
    // Extend auto-hide so the user can actually see the confirmation
    // without the window disappearing under them.
    clearAutoHide();
    revertBusy = true;
    try {
      if (settings.autoPaste) {
        const outcome = await invoke("paste_text_replacing", { text: rawText });
        if (!outcome?.pasted && outcome?.message) {
          console.warn("[preview] paste_text_replacing partial failure:", outcome.message);
        }
      } else {
        try {
          await navigator.clipboard.writeText(rawText);
        } catch {
          await invoke("copy_to_clipboard", { text: rawText }).catch(() => {});
        }
      }
      reverted = true;
    } finally {
      revertBusy = false;
      scheduleAutoHide();
    }
  }

  onMount(async () => {
    // preview-listening: recording just started, no text yet.
    unlisteners.push(
      await listen("preview-listening", () => {
        clearAutoHide();
        phase = "listening";
        rawText = "";
        finalText = "";
      }),
    );

    // preview-append: main forwards each live chunk's raw_text. We also
    // accept the full payload from single-shot transcriptions as append.
    unlisteners.push(
      await listen<{ text: string; replace?: boolean }>("preview-append", (event) => {
        const chunk = (event.payload?.text ?? "").trim();
        if (!chunk) return;
        if (event.payload?.replace) {
          rawText = chunk;
        } else if (rawText.length === 0) {
          rawText = chunk;
        } else {
          rawText = `${rawText} ${chunk}`;
        }
        phase = "live";
        scrollToBottom();
      }),
    );

    // preview-cleanup: main window is about to run the LLM cleanup pass.
    unlisteners.push(
      await listen("preview-cleanup", () => {
        phase = "cleanup";
      }),
    );

    // preview-final: cleanup done (or skipped). Show formatted text and
    // start the 4s auto-hide countdown.
    unlisteners.push(
      await listen<{ text: string }>("preview-final", (event) => {
        finalText = (event.payload?.text ?? "").trim();
        phase = "final";
        reverted = false;
        scrollToBottom();
        scheduleAutoHide();
      }),
    );

    // preview-hide: main asks us to dismiss immediately (recording
    // cancelled, or the user re-focused the main window mid-stream).
    unlisteners.push(
      await listen("preview-hide", () => {
        dismiss();
      }),
    );
  });

  onDestroy(() => {
    clearAutoHide();
    if (copyResetTimer !== null) window.clearTimeout(copyResetTimer);
    for (const off of unlisteners) off();
  });

  let activeText = $derived(phase === "final" ? finalText : rawText);
  // Only offer "use raw" when there's a meaningful delta to revert to.
  // If rawText and finalText are identical (Raw format mode, or LLM
  // cleanup disabled), the button is pointless.
  let canRevertToRaw = $derived(
    phase === "final" && rawText.trim().length > 0 && rawText.trim() !== finalText.trim(),
  );
  let phaseLabel = $derived(
    phase === "listening" ? "Listening"
      : phase === "live" ? "Raw"
      : phase === "cleanup" ? "Cleaning up"
      : "Final",
  );
  let borderColorClass = $derived(
    phase === "final" ? "border-success"
      : phase === "cleanup" ? "border-accent"
      : "border-border",
  );
</script>

<div
  class="h-full w-full flex flex-col bg-bg text-text p-3 gap-2 border-l-2 {borderColorClass}"
  style="transition: border-color var(--duration-ui) ease-out"
  data-tauri-drag-region
>
  <header class="flex items-center justify-between gap-2 text-[11px] text-text-secondary" data-tauri-drag-region>
    <div class="flex items-center gap-2" data-tauri-drag-region>
      {#if phase === "listening"}
        <span class="inline-block w-[8px] h-[8px] rounded-full bg-text-tertiary animate-pulse"></span>
      {:else if phase === "live"}
        <span class="inline-flex items-end gap-[2px] h-[10px]">
          <span class="w-[3px] bg-accent rounded-sm animate-bars-1"></span>
          <span class="w-[3px] bg-accent rounded-sm animate-bars-2"></span>
          <span class="w-[3px] bg-accent rounded-sm animate-bars-3"></span>
        </span>
      {:else if phase === "cleanup"}
        <span class="inline-flex items-end gap-[2px] h-[10px]">
          <span class="w-[3px] bg-accent/80 rounded-sm animate-bars-1"></span>
          <span class="w-[3px] bg-accent/80 rounded-sm animate-bars-2"></span>
          <span class="w-[3px] bg-accent/80 rounded-sm animate-bars-3"></span>
        </span>
      {:else}
        <Check size={12} class="text-success" />
      {/if}
      <span class="uppercase tracking-wider font-medium">{phaseLabel}</span>
    </div>

    <div class="flex items-center gap-1">
      {#if canRevertToRaw}
        <button
          class="px-2 py-1 rounded hover:bg-hover text-text-tertiary hover:text-text disabled:opacity-40 flex items-center gap-1 text-[11px]"
          onclick={revertToRaw}
          disabled={revertBusy}
          title={settings.autoPaste ? "Replace pasted text with raw transcript" : "Put raw transcript on clipboard"}
        >
          {#if reverted}
            <Check size={12} />
            <span>Raw used</span>
          {:else}
            <RotateCcw size={12} />
            <span>{settings.autoPaste ? "Use raw" : "Copy raw"}</span>
          {/if}
        </button>
      {/if}
      <button
        class="p-1 rounded hover:bg-hover text-text-tertiary hover:text-text disabled:opacity-40"
        onclick={copyActiveText}
        disabled={!activeText.trim()}
        title="Copy"
      >
        {#if copied}
          <Check size={14} />
        {:else}
          <Copy size={14} />
        {/if}
      </button>
      <button
        class="p-1 rounded hover:bg-hover text-text-tertiary hover:text-text"
        onclick={dismiss}
        title="Dismiss"
      >
        <X size={14} />
      </button>
    </div>
  </header>

  <div
    bind:this={scrollEl}
    class="flex-1 min-h-0 overflow-y-auto text-[14px] leading-relaxed whitespace-pre-wrap break-words"
  >
    {#if activeText.trim()}
      {activeText}
    {:else}
      <span class="text-text-tertiary italic">
        {phase === "listening" ? "Waiting for speech…" : ""}
      </span>
    {/if}
  </div>
</div>

<style>
  @keyframes bars-1 { 0%,100% { height: 40%; } 50% { height: 100%; } }
  @keyframes bars-2 { 0%,100% { height: 70%; } 50% { height: 30%; } }
  @keyframes bars-3 { 0%,100% { height: 50%; } 50% { height: 90%; } }
  :global(.animate-bars-1) { animation: bars-1 0.9s ease-in-out infinite; }
  :global(.animate-bars-2) { animation: bars-2 0.9s ease-in-out infinite 0.15s; }
  :global(.animate-bars-3) { animation: bars-3 0.9s ease-in-out infinite 0.3s; }
</style>
