/**
 * Workspace Pinia store.
 * Single source of truth for workspace state across all components.
 */

import { invoke } from "@tauri-apps/api/core"
import { listen } from "@tauri-apps/api/event"
import { open } from "@tauri-apps/plugin-dialog"
import type Database from "@tauri-apps/plugin-sql"
import { defineStore } from "pinia"
import { computed, ref } from "vue"
import { useFileSystem } from "~/composables/useFileSystem"
import { useSettings } from "~/composables/useSettings"
import {
  fullReindex,
  indexFile as indexFileInDb,
  initIndex,
  removeFile,
  syncIndex,
} from "~/lib/indexer"

// Track files the app wrote itself, to avoid re-indexing on our own writes
const inFlightWrites = new Set<string>()

export function markInFlight(path: string) {
  inFlightWrites.add(path)
  setTimeout(() => inFlightWrites.delete(path), 500)
}

export interface WorkspaceFile {
  path: string
  name: string
  isJournal: boolean
  date?: string
}

export const useWorkspaceStore = defineStore("workspace", () => {
  const workspacePath = ref<string | null>(null)
  const files = ref<WorkspaceFile[]>([])
  const currentFilePath = ref<string | null>(null)
  const loading = ref(false)
  const db = ref<Database | null>(null)

  const { readFile, writeFile, listDirectory, createDirectory } =
    useFileSystem()
  const settings = useSettings()

  const journals = computed(() => files.value.filter((f) => f.isJournal))
  const pages = computed(() => files.value.filter((f) => !f.isJournal))
  const recentWorkspaces = ref<string[]>([])

  async function initializeIndex(path: string) {
    db.value = await initIndex(`${path}/index.db`)
    await syncIndex({ listDirectory, readFile, workspacePath: path }, db.value)

    // Start file watcher
    try {
      await invoke("start_watcher", { workspacePath: path })
    } catch (e) {
      console.warn("[workspace] failed to start file watcher:", e)
    }

    // Listen for file changes from the watcher
    const _unlisten = await listen<{ path: string; kind: string }>(
      "fs-watch:change",
      async (event) => {
        const { path: changedPath, kind } = event.payload

        // Ignore events from files we wrote ourselves
        if (
          inFlightWrites.has(changedPath) ||
          inFlightWrites.has(changedPath.replace("/./", "/"))
        ) {
          return
        }

        if (!db.value) return

        try {
          if (kind === "deleted") {
            await removeFile(db.value, changedPath)
          } else {
            const content = await readFile(changedPath)
            await indexFileInDb(db.value, changedPath, content)
          }
          await scanFiles()
        } catch (e) {
          console.warn(
            "[workspace] watcher: failed to re-index",
            changedPath,
            e,
          )
        }
      },
    )
  }

  /** Result of attempting to restore a previously-saved workspace. */
  type InitResult =
    | { status: "ok" }
    | { status: "stale"; path: string }
    | { status: "missing" }

  /**
   * Restore the saved workspace if the path still exists.
   *
   * Checks the persisted `workspacePath` setting, verifies the directory
   * is still accessible, then re-indexes the workspace and starts the
   * file watcher. The caller should handle each status:
   *
   * - `"ok"` — workspace loaded, app can render normally
   * - `"stale"` — path was saved but the folder is gone, prompt user
   * - `"missing"` — no saved path (first launch), show onboarding
   */
  async function initialize(): Promise<InitResult> {
    const saved = await settings.get<string>("workspacePath")
    if (!saved) return { status: "missing" }

    // Verify stored path still exists
    try {
      await invoke("list_directory", { path: saved })
    } catch {
      return { status: "stale", path: saved }
    }

    workspacePath.value = saved
    const sessionFile = await settings.get<string>("currentFilePath")
    if (sessionFile) currentFilePath.value = sessionFile
    await scanFiles()
    await initializeIndex(saved)
    return { status: "ok" }
  }

  /**
   * Open a native directory picker for the user to select a workspace.
   * Returns the selected path or `null` if the user cancelled.
   */
  async function pickWorkspace(): Promise<string | null> {
    return await open({
      directory: true,
      multiple: false,
      title: "Select your workspace directory",
    })
  }

  /**
   * Open a native dialog for the user to create a workspace in a new folder.
   * Returns the selected path or `null` if cancelled.
   */
  async function createWorkspace(): Promise<string | null> {
    return await open({
      directory: true,
      multiple: false,
      title: "Choose a folder for your new workspace",
    })
  }

  /**
   * Initialize and persist a workspace at the given path.
   *
   * Creates the standard subdirectory layout (journals/, pages/, templates/),
   * scans for existing markdown files, builds the SQLite index, and starts
   * the file watcher. Persists the path to settings so `initialize()` can
   * restore it on next launch.
   *
   * When `options.isNew` is true and the chosen folder is empty, a one-time
   * welcome note is written to `pages/welcome.md`.
   */
  async function loadRecentWorkspaces(): Promise<void> {
    recentWorkspaces.value = await settings.getRecentWorkspaces()
  }

  async function trackRecentWorkspace(path: string): Promise<void> {
    await settings.addRecentWorkspace(path)
    recentWorkspaces.value = await settings.getRecentWorkspaces()
  }

  async function setWorkspace(
    path: string,
    _options?: { isNew?: boolean },
  ): Promise<void> {
    workspacePath.value = path
    await settings.set("workspacePath", path)
    await ensureDirectories()
    await scanFiles()
    await initializeIndex(path)
    await trackRecentWorkspace(path)
  }

  /**
   * Switch to a different workspace, tearing down the current one first.
   *
   * Drops the SQLite pool reference, resets in-memory state, then
   * initializes the new workspace at the given path.
   */
  async function switchWorkspace(path: string): Promise<void> {
    // Tear down current state
    db.value = null
    files.value = []
    workspacePath.value = null
    currentFilePath.value = null

    // Initialize the new workspace
    await setWorkspace(path)
  }

  /**
   * Close the current workspace and return to uninitialized state.
   *
   * Drops the SQLite pool reference, resets in-memory state, and clears
   * the persisted workspace path so the welcome screen shows on next
   * render.
   */
  async function closeWorkspace(): Promise<void> {
    db.value = null
    files.value = []
    workspacePath.value = null
    currentFilePath.value = null
    await settings.set("workspacePath", null)
  }

  async function ensureDirectories() {
    if (!workspacePath.value) return
    await createDirectory(`${workspacePath.value}/journals`)
    await createDirectory(`${workspacePath.value}/pages`)
    await createDirectory(`${workspacePath.value}/templates`)
  }

  async function reindex() {
    if (!db.value || !workspacePath.value) return
    await fullReindex(
      { listDirectory, readFile, workspacePath: workspacePath.value },
      db.value,
    )
    await scanFiles()
  }

  async function scanFiles() {
    if (!workspacePath.value) return
    loading.value = true
    try {
      const allFiles = await listDirectory(workspacePath.value)
      const markdownFiles = allFiles.filter((f) => f.endsWith(".md"))
      files.value = markdownFiles.map((p) => {
        const name = p.split("/").pop() ?? p
        const isJournal = !!name.match(/^\d{4}-\d{2}-\d{2}\.md$/)
        return {
          path: p,
          name,
          isJournal,
          date: isJournal ? name.replace(".md", "") : undefined,
        }
      })
    } finally {
      loading.value = false
    }
  }

  async function loadFile(relativePath: string): Promise<string> {
    if (!workspacePath.value) return ""
    return await readFile(`${workspacePath.value}/${relativePath}`)
  }

  async function saveFile(
    relativePath: string,
    content: string,
  ): Promise<void> {
    if (!workspacePath.value) return
    await writeFile(`${workspacePath.value}/${relativePath}`, content)
  }

  async function loadSavedFile(): Promise<string | null> {
    if (!currentFilePath.value || !workspacePath.value) return null
    try {
      return await readFile(currentFilePath.value)
    } catch {
      currentFilePath.value = null
      return null
    }
  }

  async function saveCurrentFile(content: string): Promise<void> {
    if (!currentFilePath.value || !workspacePath.value) return
    await writeFile(currentFilePath.value, content)
  }

  /**
   * Index a single file by its relative path. Reads the file from disk if
   * content is not provided, delegates to the indexer, then refreshes the
   * in-memory file list so the sidebar stays in sync.
   *
   * Used by the page auto-save flow in `[...slug].vue` after each write,
   * and by the page-creation path (no longer creates files — that's lazy).
   *
   * @param relativePath — Path relative to workspace root, e.g. "pages/Foo.md"
   * @param content — Optional content. If omitted, read from disk.
   */
  async function indexFile(
    relativePath: string,
    content?: string,
  ): Promise<void> {
    if (!workspacePath.value || !db.value) return
    const fullPath = `${workspacePath.value}/${relativePath}`
    const fileContent = content ?? (await readFile(fullPath))
    await indexFileInDb(db.value, relativePath, fileContent)
    await scanFiles()

    // Compute and store embeddings for changed blocks
    try {
      const changed = await db.value.select<
        { id: string; content: string; content_hash: string }[]
      >(
        `SELECT id, content, content_hash FROM blocks
         WHERE page_path = ? AND embedding_hash != content_hash
         LIMIT 50`,
        [relativePath],
      )
      for (const block of changed) {
        const embedding = await invoke<number[]>("compute_embedding", {
          text: block.content.slice(0, 1000),
        })
        if (embedding && embedding.length > 0) {
          const buf = new Float32Array(embedding)
          const blob = new Uint8Array(buf.buffer)
          await db.value.execute(
            "UPDATE blocks SET embedding = ?, embedding_hash = ? WHERE id = ?",
            [blob, block.content_hash, block.id],
          )
        }
      }
    } catch (e) {
      console.warn("[workspace] embedding failed:", e)
    }
  }

  function openFile(relativePath: string) {
    if (!workspacePath.value) return
    currentFilePath.value = `${workspacePath.value}/${relativePath}`
    settings.set("currentFilePath", currentFilePath.value)
  }

  return {
    workspacePath,
    files,
    journals,
    pages,
    currentFilePath,
    loading,
    reindex,
    db,
    recentWorkspaces,
    initialize,
    pickWorkspace,
    createWorkspace,
    setWorkspace,
    switchWorkspace,
    closeWorkspace,
    loadRecentWorkspaces,
    scanFiles,
    indexFile,
    loadFile,
    saveFile,
    loadSavedFile,
    saveCurrentFile,
    openFile,
  }
})

/**
 * Discriminated union returned by {@link useWorkspaceStore.initialize | initialize()}.
 *
 * - `"ok"` — workspace restored, app ready
 * - `"stale"` — saved path no longer accessible, user should pick again
 * - `"missing"` — no saved path (first launch)
 */
export type { InitResult }
