<script lang="ts">
  // @ts-nocheck
  import { onMount, onDestroy } from "svelte";
  import { Channel, invoke } from "@tauri-apps/api/core";
  import { emit } from "@tauri-apps/api/event";
  import { getCurrentWindow } from "@tauri-apps/api/window";
  import { page, settings, templates, profiles, addToHistory, addTask, tasks } from "$lib/stores/page.svelte.js";
  import { markGenerating, markGenerationDone } from "$lib/stores/llmStatus.svelte.js";
  import { playStartCue, playStopCue, playCompleteCue } from "$lib/utils/sounds.js";
  import { profilesStore } from "$lib/stores/profiles.svelte.ts";
  import { toasts } from "$lib/stores/toasts.svelte.js";
  import Card from "$lib/components/Card.svelte";
  import ModelDownloader from "$lib/components/ModelDownloader.svelte";
  import { exportTranscript } from "$lib/utils/export.js";
  import { extractTasks } from "$lib/utils/taskExtractor.js";
  import { pad } from "$lib/utils/time.js";
  import { FEEDBACK_TIMEOUT_MS } from "$lib/utils/constants.js";
  import { Mic, Loader2, SquareCheck, AlertTriangle } from 'lucide-svelte';
  import EmptyState from '$lib/components/EmptyState.svelte';
  import SpeakerButton from '$lib/components/SpeakerButton.svelte';
  import { getPreferences } from '$lib/stores/preferences.svelte.js';
  import { bionicReading } from '$lib/actions/bionicReading.js';
  import { measurePreWrap } from '$lib/utils/textMeasure.js';
  import { transcriptPretextFont, transcriptPretextLineHeight } from '$lib/utils/accessibilityTypography.js';
  import { hasTauriRuntime } from '$lib/utils/runtime.js';
  const prefs = getPreferences();
  const tauriRuntimeAvailable = hasTauriRuntime();
  const browserPreviewMessage = "You're viewing Lumotia in a normal browser. Local transcription only works in the Tauri desktop app window.";

  let transcript = $state("");
  let segments = $state([]);
  let timerInterval = $state(null);
  let startTime = $state(0);
  let modelReady = $state(false);
  let modelLoading = $state(false);
  let needsDownload = $state(false);
  let error = $state("");
  let showExportMenu = $state(false);
  let transcribing = $state(false);
  let saved = $state(false);
  let extractedCount = $state(0);
  let aiProcessing = $state(false);
  let aiStatus = $state("");
  let runtimeCapabilities = $state(null);
  let sessionId = $state(null);
  let drainingSessionId = $state(null);
  let liveWarning = $state("");
  let lastResultAt = $state(0);
  let lastLiveActivityAt = $state(0);
  let resultChannel = null;
  let statusChannel = null;

  // Cursor-based insertion
  let textareaEl = $state(null);
  let insertPos = $state(-1); // -1 = append mode, >= 0 = insert at position

  // Template
  let activeTemplate = $state("");
  let showTemplateMenu = $state(false);

  // Deduplication: track which chunk IDs have been processed
  let processedChunks = new Set();

  // Global hotkey listener
  let hotkeyHandler = () => toggleRecording();

  onMount(async () => {
    window.addEventListener("lumotia:toggle-recording", hotkeyHandler);

    if (!tauriRuntimeAvailable) {
      error = browserPreviewMessage;
      return;
    }

    await checkModelState();
    await ensureLlmModelLoaded();
  });

  onDestroy(() => {
    window.removeEventListener("lumotia:toggle-recording", hotkeyHandler);
    clearInterval(timerInterval);
    if (page.recording) {
      page.recording = false;
      page.status = "Ready";
      page.statusColor = "#7ec89a";
    }
    cleanup();
  });

  $effect(() => {
    settings.engine;
    settings.modelSize;
    if (!tauriRuntimeAvailable) return;
    queueMicrotask(() => {
      if (!page.recording) {
        void checkModelState();
      }
    });
  });

  function whisperModelId(size) {
    const map = {
      Tiny: "whisper-tiny-en",
      Base: "whisper-base-en",
      Small: "whisper-small-en",
      Medium: "whisper-medium-en",
    };
    return map[size] || "whisper-base-en";
  }

  function selectedModelId() {
    return settings.engine === "parakeet"
      ? "parakeet-ctc-0.6b-int8"
      : whisperModelId(settings.modelSize);
  }

  function currentEngineCapabilities() {
    return runtimeCapabilities?.engines?.find((engine) => engine.id === settings.engine) || null;
  }

  function currentModelCapabilities() {
    return currentEngineCapabilities()?.models?.find((model) => model.id === selectedModelId()) || null;
  }

  function currentModelIsEnglishOnly() {
    return currentModelCapabilities()?.languageSupport?.kind === "english-only";
  }

  function effectiveLanguage() {
    return currentModelIsEnglishOnly() ? "en" : settings.language;
  }

  async function refreshRuntimeCapabilities() {
    runtimeCapabilities = await invoke("get_runtime_capabilities");
  }

  function matchesLiveSession(candidateSessionId) {
    return candidateSessionId != null
      && (candidateSessionId === sessionId || candidateSessionId === drainingSessionId);
  }

  function handleLiveResult(result) {
    if (!result || !matchesLiveSession(result.sessionId) || !result.segments) {
      return;
    }

    lastResultAt = Date.now();
    lastLiveActivityAt = lastResultAt;
    if (textareaEl) {
      if (page.recording && userScrolledUp) {
        shouldPreserveScrollAnchor = true;
      } else if (page.recording) {
        shouldStickToBottom = true;
      }
    }

    if (result.chunkId != null && processedChunks.has(result.chunkId)) {
      return;
    }
    if (result.chunkId != null) processedChunks.add(result.chunkId);

    // Forward raw Whisper output to the preview overlay (if it's enabled
    // and opened). `raw_text` was captured in live.rs before post-processing.
    if (result.rawText && settings.transcriptionPreview) {
      emit("preview-append", { text: result.rawText }).catch(() => {});
    }

    const text = result.segments.map((segment) => segment.text).join(" ").trim();
    if (text) {
      if (insertPos >= 0) {
        const before = transcript.slice(0, insertPos);
        const after = transcript.slice(insertPos);
        const spaceBefore = before && !before.endsWith(" ") && !before.endsWith("\n") ? " " : "";
        const spaceAfter = after && !after.startsWith(" ") && !after.startsWith("\n") ? " " : "";
        transcript = before + spaceBefore + text + spaceAfter + after;
        insertPos += spaceBefore.length + text.length + spaceAfter.length;
        requestAnimationFrame(() => {
          if (textareaEl) {
            textareaEl.selectionStart = insertPos;
            textareaEl.selectionEnd = insertPos;
          }
        });
      } else {
        transcript = transcript ? `${transcript} ${text}` : text;
      }
    }

    const absoluteSegments = result.segments.map((segment) => ({
      ...segment,
      start: segment.start + result.chunkStartSecs,
      end: segment.end + result.chunkStartSecs,
    }));
    segments = [...segments, ...absoluteSegments];
  }

  function handleLiveStatus(status) {
    if (!status || !matchesLiveSession(status.sessionId)) return;

    lastLiveActivityAt = Date.now();

    if (status.type === "overload" || status.type === "warning") {
      liveWarning = status.message || "Lumotia is dropping older audio to stay responsive.";
      return;
    }

    if (status.type === "error") {
      error = status.message || "Live transcription failed";
      transcriptionFailed = true;
      page.status = "Error";
      page.statusColor = "#e87171";
      return;
    }

    if (status.type === "finished" && status.droppedAudioMs > 0) {
      liveWarning = `Dropped ${Math.round(status.droppedAudioMs / 1000)}s of older audio to keep up in real time.`;
    }
  }

  async function checkModelState() {
    if (!tauriRuntimeAvailable) {
      modelReady = false;
      needsDownload = false;
      return;
    }
    try {
      await refreshRuntimeCapabilities();
      const currentModel = currentModelCapabilities();
      modelReady = !!currentModel?.loaded;
      needsDownload = currentModel ? !currentModel.downloaded : true;
      if (currentModelIsEnglishOnly() && settings.language !== "en") {
        settings.language = "en";
      }
    } catch (err) {
      error = typeof err === "string" ? err : err.message || "Failed to check model";
    }
  }

  async function ensureLlmModelLoaded() {
    if (!tauriRuntimeAvailable || settings.aiTier === "off" || !settings.llmModelId) return;
    try {
      const status = await invoke("check_llm_model", { modelId: settings.llmModelId });
      if (status?.downloaded && !status.loaded) {
        await invoke("load_llm_model", {
          modelId: settings.llmModelId,
          // Sequential-GPU guard (brief item A.1 #28): frees whisper
          // before loading the LLM on tight-VRAM setups. "parallel"
          // keeps both resident (default, safe on multi-GB cards).
          concurrent: settings.aiGpuConcurrency === "parallel",
        });
      }
    } catch (err) {
      console.warn("ensureLlmModelLoaded failed", err);
    }
  }

  async function loadModel() {
    if (!tauriRuntimeAvailable) {
      error = browserPreviewMessage;
      return;
    }
    modelLoading = true;
    page.status = "Loading model...";
    page.statusColor = "#e8c86e";
    error = "";

    try {
      // concurrent flag mirrors settings.aiGpuConcurrency (brief A.1 #28).
      const concurrent = settings.aiGpuConcurrency === "parallel";
      if (settings.engine === "parakeet") {
        await invoke("load_parakeet_model", { name: "ctc-int8", concurrent });
      } else {
        await invoke("load_model", {
          size: settings.modelSize.toLowerCase(),
          concurrent,
        });
      }
      await refreshRuntimeCapabilities();
      modelReady = true;
      modelLoading = false;
      page.status = "Ready";
      page.statusColor = "#7ec89a";
    } catch (err) {
      error = typeof err === "string" ? err : err.message || "Failed to load model";
      modelLoading = false;
      page.status = "Error";
      page.statusColor = "#e87171";
    }
  }

  function onModelDownloaded() {
    needsDownload = false;
    void loadModel();
  }

  async function toggleRecording() {
    if (page.recording) {
      await stopRecording();
    } else {
      await startRecording();
    }
  }

  async function startRecording() {
    error = "";
    liveWarning = "";
    saved = false;
    transcriptionFailed = false;
    if (!tauriRuntimeAvailable) {
      error = browserPreviewMessage;
      page.status = "Desktop app required";
      page.statusColor = "#e8c86e";
      return;
    }
    if (!modelReady) {
      if (needsDownload) return;
      await loadModel();
      if (!modelReady) return;
    }

    // Capture cursor position for insert mode
    if (textareaEl && transcript.length > 0) {
      insertPos = textareaEl.selectionStart ?? transcript.length;
    } else {
      insertPos = -1; // Append mode for empty textarea
    }

    try {
      sessionId = null;
      drainingSessionId = null;
      lastResultAt = 0;
      lastLiveActivityAt = 0;
      processedChunks.clear();

      // Only clear transcript if not in insert mode (fresh recording)
      if (insertPos === -1) {
        transcript = "";
        segments = [];
        previousMeasuredContentHeight = 0;
      }

      resultChannel = new Channel((message) => handleLiveResult(message));
      statusChannel = new Channel((message) => handleLiveStatus(message));

      const response = await invoke("start_live_transcription_session", {
        config: {
          engine: settings.engine,
          modelId: selectedModelId(),
          language: effectiveLanguage(),
          // Backend (Task 13) falls back to profile.initial_prompt when this
          // is empty. Vocabulary now lives on the active profile, not on the
          // old page.activeProfile localStorage bag.
          initialPrompt: "",
          profileId: profilesStore.activeProfileId,
          saveAudio: settings.saveAudio,
          outputFolder: settings.outputFolder || null,
          removeFillers: settings.removeFillers,
          britishEnglish: settings.britishEnglish,
          antiHallucination: settings.antiHallucination,
          formatMode: settings.formatMode,
          // Honour Settings → Audio → Microphone choice. Empty = auto-select.
          microphoneDevice: settings.microphoneDevice || null,
        },
        resultChannel,
        statusChannel,
      });
      sessionId = response.sessionId;

      startTime = Date.now();
      page.recording = true;
      page.status = "Recording...";
      page.statusColor = "#e87171";
      timerInterval = setInterval(updateTimer, 1000);

      // Start cue (brief item #20). Off by default; volume user-set.
      playStartCue(settings.soundCueVolume, settings.soundCues);

      // Preview overlay: if the user opted in, reset any leftover state
      // from a prior run and open the window when the main window is not
      // focused (i.e. the user is dictating into some other app).
      if (settings.transcriptionPreview && tauriRuntimeAvailable) {
        emit("preview-listening").catch(() => {});
        try {
          const focused = await getCurrentWindow().isFocused();
          if (!focused) {
            invoke("open_preview_window").catch(() => {});
          }
        } catch { /* best effort */ }
      }
    } catch (err) {
      error = typeof err === "string" ? err : err?.message || "Microphone error";
      // Surface the failure as a sticky error toast so it does not get
      // swallowed if the inline `error` panel is not visible.
      // (Day 3 of the upgrade plan)
      toasts.error("Could not start recording", error);
      page.status = "Error";
      page.statusColor = "#e87171";
      cleanup();
    }
  }

  async function stopRecording() {
    clearInterval(timerInterval);
    page.recording = false;
    transcribing = true;
    page.status = "Finalising...";
    page.statusColor = "#e8c86e";

    // Stop cue (brief item #20). "Recording over, finalising now."
    playStopCue(settings.soundCueVolume, settings.soundCues);

    try {
      const activityBeforeStop = lastLiveActivityAt;
      drainingSessionId = sessionId;
      const response = sessionId
        ? await invoke("stop_live_transcription_session", { sessionId })
        : null;
      await waitForResultDrain(activityBeforeStop);
      cleanup();

      if (processedChunks.size === 0 && !transcript.trim()) {
        page.status = "Ready";
        page.statusColor = "#7ec89a";
      } else {
        await finaliseTranscription(response?.audioPath || null);
      }
    } catch (err) {
      error = typeof err === "string" ? err : err?.message || "Failed to stop live transcription";
      page.status = "Error";
      page.statusColor = "#e87171";
      cleanup();
    } finally {
      transcribing = false;
    }
  }

  function cleanup() {
    sessionId = null;
    drainingSessionId = null;
    resultChannel = null;
    statusChannel = null;
  }

  function replaceSegmentsWithCleanedText(cleanedText) {
    if (!cleanedText.trim()) return;
    if (segments.length === 0) {
      segments = [{
        start: 0,
        end: Math.max((Date.now() - startTime) / 1000, 0),
        text: cleanedText,
      }];
      return;
    }
    segments = [{
      start: segments[0].start,
      end: segments[segments.length - 1].end,
      text: cleanedText,
    }];
  }

  async function cleanupTranscriptIfEnabled(text) {
    if (!text.trim()) return text;
    if (settings.aiTier === "off" || settings.formatMode === "Raw") return text;

    const llmLoaded = await invoke("get_llm_status").catch(() => false);
    if (!llmLoaded) return text;

    // Flip the sidebar LLM status chip to "generating" so the user can
    // see the cleanup pass in flight (brief item #31). Always paired
    // with markGenerationDone below — success or failure.
    markGenerating("Cleaning up");
    try {
      const cleaned = await invoke("cleanup_transcript_text_cmd", {
        transcript: text,
        profileId: profilesStore.activeProfileId,
        preset: settings.llmPromptPreset,
      });
      markGenerationDone(true);
      return cleaned?.trim() ? cleaned.trim() : text;
    } catch (err) {
      console.warn("LLM cleanup failed, keeping existing transcript", err);
      markGenerationDone(false, typeof err === "string" ? err : err?.message ?? null);
      return text;
    }
  }

  async function extractTasksForTranscript(text) {
    const llmLoaded = await invoke("get_llm_status").catch(() => false);
    if (settings.aiTier === "tasks" && llmLoaded) {
      markGenerating("Extracting tasks");
      try {
        const items = await invoke("extract_tasks_from_transcript_cmd", {
          transcript: text,
          profileId: profilesStore.activeProfileId,
        });
        markGenerationDone(true);
        return items.map((taskText) => ({ text: taskText }));
      } catch (err) {
        console.warn("LLM extract_tasks failed, falling back to regex", err);
        markGenerationDone(false, typeof err === "string" ? err : err?.message ?? null);
      }
    }

    return extractTasks(text);
  }

  async function waitForResultDrain(previousActivityAt = 0) {
    const firstMessageDeadline = Date.now() + 400;
    while (Date.now() < firstMessageDeadline) {
      if (lastLiveActivityAt > previousActivityAt) {
        break;
      }
      await new Promise((resolve) => setTimeout(resolve, 25));
    }

    if (lastLiveActivityAt <= previousActivityAt) {
      return;
    }

    const idleDeadline = Date.now() + 1500;
    while (Date.now() < idleDeadline) {
      if (Date.now() - lastLiveActivityAt >= 200) {
        return;
      }
      await new Promise((resolve) => setTimeout(resolve, 25));
    }
  }

  async function finaliseTranscription(audioPath = null) {
    if (transcript.trim()) {
      if (settings.transcriptionPreview && settings.aiTier !== "off" && settings.formatMode !== "Raw") {
        emit("preview-cleanup").catch(() => {});
      }
      const cleanedTranscript = await cleanupTranscriptIfEnabled(transcript);
      if (cleanedTranscript !== transcript) {
        transcript = cleanedTranscript;
        replaceSegmentsWithCleanedText(cleanedTranscript);
      }
      if (settings.transcriptionPreview) {
        emit("preview-final", { text: transcript }).catch(() => {});
      }

      if (settings.autoPaste) {
        try {
          const outcome = await invoke("paste_text", { text: transcript });
          if (!outcome?.pasted && outcome?.message) {
            toasts.warn(
              "Auto-paste unavailable",
              `${outcome.message}${outcome.copied ? ". Transcript is on your clipboard." : ""}`,
            );
          }
        } catch (err) {
          await navigator.clipboard
            .writeText(transcript)
            .catch(() => invoke("copy_to_clipboard", { text: transcript }).catch(() => {}));
          toasts.warn("Auto-paste failed", String(err));
        }
      } else if (settings.autoCopy) {
        navigator.clipboard.writeText(transcript).catch(() => {
          invoke("copy_to_clipboard", { text: transcript }).catch(() => {});
        });
      }

      const historyId = crypto.randomUUID();
      addToHistory({
        id: historyId,
        date: new Date().toLocaleString("en-GB"),
        source: "live",
        profileId: profilesStore.activeProfileId,
        preview: transcript.slice(0, 120),
        text: transcript,
        segments: segments,
        duration: (Date.now() - startTime) / 1000,
        engine: settings.engine,
        modelId: selectedModelId(),
        language: effectiveLanguage(),
        template: activeTemplate || undefined,
        audioPath,
      });

      // Extract tasks from transcript
      const extracted = await extractTasksForTranscript(transcript);
      extractedCount = extracted.length;
      for (const item of extracted) {
        addTask({
          text: item.text,
          bucket: "inbox",
          sourceTranscriptId: historyId,
        });
      }

      // Auto-open task sidebar if tasks were extracted
      if (extracted.length > 0) {
        page.taskSidebarOpen = true;
      }

      // Complete cue (brief item #20) — major-third arpeggio. Plays
      // after the full finalise flow so the user hears "done"
      // aligned with the history entry + paste landing.
      playCompleteCue(settings.soundCueVolume, settings.soundCues);

      saved = true;
      setTimeout(() => { saved = false; extractedCount = 0; }, 4000);
    } else if (settings.transcriptionPreview) {
      // No transcript to surface — dismiss the overlay rather than leaving
      // it stuck in "listening".
      emit("preview-hide").catch(() => {});
    }
    page.status = "Ready";
    page.statusColor = "#7ec89a";
    insertPos = -1;
  }

  function updateTimer() {
    const elapsed = Math.floor((Date.now() - startTime) / 1000);
    page.timerText = `${pad(Math.floor(elapsed / 60))}:${pad(elapsed % 60)}`;
  }

  function copyAll() {
    if (transcript) {
      navigator.clipboard.writeText(transcript).catch(() => {
        invoke("copy_to_clipboard", { text: transcript }).catch(() => {});
      });
    }
  }

  function clearTranscript() {
    transcript = "";
    segments = [];
    previousMeasuredContentHeight = 0;
    shouldPreserveScrollAnchor = false;
    shouldStickToBottom = false;
    page.timerText = "00:00";
    error = "";
    liveWarning = "";
    saved = false;
    insertPos = -1;
    activeTemplate = "";
  }

  async function handleExport(format) {
    if (!transcript) return;
    showExportMenu = false;
    const content = exportTranscript(transcript, segments, format);
    const extMap = { txt: "txt", vtt: "vtt", srt: "srt", md: "md", csv: "csv", html: "html" };
    const ext = extMap[format] || "txt";
    const defaultPath = `lumotia-${new Date().toISOString().slice(0, 10)}.${ext}`;

    if (hasTauriRuntime()) {
      try {
        const { save } = await import("@tauri-apps/plugin-dialog");
        const path = await save({
          title: `Export ${format.toUpperCase()} transcript`,
          defaultPath,
          filters: [{ name: format.toUpperCase(), extensions: [ext] }],
        });
        if (!path) return;
        await invoke("write_text_file_cmd", { path, contents: content });
        toasts.success(`Exported ${format.toUpperCase()} transcript`);
        return;
      } catch (err) {
        console.error("dictation export failed", err);
        error = `Export failed: ${typeof err === "string" ? err : err.message || err}`;
        return;
      }
    }

    const blob = new Blob([content], { type: "text/plain" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = defaultPath;
    a.click();
    URL.revokeObjectURL(url);
  }

  function applyTemplate(template) {
    activeTemplate = template.name;
    showTemplateMenu = false;
    transcript = template.sections.map((s) => `## ${s}\n\n`).join("\n");
    segments = [];
    previousMeasuredContentHeight = 0;
    // Position cursor at first section body
    requestAnimationFrame(() => {
      if (textareaEl) {
        const firstNewline = transcript.indexOf("\n\n") + 2;
        textareaEl.selectionStart = firstNewline;
        textareaEl.selectionEnd = firstNewline;
        textareaEl.focus();
      }
    });
  }

  async function manualExtractTasks() {
    if (!transcript.trim() || aiProcessing) return;
    aiProcessing = true;
    aiStatus = "Extracting tasks...";
    try {
      const extracted = await extractTasksForTranscript(transcript);
      for (const item of extracted) {
        addTask({ text: item.text, bucket: "inbox" });
      }
      aiStatus = `${extracted.length} task${extracted.length === 1 ? '' : 's'} extracted`;
    } catch (err) {
      aiStatus = typeof err === "string" ? err : "Extraction failed";
    } finally {
      aiProcessing = false;
      setTimeout(() => { aiStatus = ""; }, 4000);
    }
  }

  function saveTypedText() {
    if (!transcript.trim()) return;
    addToHistory({
      id: crypto.randomUUID(),
      date: new Date().toLocaleString("en-GB"),
      source: "typed",
      profileId: profilesStore.activeProfileId,
      preview: transcript.slice(0, 120),
      text: transcript,
      segments: [],
      duration: 0,
      language: settings.language,
    });
    saved = true;
    setTimeout(() => { saved = false; }, FEEDBACK_TIMEOUT_MS);
  }

  let taskCount = $derived(tasks.filter((t) => !t.done).length);
  let hasTranscript = $derived(transcript.trim().length > 0);

  let wordCount = $derived.by(() => {
    const trimmed = transcript.trim();
    return trimmed ? trimmed.split(/\s+/).length : 0;
  });

  // Pretext content height measurement (DOM-free).
  // Used for scroll-to-bottom affordance during live transcription.
  let textareaWidth = $state(0);
  let userScrolledUp = $state(false);
  let previousMeasuredContentHeight = $state(0);
  let shouldPreserveScrollAnchor = $state(false);
  let shouldStickToBottom = $state(false);

  // Build font string matching .font-transcript CSS
  let transcriptFont = $derived(transcriptPretextFont(prefs.accessibility));
  let transcriptLineHeight = $derived(transcriptPretextLineHeight(prefs.accessibility));

  let contentHeight = $derived.by(() => {
    if (!transcript.trim() || textareaWidth <= 0) return 0;
    // Subtract padding (p-6 = 24px each side)
    const innerWidth = textareaWidth - 48;
    if (innerWidth <= 0) return 0;
    return measurePreWrap(transcript, transcriptFont, innerWidth, transcriptLineHeight).height;
  });

  function onTextareaScroll(e) {
    const el = e.target;
    // "scrolled up" = more than 1 line from the bottom
    userScrolledUp = (el.scrollHeight - el.scrollTop - el.clientHeight) > transcriptLineHeight;
  }

  function scrollToBottom() {
    if (textareaEl) {
      textareaEl.scrollTop = textareaEl.scrollHeight;
      userScrolledUp = false;
    }
  }

  // Track textarea width for Pretext layout calculations
  $effect(() => {
    if (!textareaEl) return;
    const ro = new ResizeObserver(entries => {
      for (const entry of entries) textareaWidth = entry.contentRect.width;
    });
    ro.observe(textareaEl);
    return () => ro.disconnect();
  });

  $effect(() => {
    const measuredHeight = contentHeight;
    const previousHeight = previousMeasuredContentHeight;

    if (!textareaEl) {
      previousMeasuredContentHeight = measuredHeight;
      return;
    }

    if (shouldPreserveScrollAnchor && measuredHeight > previousHeight) {
      const delta = measuredHeight - previousHeight;
      requestAnimationFrame(() => {
        if (textareaEl) {
          textareaEl.scrollTop += delta;
        }
      });
      shouldPreserveScrollAnchor = false;
    } else if (shouldStickToBottom) {
      requestAnimationFrame(() => {
        if (textareaEl) {
          textareaEl.scrollTop = textareaEl.scrollHeight;
        }
      });
      shouldStickToBottom = false;
    }

    previousMeasuredContentHeight = measuredHeight;
  });

  let showScrollHint = $derived(page.recording && userScrolledUp && contentHeight > 0);

  let reduceMotion = $derived(
    prefs.accessibility.reduceMotion === 'on'
    || (prefs.accessibility.reduceMotion === 'system'
        && typeof window !== 'undefined'
        && window.matchMedia('(prefers-reduced-motion: reduce)').matches)
  );

  let transcriptionFailed = $state(false);

  // Screen-reader announcer for recording state transitions.
  // Announces both start and stop, then clears so the live region does not
  // hold stale text. prevRecording is a plain let (not $state) because it is
  // bookkeeping and must not re-trigger the effect.
  let recordingAnnouncement = $state("");
  let prevRecording = false;
  $effect(() => {
    if (page.recording && !prevRecording) {
      recordingAnnouncement = "Recording started";
    } else if (!page.recording && prevRecording) {
      recordingAnnouncement = "Recording stopped";
      setTimeout(() => { recordingAnnouncement = ""; }, 1200);
    }
    prevRecording = page.recording;
  });
</script>

<div class="flex flex-col h-full animate-fade-in">
  <div aria-live="assertive" aria-atomic="true" class="sr-only" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);">
    {recordingAnnouncement}
  </div>
  {#if needsDownload}
    <ModelDownloader
      engine={settings.engine}
      modelSize={settings.modelSize.toLowerCase()}
      onComplete={onModelDownloaded}
    />
  {:else}
    <!-- Control strip -->
    <div class="flex items-center gap-3 px-5 h-[56px] border-b border-border-subtle flex-shrink-0">
      <!-- Record button -->
      <button
        class="relative flex items-center justify-center w-[48px] h-[48px] min-w-[48px] flex-shrink-0 rounded-full text-white
          {page.recording
            ? 'bg-danger animate-pulse-warm'
            : modelLoading
              ? 'bg-warning opacity-60 cursor-wait'
              : !tauriRuntimeAvailable
                ? 'bg-bg-elevated text-text-tertiary cursor-not-allowed'
                : 'bg-accent hover:bg-accent-hover shadow-[var(--shadow-accent-lg)]'}"
        onclick={toggleRecording}
        disabled={modelLoading || !tauriRuntimeAvailable}
        aria-label={page.recording ? "Stop recording" : "Start recording"}
      >
        {#if page.recording}
          <span class="w-[16px] h-[16px] rounded-[3px] bg-white"></span>
        {:else if modelLoading}
          <Loader2 size={18} class="animate-spin" aria-hidden="true" />
        {:else}
          <span class="w-[16px] h-[16px] rounded-full bg-white/90"></span>
        {/if}
      </button>
      <span class="text-[12px] font-medium flex-shrink-0 {page.recording ? 'text-danger' : 'text-text-secondary'}">
        {page.recording ? 'Stop' : modelLoading ? 'Loading' : 'Record'}
      </span>

      <!-- Waveform placeholder (Phase 2: live visualiser) -->
      <div class="flex-1 min-h-[32px] rounded-md border border-border-subtle bg-bg-elevated/40 flex items-center px-3">
        {#if page.recording}
          <span class="flex items-end gap-[3px] h-[18px]">
            {#each [0.4, 0.8, 0.5, 1, 0.6, 0.9, 0.4, 0.7, 0.5, 0.8, 0.3, 0.6] as h, i}
              {#if reduceMotion}
                <span
                  class="w-[2px] rounded-full bg-danger"
                  style="height: 50%"
                ></span>
              {:else}
                <span
                  class="w-[2px] rounded-full bg-danger/70 animate-pulse-soft"
                  style="height: {h * 100}%; animation-delay: {i * 60}ms"
                ></span>
              {/if}
            {/each}
          </span>
        {:else}
          <span class="text-[12px] text-text-secondary">
            {#if modelLoading}
              Loading model...
            {:else if !tauriRuntimeAvailable}
              Desktop app required for local transcription
            {:else if saved}
              <span class="text-success animate-fade-in">
                Saved{#if extractedCount > 0} · {extractedCount} task{extractedCount === 1 ? '' : 's'} extracted{/if}
              </span>
            {:else}
              Press record or <kbd class="px-1 py-0.5 rounded bg-bg-elevated text-[12px] text-text-secondary border border-border-subtle">{settings.globalHotkey}</kbd>
            {/if}
          </span>
        {/if}
      </div>

      <!-- Timer -->
      <span class="text-[20px] font-bold text-text tabular-nums tracking-tight leading-none flex-shrink-0" style="font-variant-numeric: tabular-nums;">
        {page.timerText}
      </span>

      <!-- Status indicator -->
      <span class="text-[12px] text-text-secondary flex-shrink-0 min-w-[60px] text-right">
        {#if page.recording}
          <span class="inline-flex items-center gap-1.5">
            <span class="w-[6px] h-[6px] rounded-full bg-danger animate-pulse-soft"></span>
            <span class="font-medium text-danger tracking-wide">REC</span>
          </span>
        {:else if modelLoading}
          <span class="text-warning">Loading</span>
        {:else}
          <span class="text-success">Ready</span>
        {/if}
      </span>

      <!-- Separator -->
      <span class="w-px h-4 bg-border-subtle flex-shrink-0"></span>

      <!-- Task sidebar toggle -->
      <button
        class="relative px-2 py-1.5 rounded-lg flex-shrink-0
          {page.taskSidebarOpen ? 'bg-accent/10' : 'hover:bg-hover'}"
        onclick={() => page.taskSidebarOpen = !page.taskSidebarOpen}
        aria-label="Toggle task sidebar"
      >
        <span class="flex items-center gap-1.5">
          <SquareCheck size={16} class={page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'} aria-hidden="true" />
          <span class="text-[12px] {page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}">Tasks</span>
        </span>
        {#if taskCount > 0}
          <span class="absolute -top-0.5 -right-0.5 text-[12px] px-1 py-0 rounded-full bg-accent text-white font-medium min-w-[14px] text-center leading-[14px]">
            {taskCount}
          </span>
        {/if}
      </button>
    </div>

    <!-- Toolbar -->
    <div class="flex items-center justify-end gap-1 px-5 py-2 border-b border-border-subtle flex-shrink-0">
      <!-- Template selector -->
      {#if templates.length > 0}
        <div class="relative">
          <button
            class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
            onclick={() => { showTemplateMenu = !showTemplateMenu; showExportMenu = false; }}
          >Template</button>
          {#if showTemplateMenu}
            <div class="absolute right-0 top-full mt-1 bg-bg-card border border-border rounded-lg shadow-lg py-1 z-10 animate-fade-in min-w-[160px]">
              {#each templates as template}
                <button
                  class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text
                    {activeTemplate === template.name ? 'text-accent' : ''}"
                  onclick={() => applyTemplate(template)}
                >{template.name}</button>
              {/each}
            </div>
          {/if}
        </div>
      {/if}
      <button
        class="btn-md rounded-lg flex-shrink-0 whitespace-nowrap {aiProcessing ? 'text-warning' : 'text-accent hover:bg-accent/10 hover:text-accent font-medium'}"
        onclick={manualExtractTasks}
        disabled={aiProcessing || !transcript.trim()}
      >{aiProcessing ? "Extracting..." : "Extract Tasks"}</button>
      {#if hasTranscript || page.recording}
        <div class="flex items-center gap-1 animate-fade-in">
          <span class="w-px h-4 bg-border-subtle flex-shrink-0"></span>
          <button
            class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap
              {!transcript.trim() ? 'opacity-40 cursor-default' : ''}"
            onclick={saveTypedText}
            disabled={!transcript.trim()}
          >Save</button>
          <button
            class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
            onclick={copyAll}
          >Copy</button>
          <button
            class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
            onclick={clearTranscript}
          >Clear</button>
          <div class="relative flex-shrink-0">
            <button
              class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text whitespace-nowrap"
              onclick={() => { showExportMenu = !showExportMenu; showTemplateMenu = false; }}
            >Export</button>
            {#if showExportMenu}
              <div class="absolute right-0 top-full mt-1 bg-bg-card border border-border rounded-lg shadow-lg py-1 z-10 animate-fade-in min-w-[120px]">
                {#each [["txt", "Plain Text"], ["md", "Markdown"], ["csv", "CSV"], ["html", "HTML"], ["srt", "SRT Subtitles"], ["vtt", "WebVTT"]] as [fmt, label]}
                  <button
                    class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text"
                    onclick={() => handleExport(fmt)}
                  >{label}</button>
                {/each}
              </div>
            {/if}
          </div>
        </div>
      {/if}
    </div>

    <!-- Template indicator -->
    {#if activeTemplate}
      <div class="px-5 pt-2 animate-fade-in flex-shrink-0">
        <div class="flex items-center gap-2 px-4 py-1.5 rounded-lg bg-accent-subtle border border-accent/20">
          <span class="text-[12px] text-accent font-medium">Template: {activeTemplate}</span>
          <span class="text-[12px] text-text-secondary">Click a section, then record to fill it</span>
          <div class="flex-1"></div>
          <button class="text-[12px] text-text-secondary hover:text-text" onclick={() => activeTemplate = ""}>Remove</button>
        </div>
      </div>
    {/if}

    <!-- Error -->
    {#if error}
      <div class="px-5 pt-2 animate-fade-in flex-shrink-0">
        <div class="px-4 py-2 rounded-lg bg-danger/10 border border-danger/20 text-[12px] text-danger">
          {error}
        </div>
      </div>
    {/if}

    {#if liveWarning && !error}
      <div class="px-5 pt-2 animate-fade-in flex-shrink-0">
        <div class="px-4 py-2 rounded-lg bg-warning/10 border border-warning/20 text-[12px] text-warning">
          {liveWarning}
        </div>
      </div>
    {/if}

    <!-- Transcript area -->
    <div class="flex-1 px-5 pt-3 pb-3 min-h-0" style="--text-transcript: {prefs.accessibility.transcriptSize}px; font-size: var(--text-transcript)">
      <Card classes="h-full flex flex-col">
        {#if transcriptionFailed && !transcript.trim() && !page.recording}
          <div class="flex-1 flex items-center justify-center">
            <EmptyState icon={AlertTriangle} message="Something went wrong with that transcription. Your audio is saved, try again when you're ready." />
          </div>
        {:else if !transcript.trim() && !page.recording && !transcribing}
          <div class="flex-1 flex items-center justify-center">
            <EmptyState icon={Mic} headline="Talk now, think later." message={`Press record, or ${settings.globalHotkey}.`} />
          </div>
          <textarea
            bind:this={textareaEl}
            class="font-transcript flex-1 w-full bg-transparent text-text p-6
                   resize-none focus:outline-none placeholder:text-text-tertiary min-h-0 hidden"
            placeholder={activeTemplate ? "Click a section above, then press record..." : "Your words will appear here..."}
            bind:value={transcript}
            onclick={() => { showExportMenu = false; showTemplateMenu = false; }}
            data-no-transition
            aria-label="Transcript"
            aria-live="polite"
          ></textarea>
        {:else}
          <div class="relative flex-1 min-h-0">
            <textarea
              bind:this={textareaEl}
              class="font-transcript h-full w-full bg-transparent text-text p-6
                     resize-none focus:outline-none placeholder:text-text-tertiary"
              placeholder={activeTemplate ? "Click a section above, then press record..." : "Your words will appear here..."}
              bind:value={transcript}
              onscroll={onTextareaScroll}
              onclick={() => { showExportMenu = false; showTemplateMenu = false; }}
              data-no-transition
              aria-label="Transcript"
              aria-live="polite"
            ></textarea>
            {#if showScrollHint}
              <button
                class="absolute bottom-3 right-6 px-3 py-1.5 rounded-full bg-accent text-white text-[12px] font-medium shadow-md hover:bg-accent-hover animate-fade-in"
                onclick={scrollToBottom}
                aria-label="Scroll to latest"
                style="transition-duration: var(--duration-ui)"
              >
                ↓ New text
              </button>
            {/if}
          </div>
        {/if}

        <!-- Status footer (inside transcript card) -->
        <div class="flex items-center justify-between px-6 pb-3 pt-1 flex-shrink-0">
          <span class="text-[12px] text-text-secondary">
            {#if wordCount > 0}
              {wordCount} {wordCount === 1 ? 'word' : 'words'}
            {:else}
              0 words
            {/if}
            {#if insertPos >= 0 && page.recording}
              · <span class="text-accent">inserting at cursor</span>
            {/if}
          </span>
          <div class="flex items-center gap-2">
            {#if aiStatus}
              <span class="text-[12px] text-accent animate-fade-in">{aiStatus}</span>
              <span class="text-[12px] text-text-secondary">·</span>
            {/if}
            <span class="text-[12px] text-text-secondary">
              {settings.formatMode} · {page.activeProfile === "None" ? "No profile" : page.activeProfile}
            </span>
            {#if transcript.trim()}
              <SpeakerButton text={transcript} label="Read transcript aloud" size={12} />
            {/if}
          </div>
        </div>
      </Card>
    </div>
  {/if}
</div>
