/**
 * Workspace indexer.
 * Walks markdown files in the workspace, parses them into blocks using
 * splitMarkdownIntoBlocks, and populates the SQLite index (pages, blocks, links, FTS5).
 *
 * The indexer is read-only — it never writes to markdown files. Blocks without
 * id:: stamps get transient index-time IDs that are regenerated on re-index.
 * Stamping (writing id:: to the file) happens in D2 when references are created.
 *
 * Content hash diffing skips blocks whose content hasn't changed, keeping
 * incremental re-index fast and preserving rowids for in-memory references.
 */

import { splitMarkdownIntoBlocks } from "@enesis/editor"
import Database from "@tauri-apps/plugin-sql"
import {
  contentHash,
  detectBlockType,
  extractLinks,
  extractTitle,
} from "./parse"
import { SCHEMA_SQL } from "./schema"

export interface IndexerOptions {
  /** Tauri invoke wrapper for listing directory contents. */
  listDirectory: (path: string) => Promise<string[]>
  /** Tauri invoke wrapper for reading file contents. */
  readFile: (path: string) => Promise<string>
  /** Absolute path to the workspace root directory. */
  workspacePath: string
}

/**
 * Initialize the SQLite index at the given path.
 * Creates the database file if it doesn't exist and runs CREATE TABLE IF NOT EXISTS.
 * Tables, indices, FTS5 virtual table, and sync triggers are all idempotent.
 */
export async function initIndex(dbPath: string): Promise<Database> {
  const db = await Database.load(`sqlite:${dbPath}`)
  await db.execute(SCHEMA_SQL)
  return db
}

/**
 * Index a single markdown file into the database.
 *
 * - Upserts the page row (title, word count, modified_at)
 * - Diffs existing block hashes against new content; skips unchanged blocks
 * - Upserts changed or new blocks
 * - Re-indexes links (delete-all + re-insert within each changed block)
 * - Removes blocks that no longer exist in the file
 */
export async function indexFile(
  db: Database,
  filePath: string,
  content: string,
): Promise<void> {
  const now = Math.floor(Date.now() / 1000)
  const _hash = contentHash(content)
  const title = extractTitle(content)

  await db.execute(
    `INSERT INTO pages (path, title, modified_at, word_count)
     VALUES ($1, $2, $3, $4)
     ON CONFLICT(path) DO UPDATE SET
       title = excluded.title,
       modified_at = excluded.modified_at,
       word_count = excluded.word_count`,
    [filePath, title, now, content.split(/\s+/).length],
  )

  const existing = await db.select<{ id: string; content_hash: string }[]>(
    "SELECT id, content_hash FROM blocks WHERE page_path = $1",
    [filePath],
  )
  const existingMap = new Map(existing.map((b) => [b.id, b.content_hash]))

  const blocks = splitMarkdownIntoBlocks(content)
  const seenIds = new Set<string>()

  for (const block of blocks) {
    seenIds.add(block.id)
    const blockHash = contentHash(block.content)

    if (existingMap.get(block.id) === blockHash) continue

    const blockType = detectBlockType(block.content)

    await db.execute(
      `INSERT INTO blocks (id, page_path, content, depth, block_type, has_id_stamp, content_hash, modified_at)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
       ON CONFLICT(id) DO UPDATE SET
         content = excluded.content,
         depth = excluded.depth,
         block_type = excluded.block_type,
         content_hash = excluded.content_hash,
         modified_at = excluded.modified_at`,
      [
        block.id,
        filePath,
        block.content,
        block.depth,
        blockType,
        0,
        blockHash,
        now,
      ],
    )

    // Re-index links for this block
    await db.execute("DELETE FROM links WHERE source_block_id = $1", [block.id])
    const links = extractLinks(block.content)
    for (const link of links) {
      await db.execute(
        `INSERT INTO links (source_block_id, source_page_path, target, link_type, offset_in_block)
         VALUES ($1, $2, $3, $4, $5)`,
        [block.id, filePath, link.target, link.linkType, link.offset],
      )
    }
  }

  // Remove stale blocks
  for (const existingId of existingMap.keys()) {
    if (!seenIds.has(existingId)) {
      await db.execute("DELETE FROM blocks WHERE id = $1", [existingId])
    }
  }
}

/**
 * Remove a file and all its blocks/links from the index.
 * Cascading deletes via the FK constraints handle blocks and links.
 */
export async function removeFile(
  db: Database,
  filePath: string,
): Promise<void> {
  await db.execute("DELETE FROM pages WHERE path = $1", [filePath])
}

/**
 * Rebuild the entire index by walking every .md file in the workspace.
 * Compares existing indexed pages against the filesystem and:
 * - Indexes new or changed files
 * - Removes pages that no longer exist
 * - Skips files that fail to read (logs warning, continues)
 */
export async function fullReindex(
  opts: IndexerOptions,
  db: Database,
): Promise<void> {
  const allFiles = await opts.listDirectory(opts.workspacePath)
  const mdFiles = allFiles.filter((f) => f.endsWith(".md"))

  const indexed = await db.select<{ path: string }[]>("SELECT path FROM pages")
  const indexedSet = new Set(indexed.map((p) => p.path))

  for (const filePath of mdFiles) {
    try {
      const content = await opts.readFile(filePath)
      await indexFile(db, filePath, content)
    } catch (e) {
      console.warn(`[indexer] failed to index ${filePath}:`, e)
    }
    indexedSet.delete(filePath)
  }

  for (const stalePath of indexedSet) {
    await removeFile(db, stalePath)
  }
}
