import { invoke } from "@tauri-apps/api/core";
import type {
  EnergyLevel,
  PageState,
  Profile,
  Segment,
  SettingsState,
  TaskDraft,
  TaskDto,
  TaskEntry,
  TaskList,
  TaskUpdate,
  Template,
  TranscriptDto,
  TranscriptEntry,
  TranscriptMetaPatch,
  TranscriptWriteEntry,
} from "$lib/types/app";
import { toasts } from "$lib/stores/toasts.svelte.ts";
import { errorMessage } from "$lib/utils/errors.js";
import { parseStoredJson } from "$lib/utils/storage.js";
import {
  loadSettingsWithMigration,
  saveSettingsWithVersion,
} from "$lib/utils/settingsMigrations.js";
import { hasTauriRuntime } from "$lib/utils/runtime.js";

export const page = $state<PageState>({
  current: "dictation",
  status: "Ready",
  statusColor: "#7ec89a",
  activeProfile: "None",
  recording: false,
  timerText: "00:00",
  handedness: "Right",
  taskSidebarOpen: false,
});

import { migrateLocalStorageKeys } from "$lib/utils/localStorageMigration";

const SETTINGS_KEY = "lumotia_settings";
const PROFILES_KEY = "lumotia_profiles";
const TASK_LISTS_KEY = "lumotia_task_lists";
const TEMPLATES_KEY = "lumotia_templates";

// One-shot key rename from the magnotia era. Idempotent. Runs once at
// module load before any localStorage read below.
migrateLocalStorageKeys([
  ["magnotia_settings", SETTINGS_KEY],
  ["magnotia_profiles", PROFILES_KEY],
  ["magnotia_task_lists", TASK_LISTS_KEY],
  ["magnotia_templates", TEMPLATES_KEY],
]);

const defaults: SettingsState = {
  engine: "whisper",
  modelSize: "Base",
  language: "en",
  device: "auto",
  formatMode: "Smart",
  removeFillers: true,
  antiHallucination: true,
  britishEnglish: true,
  autoCopy: true,
  autoPaste: false,
  transcriptionPreview: false,
  meetingAutoCapture: false,
  meetingAutoCaptureApps: ["zoom", "teams"],
  soundCues: false,
  soundCueVolume: 0.15,
  includeTimestamps: true,
  theme: "Dark",
  fontSize: 14,
  aiTier: "cleanup",
  llmModelId: null,
  llmPromptPreset: "default",
  aiGpuConcurrency: "parallel",
  prewarmModelOnStartup: false,
  saveAudio: false,
  outputFolder: "",
  globalHotkey: "Ctrl+Shift+R",
  sidebarCollapsed: false,
  microphoneDevice: "",
  currentEnergy: null,
  matchMyEnergy: false,
  ttsVoice: null,
  ttsRate: 1.0,
  ritualsMorning: false,
  ritualsMorningTime: "08:00",
  ritualsEvening: false,
  launchAtLogin: false,
  ritualsPromptSeen: false,
  nudgesEnabled: false,
  nudgesMuted: false,
  nudgesSpeakAloud: false,
  showMomentumSparkline: true,
};

function canUseStorage(): boolean {
  return typeof localStorage !== "undefined";
}

function loadSettings(): SettingsState {
  if (!canUseStorage()) return { ...defaults };
  // Versioned migration: reads the envelope or an unversioned legacy
  // blob, runs the migration chain (currently identity: bare object ->
  // v1), and returns the current-shape data. Falls back to defaults
  // on corruption — the toast below surfaces the reset to the user.
  const migrated = loadSettingsWithMigration<Partial<SettingsState>>(SETTINGS_KEY);
  if (migrated === null && localStorage.getItem(SETTINGS_KEY) !== null) {
    // There was data but it failed to migrate — tell the user we
    // reset rather than silently mangling their preferences.
    queueMicrotask(() => {
      toasts.warn(
        "Settings reset",
        "Your saved settings couldn't be read, so Lumotia fell back to defaults. "
          + "Earlier Lumotia builds can still read your old data.",
      );
    });
  }
  return {
    ...defaults,
    ...(migrated ?? {}),
  };
}

export const settings = $state<SettingsState>(loadSettings());

export function saveSettings() {
  if (!canUseStorage()) return;
  // Versioned envelope {version, data}. Older Lumotia builds that used
  // the bare-object layout can still spread the `data` key over
  // their defaults — they will just silently drop fields they don't
  // know about, which is the same behaviour as today.
  saveSettingsWithVersion(SETTINGS_KEY, { ...settings });
}

function loadProfiles(): Profile[] {
  if (!canUseStorage()) return [];
  return parseStoredJson<Profile[]>(localStorage.getItem(PROFILES_KEY)) ?? [];
}

export const profiles = $state<Profile[]>(loadProfiles());

export function saveProfiles() {
  if (!canUseStorage()) return;
  try {
    localStorage.setItem(PROFILES_KEY, JSON.stringify(profiles));
  } catch {}
}

export const history = $state<TranscriptEntry[]>([]);

export function mapTranscriptRow(row: TranscriptDto): TranscriptEntry {
  const text = row.text ?? "";
  const rawTags = row.manualTags ?? "";
  const manualTags = rawTags ? rawTags.split(",").filter(Boolean) : [];
  const rawLlmTags = row.llmTags ?? "";
  const llmTags = rawLlmTags ? rawLlmTags.split(",").filter(Boolean) : [];
  const rawSegments = row.segmentsJson ?? "";
  let segments: Segment[] = [];
  if (rawSegments) {
    try {
      const parsed = JSON.parse(rawSegments);
      if (Array.isArray(parsed)) segments = parsed as Segment[];
    } catch (err) {
      console.warn("mapTranscriptRow: segmentsJson parse failed", err);
    }
  }

  return {
    id: row.id,
    text,
    source: row.source ?? "",
    profileId: row.profileId ?? "00000000-0000-0000-0000-000000000001",
    title: row.title ?? "",
    audioPath: row.audioPath ?? null,
    duration: Number(row.duration ?? 0),
    engine: row.engine ?? null,
    modelId: row.modelId ?? null,
    createdAt: row.createdAt ?? null,
    date: row.createdAt ? new Date(row.createdAt).toLocaleString("en-GB") : "",
    preview: text.slice(0, 120),
    starred: !!row.starred,
    manualTags,
    template: row.template ?? "",
    language: row.language ?? "",
    segments,
    llmTags,
  };
}

function normaliseTranscriptEntry(entry: TranscriptWriteEntry): TranscriptEntry {
  const createdAt = entry.createdAt ?? new Date().toISOString();
  return {
    id: String(entry.id),
    text: entry.text ?? "",
    source: entry.source ?? "microphone",
    profileId: entry.profileId ?? "00000000-0000-0000-0000-000000000001",
    title: entry.title ?? "",
    audioPath: entry.audioPath ?? null,
    duration: Number(entry.duration ?? 0),
    engine: entry.engine ?? null,
    modelId: entry.modelId ?? null,
    createdAt,
    date: entry.date ?? new Date(createdAt).toLocaleString("en-GB"),
    preview: entry.preview ?? entry.text.slice(0, 120),
    starred: !!entry.starred,
    manualTags: Array.isArray(entry.manualTags) ? entry.manualTags : [],
    template: entry.template ?? "",
    language: entry.language ?? "",
    segments: Array.isArray(entry.segments) ? entry.segments : [],
    llmTags: Array.isArray(entry.llmTags) ? entry.llmTags : [],
  };
}

async function loadHistory() {
  if (!hasTauriRuntime()) {
    history.length = 0;
    return;
  }

  try {
    const rows = await invoke<TranscriptDto[]>("list_transcripts", { limit: 500, offset: 0 });
    history.splice(0, history.length, ...(Array.isArray(rows) ? rows.map(mapTranscriptRow) : []));
  } catch (err) {
    console.error("loadHistory failed", err);
    history.length = 0;
    toasts.error("Failed to load history", errorMessage(err));
  }
}

if (typeof window !== "undefined" && hasTauriRuntime()) {
  loadHistory().catch(() => {});
}

export async function addToHistory(entry: TranscriptWriteEntry) {
  const normalised = normaliseTranscriptEntry(entry);
  history.unshift(normalised);
  if (history.length > 500) history.length = 500;

  if (!hasTauriRuntime()) return;

  try {
    await invoke("add_transcript", {
      transcript: {
        id: normalised.id,
        text: normalised.text,
        source: normalised.source,
        profileId: normalised.profileId,
        title: normalised.title || null,
        audioPath: normalised.audioPath ?? null,
        duration: normalised.duration,
        engine: normalised.engine ?? null,
        modelId: normalised.modelId ?? null,
        inferenceMs: entry.inferenceMs ?? null,
        sampleRate: entry.sampleRate ?? null,
        audioChannels: entry.audioChannels ?? null,
        formatMode: entry.formatMode ?? null,
        removeFillers: !!entry.removeFillers,
        britishEnglish: !!entry.britishEnglish,
        antiHallucination: !!entry.antiHallucination,
      },
    });
  } catch (err) {
    console.warn("addToHistory: SQLite write failed, entry will not persist past restart", err);
  }
}

export async function renameHistoryEntry(
  id: string,
  updates: { title?: string; text?: string },
) {
  const idx = history.findIndex((entry) => String(entry.id) === String(id));
  if (idx >= 0) {
    if (updates.title !== undefined) history[idx].title = updates.title;
    if (updates.text !== undefined) history[idx].text = updates.text;
  }

  if (!hasTauriRuntime()) return;

  try {
    await invoke("update_transcript", {
      id: String(id),
      text: updates.text ?? null,
      title: updates.title ?? null,
    });
  } catch (err) {
    console.warn("renameHistoryEntry: SQLite update failed", err);
    throw err;
  }
}

export async function saveTranscriptMeta(id: string, patch: TranscriptMetaPatch) {
  if (!hasTauriRuntime()) return;

  try {
    const payload = {
      starred: patch.starred,
      manualTags: patch.manualTags == null
        ? undefined
        : (Array.isArray(patch.manualTags)
            ? patch.manualTags.join(",")
            : String(patch.manualTags)),
      template: patch.template,
      language: patch.language,
      segmentsJson: patch.segments === undefined ? undefined : JSON.stringify(patch.segments),
      llmTags: patch.llmTags == null
        ? undefined
        : (Array.isArray(patch.llmTags)
            ? patch.llmTags.join(",")
            : String(patch.llmTags)),
    };
    const row = await invoke<TranscriptDto>("update_transcript_meta_cmd", {
      id: String(id),
      patch: payload,
    });
    const idx = history.findIndex((entry) => String(entry.id) === String(id));
    if (idx !== -1) history[idx] = mapTranscriptRow(row);
  } catch (err) {
    toasts.error("Failed to save transcript metadata", errorMessage(err));
  }
}

export function deleteFromHistory(index: number) {
  const entry = history[index];
  history.splice(index, 1);

  if (!hasTauriRuntime() || !entry?.id) return;

  invoke("delete_transcript", { id: String(entry.id) })
    .catch((err) => console.warn("deleteFromHistory: SQLite delete failed", err));
}

export function deleteFromHistoryById(id: string) {
  const idx = history.findIndex((entry) => String(entry.id) === String(id));
  if (idx === -1) return;
  deleteFromHistory(idx);
}

export const tasks = $state<TaskEntry[]>([]);

function mapTaskRow(row: TaskDto): TaskEntry {
  return {
    id: row.id,
    text: row.text ?? "",
    bucket: (row.bucket ?? "inbox") as TaskEntry["bucket"],
    listId: row.listId ?? null,
    effort: row.effort ?? "",
    notes: row.notes ?? "",
    done: !!row.done,
    doneAt: row.doneAt ?? null,
    createdAt: row.createdAt ?? new Date().toISOString(),
    sourceTranscriptId: row.sourceTranscriptId ?? null,
    parentTaskId: row.parentTaskId ?? null,
    energy: row.energy ?? null,
  };
}

async function loadTasks() {
  if (!hasTauriRuntime()) {
    tasks.length = 0;
    return;
  }

  try {
    const rows = await invoke<TaskDto[]>("list_tasks_cmd");
    tasks.splice(0, tasks.length, ...(Array.isArray(rows) ? rows.map(mapTaskRow) : []));
  } catch (err) {
    console.error("loadTasks failed", err);
    tasks.length = 0;
    toasts.error("Failed to load tasks", errorMessage(err));
  }
}

if (typeof window !== "undefined" && hasTauriRuntime()) {
  loadTasks().catch(() => {});
}

export function saveTasks() {}

export async function addTask(task: TaskDraft) {
  if (!hasTauriRuntime()) return;

  const id = crypto.randomUUID();
  try {
    const row = await invoke<TaskDto>("create_task_cmd", {
      request: {
        id,
        text: task.text,
        bucket: task.bucket || "inbox",
        sourceTranscriptId: task.sourceTranscriptId || null,
        listId: task.listId ?? null,
        effort: task.effort ?? null,
        energy: task.energy ?? null,
      },
    });
    tasks.unshift(mapTaskRow(row));
    broadcastTasks();
  } catch (err) {
    toasts.error("Couldn't add task", errorMessage(err));
  }
}

function applyLocalTaskUpdate(id: string, updates: Partial<TaskEntry>) {
  const idx = tasks.findIndex((task) => task.id === id);
  if (idx >= 0) {
    Object.assign(tasks[idx], updates);
    broadcastTasks();
  }
}

export async function updateTask(id: string, updates: TaskUpdate) {
  if (!hasTauriRuntime()) {
    applyLocalTaskUpdate(id, updates as Partial<TaskEntry>);
    return;
  }

  try {
    const row = await invoke<TaskDto>("update_task_cmd", {
      id,
      patch: {
        text: updates.text ?? null,
        bucket: updates.bucket ?? null,
        listId: updates.listId ?? null,
        effort: updates.effort ?? null,
        notes: updates.notes ?? null,
      },
    });
    const idx = tasks.findIndex((task) => task.id === id);
    if (idx >= 0) {
      tasks[idx] = mapTaskRow(row);
      broadcastTasks();
    }
  } catch (err) {
    toasts.error("Couldn't update task", errorMessage(err));
  }
}

/**
 * Phase 3: explicit tri-state energy setter. Lives outside `updateTask`
 * because the backend uses a dedicated command for clearing — see
 * `set_task_energy_cmd` in src-tauri/src/commands/tasks.rs. Pass `null`
 * to clear the tag entirely; pass an `EnergyLevel` string to set it.
 */
export async function setTaskEnergy(id: string, energy: EnergyLevel | null) {
  if (!hasTauriRuntime()) {
    applyLocalTaskUpdate(id, { energy });
    return;
  }
  try {
    const row = await invoke<TaskDto>("set_task_energy_cmd", { id, energy });
    const idx = tasks.findIndex((task) => task.id === id);
    if (idx >= 0) {
      tasks[idx] = mapTaskRow(row);
      broadcastTasks();
    }
  } catch (err) {
    toasts.error("Couldn't change energy", errorMessage(err));
  }
}

export async function deleteTask(id: string) {
  if (!hasTauriRuntime()) return;

  try {
    await invoke("delete_task_cmd", { id });
    const idx = tasks.findIndex((task) => task.id === id);
    if (idx >= 0) {
      tasks.splice(idx, 1);
      broadcastTasks();
      if (typeof window !== "undefined") {
        window.dispatchEvent(new CustomEvent("lumotia:task-deleted", { detail: { id } }));
      }
    }
  } catch (err) {
    toasts.error("Couldn't delete task", errorMessage(err));
  }
}

export async function completeTask(id: string) {
  if (!hasTauriRuntime()) return;

  try {
    await invoke("complete_task_cmd", { id });
    applyLocalTaskUpdate(id, { done: true, doneAt: new Date().toISOString() });
    // Phase 6 nudge-bus signal. Fire-and-forget; the bus subscribes
    // to this to clear micro-step-idle timers for the completed task
    // and, later, to drive Phase 7 "after a task completes" rules.
    if (typeof window !== "undefined") {
      window.dispatchEvent(new CustomEvent("lumotia:task-completed", { detail: { id } }));
    }
  } catch (err) {
    toasts.error("Couldn't complete task", errorMessage(err));
  }
}

export async function uncompleteTask(id: string) {
  if (!hasTauriRuntime()) return;

  try {
    await invoke("uncomplete_task_cmd", { id });
    applyLocalTaskUpdate(id, { done: false, doneAt: null });
    if (typeof window !== "undefined") {
      window.dispatchEvent(new CustomEvent("lumotia:task-uncompleted", { detail: { id } }));
    }
  } catch (err) {
    toasts.error("Couldn't uncomplete task", errorMessage(err));
  }
}

interface TaskChannelMessage {
  type: "tasks_updated";
  tasks: TaskEntry[];
}

const taskChannel = typeof BroadcastChannel !== "undefined"
  ? new BroadcastChannel("lumotia_task_sync")
  : null;

if (taskChannel) {
  taskChannel.onmessage = (event: MessageEvent<TaskChannelMessage>) => {
    if (event.data.type === "tasks_updated") {
      tasks.length = 0;
      tasks.push(...event.data.tasks);
    }
  };
}

function broadcastTasks() {
  if (!taskChannel) return;
  taskChannel.postMessage({
    type: "tasks_updated",
    tasks: $state.snapshot(tasks),
  } satisfies TaskChannelMessage);
}

function defaultTaskLists(): TaskList[] {
  return [
    { id: "all", name: "All Tasks", builtIn: true, createdAt: null },
    { id: "inbox", name: "Inbox", builtIn: true, createdAt: null },
  ];
}

function loadTaskLists(): TaskList[] {
  if (!canUseStorage()) return defaultTaskLists();
  return parseStoredJson<TaskList[]>(localStorage.getItem(TASK_LISTS_KEY)) ?? defaultTaskLists();
}

export const taskLists = $state<TaskList[]>(loadTaskLists());

export function saveTaskLists() {
  if (canUseStorage()) {
    try {
      localStorage.setItem(TASK_LISTS_KEY, JSON.stringify(taskLists));
    } catch {}
  }
  broadcastTaskLists();
}

export function addTaskList(name: string, profileId: string | null = null) {
  taskLists.push({
    id: crypto.randomUUID(),
    name,
    builtIn: false,
    profileId,
    createdAt: new Date().toISOString(),
  });
  saveTaskLists();
}

export function addProfileTaskList(profileName: string) {
  const existing = taskLists.find((list) => list.profileId === profileName);
  if (existing) return existing.id;

  const id = crypto.randomUUID();
  taskLists.push({
    id,
    name: profileName,
    builtIn: false,
    profileId: profileName,
    createdAt: new Date().toISOString(),
  });
  saveTaskLists();
  return id;
}

export function removeProfileTaskList(profileName: string) {
  const idx = taskLists.findIndex((list) => list.profileId === profileName);
  if (idx < 0) return;

  const listId = taskLists[idx].id;
  for (const task of tasks) {
    if (task.listId === listId) task.listId = null;
  }
  saveTasks();
  broadcastTasks();
  taskLists.splice(idx, 1);
  saveTaskLists();
}

export function moveTaskToList(taskId: string, listId: string) {
  updateTask(taskId, { listId: listId === "inbox" || listId === "all" ? null : listId });
}

export function moveTaskListToProfile(listId: string, profileId: string | null) {
  const list = taskLists.find((entry) => entry.id === listId);
  if (list && !list.builtIn) {
    list.profileId = profileId || null;
    if (profileId && !list.name) list.name = profileId;
    saveTaskLists();
  }
}

export function renameTaskList(id: string, name: string) {
  const list = taskLists.find((entry) => entry.id === id);
  if (list && !list.builtIn) {
    list.name = name;
    saveTaskLists();
  }
}

export function deleteTaskList(id: string) {
  const idx = taskLists.findIndex((list) => list.id === id);
  if (idx < 0 || taskLists[idx].builtIn) return;

  for (const task of tasks) {
    if (task.listId === id) task.listId = null;
  }
  saveTasks();
  broadcastTasks();
  taskLists.splice(idx, 1);
  saveTaskLists();
}

export function moveTaskList(id: string, direction: number) {
  const idx = taskLists.findIndex((list) => list.id === id);
  const targetIdx = idx + direction;
  if (idx < 0 || targetIdx < 0 || targetIdx >= taskLists.length) return;
  if (taskLists[targetIdx].builtIn) return;
  [taskLists[idx], taskLists[targetIdx]] = [taskLists[targetIdx], taskLists[idx]];
  saveTaskLists();
}

export function sortTaskLists(mode: "alpha" | "date") {
  const builtIn = taskLists.filter((list) => list.builtIn);
  const custom = taskLists.filter((list) => !list.builtIn);

  if (mode === "alpha") {
    custom.sort((a, b) => a.name.localeCompare(b.name));
  } else {
    custom.sort(
      (a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime(),
    );
  }

  taskLists.length = 0;
  taskLists.push(...builtIn, ...custom);
  saveTaskLists();
}

interface TaskListChannelMessage {
  type: "task_lists_updated";
  lists: TaskList[];
}

const taskListChannel = typeof BroadcastChannel !== "undefined"
  ? new BroadcastChannel("lumotia_task_lists")
  : null;

if (taskListChannel) {
  taskListChannel.onmessage = (event: MessageEvent<TaskListChannelMessage>) => {
    if (event.data.type === "task_lists_updated") {
      taskLists.length = 0;
      taskLists.push(...event.data.lists);
    }
  };
}

function broadcastTaskLists() {
  if (!taskListChannel) return;
  taskListChannel.postMessage({
    type: "task_lists_updated",
    lists: $state.snapshot(taskLists),
  } satisfies TaskListChannelMessage);
}

function defaultTemplates(): Template[] {
  return [
    { name: "Meeting Notes", sections: ["Attendees", "Agenda", "Discussion", "Action Items", "Next Steps"] },
    { name: "Report", sections: ["Summary", "Background", "Findings", "Recommendations"] },
    { name: "Interview", sections: ["Candidate", "Role", "Questions & Answers", "Assessment", "Next Steps"] },
  ];
}

function loadTemplates(): Template[] {
  if (!canUseStorage()) return defaultTemplates();
  return parseStoredJson<Template[]>(localStorage.getItem(TEMPLATES_KEY)) ?? defaultTemplates();
}

export const templates = $state<Template[]>(loadTemplates());

export function saveTemplates() {
  if (!canUseStorage()) return;
  try {
    localStorage.setItem(TEMPLATES_KEY, JSON.stringify(templates));
  } catch {}
}
