<script lang="ts">
  import { onMount, onDestroy, tick } from "svelte";
  import { getCurrentWindow } from "@tauri-apps/api/window";
  import { convertFileSrc, invoke } from "@tauri-apps/api/core";
  import type { TranscriptDto, TranscriptEntry, ViewerSegment } from "$lib/types/app";
  import SpeakerButton from "$lib/components/SpeakerButton.svelte";
  import { formatTime, formatDuration } from "$lib/utils/time.js";
  import { PLAYBACK_SPEEDS } from "$lib/utils/constants.js";
  import { errorMessage } from "$lib/utils/errors.js";
  import { parseStoredJson } from "$lib/utils/storage.js";
  import { mapTranscriptRow, saveTranscriptMeta } from "$lib/stores/page.svelte.js";
  import { toasts } from "$lib/stores/toasts.svelte.ts";
  import { DEFAULT_PROFILE_ID } from "$lib/stores/profiles.svelte.ts";

  let item = $state<TranscriptEntry | null>(null);
  let audioEl = $state<HTMLAudioElement | null>(null);
  let playing = $state(false);
  let currentTime = $state(0);
  let duration = $state(0);
  let playbackRate = $state(1);
  let searchQuery = $state("");
  let activeSegmentIdx = $state(-1);
  let editingIdx = $state(-1);
  let editingText = $state("");
  let showTimestamps = $state(true);
  let showStarredOnly = $state(false);
  let animFrameId: number | null = null;
  let segmentRefs = $state<Array<HTMLElement | null>>([]);
  let viewerMode = $state<"view" | "edit">("view");
  let textDraft = $state("");
  let textDirty = $state(false);
  let textSaveTimer: ReturnType<typeof setTimeout> | null = null;
  let editingTextareaEl = $state<HTMLTextAreaElement | null>(null);
  let textLearnBase = $state("");

  function stopAudio() {
    if (audioEl) audioEl.pause();
    playing = false;
    if (animFrameId !== null) cancelAnimationFrame(animFrameId);
    animFrameId = null;
  }

  function buildAudio(audioPath: string | null) {
    stopAudio();
    audioEl = null;
    duration = 0;
    currentTime = 0;
    if (!audioPath) return;

    const src = convertFileSrc(audioPath);
    const audio = new Audio(src);
    audio.playbackRate = playbackRate;
    audio.onloadedmetadata = () => { duration = audio.duration; };
    audio.onended = () => {
      playing = false;
      if (animFrameId !== null) cancelAnimationFrame(animFrameId);
      animFrameId = null;
    };
    audio.onerror = () => { playing = false; };
    audioEl = audio;
  }

  function loadViewerItem(nextItem: TranscriptEntry | null) {
    item = nextItem;
    textDraft = nextItem?.text ?? "";
    textLearnBase = nextItem?.text ?? "";
    activeSegmentIdx = -1;
    editingIdx = -1;
    editingText = "";
    segmentRefs.length = 0;
    buildAudio(nextItem?.audioPath ?? null);
  }

  // Hydrate the viewer from a transcript ID handed off via localStorage.
  // The full row is fetched from SQLite via `get_transcript` so transcript
  // text never lives in localStorage where any same-origin script could
  // read it.
  async function loadFromHandoff(raw: string | null) {
    const handoff = parseStoredJson<{ id?: unknown }>(raw);
    const id = handoff?.id != null ? String(handoff.id) : "";
    if (!id) {
      loadViewerItem(null);
      return;
    }
    try {
      const dto = await invoke<TranscriptDto | null>("get_transcript", { id });
      loadViewerItem(dto ? mapTranscriptRow(dto) : null);
    } catch (err) {
      console.warn("viewer get_transcript failed", errorMessage(err));
      loadViewerItem(null);
    }
  }

  onMount(() => {
    loadFromHandoff(localStorage.getItem("lumotia_viewer_item"));
    const mode = localStorage.getItem("lumotia_viewer_mode");
    if (mode === "edit" || mode === "view") viewerMode = mode;

    // Listen for new items via storage events
    window.addEventListener("storage", handleStorageChange);
  });

  onDestroy(() => {
    stopAudio();
    window.removeEventListener("storage", handleStorageChange);
    // Flush any pending text edit before the window tears down.
    if (textSaveTimer) {
      clearTimeout(textSaveTimer);
      textSaveTimer = null;
    }
    if (viewerMode === "edit") {
      commitTextEdit(true);
    } else if (textDirty) {
      commitTextEdit();
    }
  });

  async function maybeLearnCorrections(originalText: string, editedText: string) {
    if (!item || !originalText.trim() || !editedText.trim() || originalText === editedText) {
      return;
    }

    try {
      const learned = await invoke<Array<{ term: string }>>("learn_profile_terms_from_edit_cmd", {
        profileId: item.profileId || DEFAULT_PROFILE_ID,
        originalText,
        editedText,
      });
      if (learned.length > 0) {
        const count = learned.length;
        toasts.success(
          count === 1 ? "Learned 1 profile term" : `Learned ${count} profile terms`,
          learned.map((entry) => entry.term).join(", "),
        );
      }
    } catch (err) {
      console.warn("viewer maybeLearnCorrections failed", errorMessage(err));
    }
  }

  function handleStorageChange(e: StorageEvent) {
    if (e.key === "lumotia_viewer_item" && e.newValue) {
      void loadFromHandoff(e.newValue);
    }
  }

  function togglePlay() {
    if (!audioEl) return;
    if (playing) {
      audioEl.pause();
      playing = false;
      if (animFrameId !== null) cancelAnimationFrame(animFrameId);
    } else {
      audioEl.play();
      playing = true;
      tickTime();
    }
  }

  function tickTime() {
    if (audioEl) {
      currentTime = audioEl.currentTime;
      updateActiveSegment();
      if (!audioEl.paused) {
        animFrameId = requestAnimationFrame(tickTime);
      }
    }
  }

  function updateActiveSegment() {
    if (!item?.segments) return;
    const t = currentTime;
    for (let i = 0; i < item.segments.length; i++) {
      const seg = item.segments[i];
      if (t >= seg.start && t < seg.end) {
        if (activeSegmentIdx !== i) {
          activeSegmentIdx = i;
          // Auto-scroll to active segment
          segmentRefs[i]?.scrollIntoView({ behavior: "smooth", block: "center" });
        }
        return;
      }
    }
  }

  function seekTo(e: Event) {
    const val = parseFloat((e.currentTarget as HTMLInputElement).value);
    if (audioEl) {
      audioEl.currentTime = val;
      currentTime = val;
      updateActiveSegment();
    }
  }

  function seekToTime(time: number) {
    if (audioEl) {
      audioEl.currentTime = time;
      currentTime = time;
      if (!playing) {
        audioEl.play();
        playing = true;
        tickTime();
      }
      updateActiveSegment();
    }
  }

  function setSpeed(speed: number) {
    playbackRate = speed;
    if (audioEl) audioEl.playbackRate = speed;
  }


  // Search: find matching segments
  let matchingSegments = $derived.by(() => {
    if (!searchQuery.trim() || !item?.segments) return new Set<number>();
    const q = searchQuery.toLowerCase();
    const matches = new Set<number>();
    item.segments.forEach((seg, i) => {
      if (seg.text.toLowerCase().includes(q)) matches.add(i);
    });
    return matches;
  });

  let matchCount = $derived.by(() => matchingSegments.size);

  // Highlight search matches in text (XSS-safe: escapes HTML entities first)
  function highlightText(text: string) {
    if (!searchQuery.trim()) return text;
    const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
    const q = searchQuery.trim();
    const regex = new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
    return escaped.replace(regex, '<mark class="bg-accent/25 text-text rounded px-0.5">$1</mark>');
  }

  // Segment editing
  async function startEditing(idx: number) {
    if (!item?.segments?.[idx]) return;
    editingIdx = idx;
    editingText = item.segments[idx].text;
    await tick();
    editingTextareaEl?.focus();
    editingTextareaEl?.select();
  }

  function finishEditing() {
    if (editingIdx >= 0 && item?.segments) {
      const previousText = item.text;
      item.segments[editingIdx].text = editingText.trim();
      // Update full text
      item.text = item.segments.map((s) => s.text).join(" ").trim();
      textDraft = item.text;
      saveItemToHistory();
      // Task 2.5 — persist segment boundaries (text + starred flags) to
      // SQLite via segments_json. update_transcript above only covers text
      // and title; the segment array needs the dedicated meta command.
      persistSegments();
      void maybeLearnCorrections(previousText, item.text);
      textLearnBase = item.text;
    }
    editingIdx = -1;
    editingText = "";
  }

  function deleteSegment(idx: number) {
    if (item?.segments) {
      item.segments.splice(idx, 1);
      item.text = item.segments.map((s) => s.text).join(" ").trim();
      saveItemToHistory();
      // Task 2.5 — persist the shrunk segment array.
      persistSegments();
      if (editingIdx === idx) { editingIdx = -1; }
    }
  }

  function toggleStar(idx: number) {
    if (item?.segments) {
      item.segments[idx].starred = !item.segments[idx].starred;
      // Task 2.5 — persist the updated segments array (incl. starred flags)
      // via the new update_transcript_meta_cmd. Before this, starring a
      // segment was in-memory only and vanished on reload.
      persistSegments();
    }
  }

  // Task 2.5 — single entry point for segments_json persistence. Callers
  // already guard with `if (item?.segments)` so the snapshot here is safe;
  // the @ts-ignore matches the file-wide pattern where `item` inferred as
  // `null` from `$state(null)` narrows to `never` after guards. Replacing
  // the annotation needs a full viewer re-typing pass — out of scope for
  // Task 2.5, tracked by the file's 80-odd pre-existing TS errors.
  function persistSegments() {
    if (!item) return;
    saveTranscriptMeta(item.id, { segments: item.segments });
  }

  function copySegment(idx: number) {
    if (item?.segments?.[idx]) {
      const text = item.segments[idx].text.trim();
      navigator.clipboard.writeText(text).catch(() => {
        invoke("copy_to_clipboard", { text }).catch(() => {});
      });
    }
  }

  // Persist edits back to SQLite. `update_transcript` covers `text` / `title`;
  // segment structure, per-segment starred flags, manualTags, template and
  // language go through `update_transcript_meta_cmd` (Task 2.5) via the
  // store's `saveTranscriptMeta` helper — callers in this file wire those
  // up alongside the plain-text update below.
  //
  // We re-stamp the localStorage handoff with the same ID so a sibling
  // viewer window receives a `storage` event and re-fetches from SQLite.
  // The handoff intentionally contains no transcript content — see
  // `loadFromHandoff` for the rationale.
  function saveItemToHistory() {
    if (!item) return;
    try {
      localStorage.setItem(
        "lumotia_viewer_item",
        JSON.stringify({ id: String(item.id), stamp: Date.now() }),
      );
    } catch {}
    // Best-effort SQLite persistence. Silently ignored in browser preview
    // (no Tauri runtime) — matches the main-window store's behaviour.
    invoke("update_transcript", {
      id: String(item.id),
      text: item.text ?? null,
      title: item.title ?? null,
    }).catch((err) => {
      console.warn("viewer saveItemToHistory: update_transcript failed", errorMessage(err));
    });
  }

  function scheduleTextSave() {
    textDirty = true;
    if (textSaveTimer) clearTimeout(textSaveTimer);
    textSaveTimer = setTimeout(() => {
      commitTextEdit();
    }, 400);
  }

  function commitTextEdit(learnCorrections = false) {
    if (!item) return;
    const next = textDraft;
    if (next !== item.text) {
      item.text = next;
      // The compact preview in History is derived from `preview`; keep it
      // roughly in sync so the list does not show stale copy after an edit.
      item.preview = next.slice(0, 120);
      saveItemToHistory();
    }
    if (learnCorrections && next !== textLearnBase) {
      void maybeLearnCorrections(textLearnBase, next);
      textLearnBase = next;
    }
    if (next === item.text) {
      textDirty = false;
      return;
    }
    textDirty = false;
  }

  // Filtered segments (starred filter + search)
  let visibleSegments = $derived.by(() => {
    if (!item?.segments) return [] as ViewerSegment[];
    let segs: ViewerSegment[] = item.segments.map((seg, i) => ({ ...seg, _idx: i }));
    if (showStarredOnly) {
      segs = segs.filter((s) => s.starred);
    }
    return segs;
  });

  // Window drag — pointerdown + setPointerCapture avoids the mousedown
  // latency that makes KWin's initial grab feel draggy.
  function handleDragStart(e: PointerEvent) {
    if (e.button !== 0) return;
    if ((e.target as HTMLElement | null)?.closest("button")) return;
    if ((e.target as HTMLElement | null)?.closest("input")) return;
    try { (e.currentTarget as HTMLElement | null)?.setPointerCapture?.(e.pointerId); } catch {}
    getCurrentWindow().startDragging();
  }

</script>

<div class="flex flex-col h-full bg-bg">
  <!-- Drag handle -->
  <!-- svelte-ignore a11y_no_static_element_interactions -->
  <div
    class="flex items-center h-[36px] bg-bg-elevated select-none px-3"
    onpointerdown={handleDragStart}
  >
    <span class="text-[12px] font-medium text-text-secondary tracking-wide">
      Lumotia - {viewerMode === "edit" ? "Editor" : "Viewer"}
    </span>
    <div class="flex-1"></div>
  </div>

  {#if item}
    <!-- Item info -->
    <div class="px-5 pt-3 pb-2 flex items-start gap-2">
      <div class="flex-1 min-w-0">
        <p class="text-[13px] text-text font-medium">
          {item.title || "Transcript"}
        </p>
        <p class="text-[12px] text-text-secondary mt-0.5">
          {item.date}{#if item.duration} · {formatDuration(item.duration)}{/if} · {item.source}
        </p>
      </div>
      {#if item.text?.trim()}
        <SpeakerButton text={item.text} label="Read transcript aloud" size={14} />
      {/if}
    </div>

    <!-- Media controls -->
    {#if audioEl}
      <div class="flex items-center gap-3 px-5 py-2 border-b border-border-subtle">
        <!-- Play/Pause -->
        <button
          class="w-9 h-9 rounded-full flex items-center justify-center flex-shrink-0
            {playing ? 'bg-accent text-white' : 'bg-accent/15 text-accent hover:bg-accent/25'}"
          onclick={togglePlay}
          aria-label={playing ? "Pause" : "Play"}
        >
          {#if playing}
            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
              <rect x="6" y="4" width="4" height="16" rx="1" />
              <rect x="14" y="4" width="4" height="16" rx="1" />
            </svg>
          {:else}
            <svg class="w-4 h-4 ml-0.5" viewBox="0 0 24 24" fill="currentColor">
              <path d="M8 5v14l11-7z" />
            </svg>
          {/if}
        </button>

        <!-- Time -->
        <span class="text-[12px] text-text-secondary tabular-nums min-w-[70px]">
          {formatTime(currentTime)} / {formatTime(duration)}
        </span>

        <!-- Seek bar -->
        <input
          type="range"
          min="0"
          max={duration || 0}
          step="0.1"
          value={currentTime}
          oninput={seekTo}
          class="flex-1 accent-accent h-1"
          data-no-transition
        />

        <!-- Speed pills -->
        <div class="flex gap-0.5 flex-shrink-0">
          {#each PLAYBACK_SPEEDS as speed}
            <button
              class="text-[12px] px-1.5 py-0.5 rounded-full
                {playbackRate === speed
                  ? 'bg-accent/15 text-accent font-medium'
                  : 'text-text-tertiary hover:text-text-secondary'}"
              onclick={() => setSpeed(speed)}
            >{speed}x</button>
          {/each}
        </div>
      </div>
    {/if}

    <!-- Search bar + view controls (segment-specific; hidden in edit mode) -->
    {#if viewerMode !== "edit"}
    <div class="flex items-center gap-2 px-5 py-2 border-b border-border-subtle">
      <div class="flex items-center gap-2 bg-bg-input border border-border rounded-lg px-3 py-1.5 focus-within:border-accent flex-1">
        <svg class="w-3.5 h-3.5 text-text-tertiary flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <circle cx="11" cy="11" r="8" />
          <path d="M21 21l-4.35-4.35" stroke-linecap="round" />
        </svg>
        <input
          type="text"
          class="flex-1 bg-transparent text-[12px] text-text placeholder:text-text-tertiary focus:outline-none"
          placeholder="Search transcript..."
          bind:value={searchQuery}
          data-no-transition
        />
        {#if searchQuery}
          <span class="text-[12px] text-text-secondary">{matchCount} matches</span>
          <button class="text-[12px] text-text-secondary hover:text-text" onclick={() => searchQuery = ""}>Clear</button>
        {/if}
      </div>
      <!-- Compact mode toggle -->
      <button
        class="text-[12px] px-2 py-1 rounded-lg flex-shrink-0
          {showTimestamps ? 'text-text-tertiary hover:text-text-secondary' : 'text-accent bg-accent/10'}"
        onclick={() => showTimestamps = !showTimestamps}
        title={showTimestamps ? "Hide timestamps (compact)" : "Show timestamps"}
      >Compact</button>
      <!-- Starred filter -->
      <button
        class="text-[12px] px-2 py-1 rounded-lg flex-shrink-0
          {showStarredOnly ? 'text-accent bg-accent/10' : 'text-text-tertiary hover:text-text-secondary'}"
        onclick={() => showStarredOnly = !showStarredOnly}
        title={showStarredOnly ? "Show all segments" : "Show starred only"}
      >Starred</button>
    </div>
    {/if}

    <!-- Transcript segments / editor -->
    <div class="flex-1 overflow-y-auto px-5 py-3 min-h-0">
      {#if viewerMode === "edit"}
        <textarea
          class="w-full h-full bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px]
                 text-text leading-relaxed resize-none focus:outline-none focus:border-accent"
          bind:value={textDraft}
          oninput={scheduleTextSave}
          onblur={() => commitTextEdit(true)}
          data-no-transition
          placeholder="Edit the transcript..."
        ></textarea>
      {:else if item.segments && item.segments.length > 0}
        <div class="flex flex-col gap-1">
          {#each visibleSegments as seg (seg._idx)}
            <div
              bind:this={segmentRefs[seg._idx]}
              class="group flex gap-3 px-3 py-2 rounded-lg cursor-pointer transition-colors
                {activeSegmentIdx === seg._idx
                  ? 'bg-accent/10'
                  : matchingSegments.has(seg._idx)
                    ? 'bg-warning/10'
                    : 'hover:bg-hover'}"
              onclick={() => seekToTime(seg.start)}
              ondblclick={() => startEditing(seg._idx)}
              role="button"
              tabindex="0"
              onkeydown={(e) => { if (e.key === "Enter" && editingIdx !== seg._idx) seekToTime(seg.start); }}
            >
              <!-- Star -->
              <button
                class="mt-0.5 flex-shrink-0 {seg.starred ? 'text-accent' : 'text-text-tertiary opacity-0 group-hover:opacity-100'}"
                onclick={(e) => { e.stopPropagation(); toggleStar(seg._idx); }}
                aria-label={seg.starred ? "Unstar" : "Star"}
              >
                <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill={seg.starred ? "currentColor" : "none"} stroke="currentColor" stroke-width="2">
                  <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
                </svg>
              </button>
              <!-- Timestamp -->
              {#if showTimestamps}
                <span class="text-[12px] text-text-secondary tabular-nums flex-shrink-0 mt-0.5 min-w-[36px]">
                  {formatTime(seg.start)}
                </span>
              {/if}
              <!-- Text (editable or display) -->
              {#if editingIdx === seg._idx}
                <textarea
                  bind:this={editingTextareaEl}
                  class="flex-1 bg-bg-input border border-accent rounded-lg px-2 py-1 text-[13px] text-text
                         leading-relaxed resize-none focus:outline-none min-h-[40px]"
                  bind:value={editingText}
                  onkeydown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); finishEditing(); } if (e.key === "Escape") { editingIdx = -1; } }}
                  onblur={finishEditing}
                  onclick={(e) => e.stopPropagation()}
                  data-no-transition
                ></textarea>
              {:else}
                <p class="text-[13px] text-text leading-relaxed flex-1">
                  {#if searchQuery.trim()}
                    {@html highlightText(seg.text.trim())}
                  {:else}
                    {seg.text.trim()}
                  {/if}
                </p>
              {/if}
              <!-- Actions (hover reveal) -->
              <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 flex-shrink-0">
                <button
                  class="text-text-tertiary hover:text-accent"
                  onclick={(e) => { e.stopPropagation(); copySegment(seg._idx); }}
                  title="Copy segment"
                >
                  <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <rect x="9" y="9" width="13" height="13" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
                  </svg>
                </button>
                <button
                  class="text-text-tertiary hover:text-danger"
                  onclick={(e) => { e.stopPropagation(); deleteSegment(seg._idx); }}
                  title="Delete segment"
                >
                  <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" />
                  </svg>
                </button>
              </div>
            </div>
          {/each}
        </div>
      {:else}
        <!-- No segments — show full text -->
        <p class="text-[13px] text-text leading-relaxed whitespace-pre-wrap">{item.text}</p>
      {/if}
    </div>
  {:else}
    <div class="flex-1 flex items-center justify-center">
      <p class="text-[13px] text-text-secondary">No transcript loaded</p>
    </div>
  {/if}
</div>
