/// <reference types="vitest/config" />
import type { IncomingMessage, ServerResponse } from 'http'
import path from 'path'
import {
  closeSync,
  fstatSync,
  mkdirSync,
  openSync,
  opendirSync,
  readFileSync,
  renameSync,
  unlinkSync,
  writeFileSync,
  type Dirent,
} from 'fs'
import os from 'os'
import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import matter from 'gray-matter'

// --- Vault API middleware (dev only) ---

interface VaultEntry {
  path: string
  filename: string
  title: string
  isA: string | null
  aliases: string[]
  belongsTo: string[]
  relatedTo: string[]
  status: string | null
  archived: boolean
  trashed: boolean
  trashedAt: number | null
  modifiedAt: number | null
  createdAt: number | null
  fileSize: number
  snippet: string
  wordCount: number
  relationships: Record<string, string[]>
  icon: string | null
  color: string | null
  order: number | null
  sidebarLabel: string | null
  template: string | null
  sort: string | null
  view: string | null
  visible: boolean | null
  outgoingLinks: string[]
  properties: Record<string, string | number | boolean | null>
}

/** Extract all [[wiki-links]] from a string. */
function extractWikiLinks(value: string): string[] {
  const matches = value.match(/\[\[[^\]]+\]\]/g)
  return matches ?? []
}

/** Extract wiki-links from a frontmatter value (string or array of strings). */
function wikiLinksFromValue(value: unknown): string[] {
  return collectWikiLinksFromValue(value, 0)
}

function collectWikiLinksFromValue(value: unknown, depth: number): string[] {
  if (typeof value === 'string') return extractWikiLinks(value)
  if (!Array.isArray(value)) return []

  const nestedLink = nestedFlowWikilink(value, depth)
  if (nestedLink) return [nestedLink]
  return value.flatMap((item) => collectWikiLinksFromValue(item, depth + 1))
}

function nestedFlowWikilink(value: unknown[], depth: number): string | null {
  if (depth === 0 || value.length !== 1 || typeof value[0] !== 'string') return null
  return extractWikiLinks(value[0]).length === 0 ? `[[${value[0]}]]` : null
}

// Frontmatter keys that map to dedicated VaultEntry fields (skip in generic properties/relationships)
const DEDICATED_KEYS = new Set([
  'aliases', 'is_a', 'is a', 'type', 'status', 'title', '_archived',
  'archived', '_icon', 'icon', 'color', '_order', 'order',
  '_sidebar_label', 'sidebar_label', 'sidebar label', 'template',
  '_sort', 'sort', 'view', '_width', 'width', 'visible',
  '_organized', '_favorite', '_favorite_index', '_list_properties_display',
].map((key) => key.toLowerCase()))

type FrontmatterPropertyValue = string | number | boolean | null
type VaultSearchResult = { title: string; path: string; snippet: string; score: number; note_type: string | null }

interface SearchEntryInput {
  excludeFrontmatter: boolean
  entry: VaultEntry
  query: string
  rawContent: string
}

interface SearchRequestInput {
  excludeFrontmatter: boolean
  query: string
  vaultPath: string
}

interface VaultCommandPayload {
  args?: Record<string, unknown>
  cmd?: string
}

interface VaultCommandContext {
  args: Record<string, unknown>
  cmd: string
}

interface VaultCommandResponse {
  payload: unknown
  statusCode?: number
}

type VaultCommandHandler = (context: VaultCommandContext) => VaultCommandResponse

interface CommandStringInput {
  args: Record<string, unknown>
  key: string
}

interface CommandResponseInput {
  payload: unknown
  statusCode?: number
}

interface VaultReadCommandInput {
  cmd: string
  pathname: string
  req: IncomingMessage
  res: ServerResponse
  url: URL
}

interface TitleWikilinkUpdateInput {
  excludePath: string
  oldTitle: string
  vaultPath: string
}

interface LegacyWikilinkTargetInput {
  oldPath: string
  oldTitle: string
  vaultPath: string
}

interface WikilinkTargetUpdateInput {
  excludePath: string
  newTarget: string
  oldTargets: string[]
  vaultPath: string
}

interface PathWikilinkUpdateInput {
  newPath: string
  oldPath: string
  oldTitle: string
  vaultPath: string
}

function getFrontmatterValue(
  frontmatter: Record<string, unknown>,
  keys: string[],
): unknown {
  const normalizedKeys = new Set(keys.map((key) => key.toLowerCase()))
  return Object.entries(frontmatter).find(([key]) => normalizedKeys.has(key.toLowerCase()))?.[1]
}

function parseYamlBool(value: unknown): boolean | null {
  if (typeof value === 'boolean') return value
  if (typeof value !== 'string') return null

  switch (value.toLowerCase()) {
    case 'true':
    case 'yes':
      return true
    case 'false':
    case 'no':
      return false
    default:
      return null
  }
}

const vitestCoverageDirectory = process.env.VITEST_COVERAGE_DIR
  ?? path.join(os.tmpdir(), 'tolaria-vitest-coverage', String(process.pid))

const devServerWatchIgnored = [
  '**/coverage/**',
  '**/test-results/**',
  '**/playwright-report/**',
  '**/dist/**',
  '**/src-tauri/target/**',
]

function readUtf8File(filePath: string): string {
  const fd = openSync(filePath, 'r')
  try {
    return readFileSync(fd, 'utf-8')
  } finally {
    closeSync(fd)
  }
}

function writeUtf8File(filePath: string, content: string): void {
  const fd = openSync(filePath, 'w')
  try {
    writeFileSync(fd, content, 'utf-8')
  } finally {
    closeSync(fd)
  }
}

function pathStats(filePath: string) {
  const fd = openSync(filePath, 'r')
  try {
    return fstatSync(fd)
  } finally {
    closeSync(fd)
  }
}

function pathExists(filePath: string): boolean {
  try {
    pathStats(filePath)
    return true
  } catch {
    return false
  }
}

function directoryEntries(dir: string): Dirent[] {
  const directory = opendirSync(dir)
  try {
    const entries: Dirent[] = []
    let entry = directory.readSync()
    while (entry) {
      entries.push(entry)
      entry = directory.readSync()
    }
    return entries
  } finally {
    directory.closeSync()
  }
}

function isInsideRelativePath(relative: string): boolean {
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
}

function resolveInside(root: string, target: string): string | null {
  const normalizedTarget = path.normalize(target)
  if (path.isAbsolute(normalizedTarget)) return null
  const candidate = path.normalize(`${root}${path.sep}${normalizedTarget}`)
  return isInsideRelativePath(path.relative(root, candidate)) ? candidate : null
}

function frontmatterString(frontmatter: Record<string, unknown>, ...keys: string[]): string | null {
  const value = getFrontmatterValue(frontmatter, keys)
  return typeof value === 'string' ? value : null
}

function frontmatterStringArray(frontmatter: Record<string, unknown>, ...keys: string[]): string[] {
  const value = getFrontmatterValue(frontmatter, keys)
  if (Array.isArray(value)) return value.map(String)
  if (typeof value === 'string') return [value]
  return []
}

function frontmatterBool(frontmatter: Record<string, unknown>, ...keys: string[]): boolean | null {
  return parseYamlBool(getFrontmatterValue(frontmatter, keys))
}

function markdownTitle(content: string, frontmatter: Record<string, unknown>, fallback: string): string {
  const title = frontmatterString(frontmatter, 'title')
  if (title) return title

  const h1Match = content.match(/^#\s+(.+)$/m)
  return h1Match ? h1Match[1].trim() : fallback
}

function markdownBodyText(content: string): string {
  return content.replace(/^#+\s+.+$/gm, '').replace(/[\n\r]+/g, ' ').trim()
}

interface TypeTemplateSource {
  body: string
  explicitTemplate: string | null
  isA: string | null
  title: string
}

function resolveTypeTemplate(source: TypeTemplateSource): string | null {
  if (source.explicitTemplate !== null) return source.explicitTemplate
  if (source.isA !== 'Type') return null

  const template = bodyAfterTypeTitle(source)?.trim()
  if (!template) return null
  return template.split(/\r?\n/).some((line) => templateLineHasStructure({ line }))
    ? template
    : null
}

function bodyAfterTypeTitle(source: Pick<TypeTemplateSource, 'body' | 'title'>): string | null {
  const body = source.body.trimStart()
  const lineEnd = body.indexOf('\n')
  const firstLine = (lineEnd === -1 ? body : body.slice(0, lineEnd)).replace(/\r$/, '')
  const heading = firstLine.startsWith('# ') ? firstLine.slice(2).trim() : null
  if (heading !== source.title) return null
  return lineEnd === -1 ? '' : body.slice(lineEnd + 1)
}

function templateLineHasStructure({ line }: { line: string }): boolean {
  const trimmed = line.trimStart()
  return trimmed.startsWith('## ')
    || trimmed.startsWith('- [ ] ')
    || templateLineIsField({ line: trimmed })
}

function templateLineIsField({ line }: { line: string }): boolean {
  const trimmed = line.trim()
  if (!trimmed.endsWith(':')) return false
  const label = trimmed.replace(/:+$/, '').trim()
  return label.length > 0 && !label.startsWith('-')
}

function frontmatterWikiLinks(frontmatter: Record<string, unknown>, ...keys: string[]): string[] {
  return frontmatterStringArray(frontmatter, ...keys).flatMap((value) => extractWikiLinks(value))
}

function frontmatterRelationships(frontmatter: Record<string, unknown>): Record<string, string[]> {
  const relationships: Record<string, string[]> = {}
  for (const [key, value] of Object.entries(frontmatter)) {
    if (DEDICATED_KEYS.has(key.toLowerCase())) continue
    const links = wikiLinksFromValue(value)
    if (links.length > 0) relationships[key] = links
  }
  return relationships
}

function frontmatterProperties(frontmatter: Record<string, unknown>): Record<string, FrontmatterPropertyValue> {
  const properties: Record<string, FrontmatterPropertyValue> = {}
  for (const [key, value] of Object.entries(frontmatter)) {
    if (DEDICATED_KEYS.has(key.toLowerCase()) || key.trim().startsWith('_')) continue
    const propertyValue = frontmatterPropertyValue(value)
    if (propertyValue !== undefined) properties[key] = propertyValue
  }
  return properties
}

function isScalarFrontmatterProperty(value: unknown): value is number | boolean {
  return typeof value === 'number' || typeof value === 'boolean'
}

function singleStringArrayValue(value: unknown): string | undefined {
  if (!Array.isArray(value)) return undefined
  if (value.length !== 1) return undefined
  return typeof value[0] === 'string' ? value[0] : undefined
}

function wikiLinkFreeString(value: string): string | undefined {
  return extractWikiLinks(value).length === 0 ? value : undefined
}

function frontmatterPropertyValue(value: unknown): FrontmatterPropertyValue | undefined {
  if (value === null) return null
  if (isScalarFrontmatterProperty(value)) return value
  if (typeof value === 'string') return wikiLinkFreeString(value)
  const singleArrayValue = singleStringArrayValue(value)
  return singleArrayValue === undefined ? undefined : wikiLinkFreeString(singleArrayValue)
}

function parseMarkdownFile(filePath: string): VaultEntry | null {
  try {
    const raw = readUtf8File(filePath)
    const stats = pathStats(filePath)
    const { data, content } = matter(raw)
    const fm = data as Record<string, unknown>

    const filename = path.basename(filePath)
    const basename = filename.replace(/\.md$/, '')

    const title = markdownTitle(content, fm, basename)
    const isA = frontmatterString(fm, 'is_a', 'is a', 'type')
    const bodyText = markdownBodyText(content)
    const snippet = bodyText.slice(0, 200)
    const template = resolveTypeTemplate({
      body: content,
      explicitTemplate: frontmatterString(fm, 'template'),
      isA,
      title,
    })

    return {
      path: filePath,
      filename,
      title,
      isA,
      aliases: frontmatterStringArray(fm, 'aliases'),
      belongsTo: frontmatterWikiLinks(fm, 'belongs_to', 'belongs to'),
      relatedTo: frontmatterWikiLinks(fm, 'related_to', 'related to'),
      status: frontmatterString(fm, 'status'),
      archived: frontmatterBool(fm, 'archived') ?? false,
      trashed: frontmatterBool(fm, 'trashed') ?? false,
      trashedAt: null,
      modifiedAt: stats.mtimeMs,
      createdAt: stats.birthtimeMs,
      fileSize: stats.size,
      snippet,
      wordCount: bodyText.split(/\s+/).filter(Boolean).length,
      relationships: frontmatterRelationships(fm),
      icon: frontmatterString(fm, 'icon'),
      color: frontmatterString(fm, 'color'),
      order: fm.order != null ? Number(fm.order) : null,
      sidebarLabel: frontmatterString(fm, 'sidebar label', 'sidebar_label'),
      template,
      sort: frontmatterString(fm, 'sort'),
      view: frontmatterString(fm, 'view'),
      visible: frontmatterBool(fm, 'visible'),
      outgoingLinks: [],
      properties: frontmatterProperties(fm),
    }
  } catch {
    return null
  }
}

/** Recursively find all .md files under a directory. */
function findMarkdownFiles(dir: string): string[] {
  const results: string[] = []
  try {
    const items = directoryEntries(dir)
    for (const item of items) {
      if (item.name.startsWith('.')) continue
      const full = resolveInside(dir, item.name)
      if (!full) continue
      if (item.isDirectory()) {
        results.push(...findMarkdownFiles(full))
      } else if (item.name.endsWith('.md')) {
        results.push(full)
      }
    }
  } catch {
    // skip unreadable dirs
  }
  return results
}

function sendJson(res: ServerResponse, payload: unknown, statusCode = 200): void {
  res.statusCode = statusCode
  res.setHeader('Content-Type', 'application/json')
  res.end(JSON.stringify(payload))
}

function commandString({ args, key }: CommandStringInput): string | null {
  const value = Reflect.get(args, key)
  return typeof value === 'string' && value.length > 0 ? value : null
}

function commandBool({ args, key }: CommandStringInput): boolean {
  const value = Reflect.get(args, key)
  return value === true || value === '1' || value === 'true'
}

function commandResponse({ payload, statusCode = 200 }: CommandResponseInput): VaultCommandResponse {
  return { payload, statusCode }
}

function invalidPathResponse(): VaultCommandResponse {
  return commandResponse({ payload: { error: 'Invalid or missing path' }, statusCode: 400 })
}

function existingCommandPath(input: CommandStringInput): string | null {
  const filePath = commandString(input)
  return filePath && pathExists(filePath) ? filePath : null
}

function commandVaultList({ args }: VaultCommandContext): VaultCommandResponse {
  const dirPath = existingCommandPath({ args, key: 'path' })
  if (!dirPath) return invalidPathResponse()

  const entries = findMarkdownFiles(dirPath).map(parseMarkdownFile).filter(Boolean)
  return commandResponse({ payload: entries })
}

function commandVaultContent({ args }: VaultCommandContext): VaultCommandResponse {
  const filePath = existingCommandPath({ args, key: 'path' })
  if (!filePath) return invalidPathResponse()
  return commandResponse({ payload: { content: readUtf8File(filePath) } })
}

function commandVaultAllContent({ args }: VaultCommandContext): VaultCommandResponse {
  const dirPath = existingCommandPath({ args, key: 'path' })
  if (!dirPath) return invalidPathResponse()

  const contentMap: Record<string, string> = {}
  for (const filePath of findMarkdownFiles(dirPath)) {
    try {
      Reflect.set(contentMap, filePath, readUtf8File(filePath))
    } catch {
      // Skip unreadable files.
    }
  }
  return commandResponse({ payload: contentMap })
}

function commandVaultEntry({ args }: VaultCommandContext): VaultCommandResponse {
  const filePath = existingCommandPath({ args, key: 'path' })
  if (!filePath) return invalidPathResponse()
  return commandResponse({ payload: parseMarkdownFile(filePath) })
}

function commandVaultSearch({ args }: VaultCommandContext): VaultCommandResponse {
  const vaultPath = commandString({ args, key: 'vault_path' })
  const query = (commandString({ args, key: 'query' }) ?? '').toLowerCase()
  const mode = commandString({ args, key: 'mode' }) ?? 'all'
  const excludeFrontmatter = commandBool({ args, key: 'exclude_frontmatter' })
  const results = vaultPath && query
    ? collectVaultSearchResults({ vaultPath, query, excludeFrontmatter })
    : []
  return commandResponse({ payload: { results, elapsed_ms: results.length > 0 ? 1 : 0, query, mode } })
}

function commandVaultSave({ args }: VaultCommandContext): VaultCommandResponse {
  const filePath = commandString({ args, key: 'path' })
  const content = Reflect.get(args, 'content')
  if (!filePath || typeof content !== 'string') return commandResponse({ payload: { error: 'Missing path or content' }, statusCode: 400 })

  mkdirSync(path.dirname(filePath), { recursive: true })
  writeUtf8File(filePath, content)
  return commandResponse({ payload: null })
}

function commandVaultRename({ args }: VaultCommandContext): VaultCommandResponse {
  const oldPath = commandString({ args, key: 'old_path' })
  const newTitle = commandString({ args, key: 'new_title' })
  if (!oldPath || !newTitle) return commandResponse({ payload: { error: 'Missing rename input' }, statusCode: 400 })

  const vaultPath = commandString({ args, key: 'vault_path' })
  const oldContent = readUtf8File(oldPath)
  const oldTitle = oldContent.match(/^# (.+)$/m)?.[1]?.trim() ?? ''
  const slug = newTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
  const newPath = markdownSiblingPath(oldPath, slug)
  if (!newPath) return commandResponse({ payload: { error: 'Invalid title' }, statusCode: 400 })

  writeUtf8File(newPath, oldContent.replace(/^# .+$/m, `# ${newTitle}`))
  if (newPath !== oldPath) unlinkSync(oldPath)

  const updatedFiles = vaultPath ? updateTitleWikilinks({ excludePath: newPath, oldTitle, vaultPath }) : 0
  return commandResponse({ payload: { new_path: newPath, updated_files: updatedFiles } })
}

function commandVaultRenameFilename({ args }: VaultCommandContext): VaultCommandResponse {
  const oldPath = commandString({ args, key: 'old_path' })
  if (!oldPath) return commandResponse({ payload: { error: 'Missing old path' }, statusCode: 400 })

  const filename = validateMarkdownFilenameStem(commandString({ args, key: 'new_filename_stem' }))
  if (!filename.ok) return commandResponse({ payload: { error: filename.error }, statusCode: 400 })

  const newPath = markdownSiblingPath(oldPath, filename.stem)
  if (!newPath) return commandResponse({ payload: { error: 'Invalid filename' }, statusCode: 400 })
  if (newPath !== oldPath && pathExists(newPath)) {
    return commandResponse({ payload: { error: 'A note with that name already exists' }, statusCode: 409 })
  }

  const vaultPath = commandString({ args, key: 'vault_path' })
  const oldTitle = parseMarkdownFile(oldPath)?.title ?? path.basename(oldPath, '.md')
  renameSync(oldPath, newPath)
  const updatedFiles = vaultPath ? updatePathWikilinks({ newPath, oldPath, oldTitle, vaultPath }) : 0
  return commandResponse({ payload: { new_path: newPath, updated_files: updatedFiles } })
}

function commandVaultDelete({ args }: VaultCommandContext): VaultCommandResponse {
  const filePath = commandString({ args, key: 'path' })
  if (!filePath) return commandResponse({ payload: { error: 'Missing path' }, statusCode: 400 })
  unlinkSync(filePath)
  return commandResponse({ payload: filePath })
}

const VAULT_COMMAND_HANDLERS = new Map<string, VaultCommandHandler>([
  ['delete_note', commandVaultDelete],
  ['get_all_content', commandVaultAllContent],
  ['get_note_content', commandVaultContent],
  ['list_vault', commandVaultList],
  ['reload_vault', commandVaultList],
  ['reload_vault_entry', commandVaultEntry],
  ['rename_note', commandVaultRename],
  ['rename_note_filename', commandVaultRenameFilename],
  ['save_note_content', commandVaultSave],
  ['search_vault', commandVaultSearch],
  ['validate_note_content', commandVaultContent],
])

function runVaultCommand(context: VaultCommandContext): VaultCommandResponse {
  const handler = VAULT_COMMAND_HANDLERS.get(context.cmd)
  if (handler) return handler(context)
  return commandResponse({ payload: { error: 'Unsupported vault command' }, statusCode: 404 })
}

function vaultCommandContext(payload: VaultCommandPayload): VaultCommandContext | null {
  if (!payload.cmd || !payload.args) return null
  return { cmd: payload.cmd, args: payload.args }
}

const VAULT_ENDPOINT_ARG_KEYS = ['path', 'vault_path', 'query', 'mode', 'reload', 'exclude_frontmatter'] as const

function readVaultQueryArgs(url: URL): Record<string, unknown> {
  const args: Record<string, unknown> = {}
  for (const key of VAULT_ENDPOINT_ARG_KEYS) {
    const value = url.searchParams.get(key)
    if (value !== null) Reflect.set(args, key, value)
  }
  return args
}

async function readVaultEndpointArgs(url: URL, req: IncomingMessage): Promise<Record<string, unknown>> {
  if (req.method === 'POST') return readJsonBody<Record<string, unknown>>(req)
  return readVaultQueryArgs(url)
}

async function handleVaultReadCommand(
  { cmd, pathname, req, res, url }: VaultReadCommandInput,
): Promise<boolean> {
  if (url.pathname !== pathname) return false
  if (req.method !== 'GET' && req.method !== 'POST') {
    sendJson(res, { error: 'Unsupported method' }, 405)
    return true
  }

  try {
    const response = runVaultCommand({ args: await readVaultEndpointArgs(url, req), cmd })
    sendJson(res, response.payload, response.statusCode)
  } catch (err: unknown) {
    sendCaughtError(res, err, 'Vault read failed')
  }
  return true
}

function updateTitleWikilinks({ excludePath, oldTitle, vaultPath }: TitleWikilinkUpdateInput): number {
  const newPathStem = path.relative(vaultPath, excludePath).replace(/\.md$/i, '')
  const oldTargets = collectLegacyWikilinkTargets({ oldPath: excludePath, oldTitle, vaultPath })
  return updateWikilinksForTargets({ excludePath, newTarget: newPathStem, oldTargets, vaultPath })
}

function collectLegacyWikilinkTargets({ oldPath, oldTitle, vaultPath }: LegacyWikilinkTargetInput): string[] {
  const oldRelativeStem = path.relative(vaultPath, oldPath).replace(/\.md$/i, '')
  const oldFilenameStem = path.basename(oldPath, '.md')
  return [...new Set([oldTitle, oldRelativeStem, oldFilenameStem].filter(Boolean))]
}

function updateWikilinksForTargets({ excludePath, newTarget, oldTargets, vaultPath }: WikilinkTargetUpdateInput): number {
  if (oldTargets.length === 0) return 0
  const allFiles = findMarkdownFiles(vaultPath)
  const targets = new Set(oldTargets)
  let updatedFiles = 0
  for (const filePath of allFiles) {
    if (filePath === excludePath) continue
    try {
      const content = readUtf8File(filePath)
      const replaced = content.replace(/\[\[([^\]|]+)(\|[^\]]*)?\]\]/g, (match: string, target: string, pipe: string | undefined) => {
        if (!targets.has(target)) return match
        return pipe ? `[[${newTarget}${pipe}]]` : `[[${newTarget}]]`
      })
      if (replaced !== content) {
        writeUtf8File(filePath, replaced)
        updatedFiles++
      }
    } catch {
      // Skip unreadable files in the dev vault API.
    }
  }
  return updatedFiles
}

function updatePathWikilinks({ newPath, oldPath, oldTitle, vaultPath }: PathWikilinkUpdateInput): number {
  const newRelativeStem = path.relative(vaultPath, newPath).replace(/\.md$/i, '')
  const oldTargets = collectLegacyWikilinkTargets({ oldPath, oldTitle, vaultPath })
  return updateWikilinksForTargets({ excludePath: newPath, newTarget: newRelativeStem, oldTargets, vaultPath })
}

function handleVaultPing(url: URL, res: ServerResponse): boolean {
  if (url.pathname !== '/api/vault/ping') return false
  sendJson(res, { ok: true })
  return true
}

async function handleVaultList(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  return handleVaultReadCommand({ cmd: 'list_vault', pathname: '/api/vault/list', req, res, url })
}

async function handleVaultContent(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  return handleVaultReadCommand({ cmd: 'get_note_content', pathname: '/api/vault/content', req, res, url })
}

async function handleVaultAllContent(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  return handleVaultReadCommand({ cmd: 'get_all_content', pathname: '/api/vault/all-content', req, res, url })
}

async function handleVaultEntry(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  return handleVaultReadCommand({ cmd: 'reload_vault_entry', pathname: '/api/vault/entry', req, res, url })
}

async function handleVaultSearch(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  return handleVaultReadCommand({ cmd: 'search_vault', pathname: '/api/vault/search', req, res, url })
}

function collectVaultSearchResults({ excludeFrontmatter, vaultPath, query }: SearchRequestInput): VaultSearchResult[] {
  const results: VaultSearchResult[] = []
  for (const filePath of findMarkdownFiles(vaultPath)) {
    const entry = parseMarkdownFile(filePath)
    if (!entry || entry.trashed) continue
    const rawContent = readUtf8File(filePath)
    if (entryMatchesSearch({ entry, rawContent, query, excludeFrontmatter })) {
      results.push(searchResultFromEntry(entry))
    }
  }
  return results.slice(0, 20)
}

function searchableSearchContent(rawContent: string, excludeFrontmatter: boolean): string {
  return excludeFrontmatter ? matter(rawContent).content : rawContent
}

function entryMatchesSearch({ entry, excludeFrontmatter, rawContent, query }: SearchEntryInput): boolean {
  const content = searchableSearchContent(rawContent, excludeFrontmatter)
  return entry.title.toLowerCase().includes(query) || content.toLowerCase().includes(query)
}

function searchResultFromEntry(entry: VaultEntry): VaultSearchResult {
  return { title: entry.title, path: entry.path, snippet: entry.snippet, score: 1.0, note_type: entry.isA }
}

async function handleVaultSave(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  if (!isPostRoute(url, req, '/api/vault/save')) return false
  try {
    await saveVaultContent(req, res)
  } catch (err: unknown) {
    sendCaughtError(res, err, 'Save failed')
  }
  return true
}

async function saveVaultContent(req: IncomingMessage, res: ServerResponse): Promise<void> {
  const { path: filePath, content } = await readJsonBody<{ path?: string; content?: string }>(req)
  if (!filePath || content === undefined) {
    sendJson(res, { error: 'Missing path or content' }, 400)
    return
  }

  mkdirSync(path.dirname(filePath), { recursive: true })
  writeUtf8File(filePath, content)
  sendJson(res, null)
}

async function handleVaultRename(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  if (!isPostRoute(url, req, '/api/vault/rename')) return false
  try {
    await renameVaultNoteTitle(req, res)
  } catch (err: unknown) {
    sendCaughtError(res, err, 'Rename failed')
  }
  return true
}

async function renameVaultNoteTitle(req: IncomingMessage, res: ServerResponse): Promise<void> {
  const {
    vault_path: vaultPath,
    old_path: oldPath,
    new_title: newTitle,
  } = await readJsonBody<{ vault_path?: string; old_path: string; new_title: string }>(req)
  const oldContent = readUtf8File(oldPath)
  const oldTitle = oldContent.match(/^# (.+)$/m)?.[1]?.trim() ?? ''
  const slug = newTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
  const newPath = markdownSiblingPath(oldPath, slug)
  if (!newPath) {
    sendJson(res, { error: 'Invalid title' }, 400)
    return
  }

  writeUtf8File(newPath, oldContent.replace(/^# .+$/m, `# ${newTitle}`))
  if (newPath !== oldPath) unlinkSync(oldPath)

  const updatedFiles = vaultPath ? updateTitleWikilinks({ excludePath: newPath, oldTitle, vaultPath }) : 0
  sendJson(res, { new_path: newPath, updated_files: updatedFiles })
}

type FilenameStemValidation =
  | { ok: true; stem: string }
  | { ok: false; error: string }

function validateMarkdownFilenameStem(value: unknown): FilenameStemValidation {
  const stem = String(value ?? '').trim().replace(/\.md$/i, '').trim()
  if (!stem) return { ok: false, error: 'New filename cannot be empty' }
  if (isUnsafeMarkdownFilenameStem(stem)) return { ok: false, error: 'Invalid filename' }
  return { ok: true, stem }
}

function isUnsafeMarkdownFilenameStem(stem: string): boolean {
  return stem === '.' || stem === '..' || stem.includes('/') || stem.includes('\\')
}

function markdownSiblingPath(filePath: string, stem: string): string | null {
  if (isUnsafeMarkdownFilenameStem(stem)) return null
  return resolveInside(path.dirname(filePath), `${stem}.md`)
}

async function handleVaultRenameFilename(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  if (!isPostRoute(url, req, '/api/vault/rename-filename')) return false
  try {
    await renameVaultNoteFilename(req, res)
  } catch (err: unknown) {
    sendCaughtError(res, err, 'Rename failed')
  }
  return true
}

async function renameVaultNoteFilename(req: IncomingMessage, res: ServerResponse): Promise<void> {
  const {
    vault_path: vaultPath,
    old_path: oldPath,
    new_filename_stem: newFilenameStem,
  } = await readJsonBody<{ vault_path?: string; old_path: string; new_filename_stem: string }>(req)
  const filename = validateMarkdownFilenameStem(newFilenameStem)
  if (!filename.ok) {
    sendJson(res, { error: filename.error }, 400)
    return
  }

  const newPath = markdownSiblingPath(oldPath, filename.stem)
  if (!newPath) {
    sendJson(res, { error: 'Invalid filename' }, 400)
    return
  }
  if (newPath !== oldPath && pathExists(newPath)) {
    sendJson(res, { error: 'A note with that name already exists' }, 409)
    return
  }

  const oldTitle = parseMarkdownFile(oldPath)?.title ?? path.basename(oldPath, '.md')
  renameSync(oldPath, newPath)
  const updatedFiles = vaultPath ? updatePathWikilinks({ newPath, oldPath, oldTitle, vaultPath }) : 0
  sendJson(res, { new_path: newPath, updated_files: updatedFiles })
}

async function handleVaultDelete(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  if (url.pathname !== '/api/vault/delete' || req.method !== 'POST') return false
  try {
    const body = await readRequestBody(req)
    const { path: filePath } = JSON.parse(body)
    if (!filePath) {
      sendJson(res, { error: 'Missing path' }, 400)
      return true
    }
    unlinkSync(filePath)
    sendJson(res, filePath)
  } catch (err: unknown) {
    sendJson(res, { error: err instanceof Error ? err.message : 'Delete failed' }, 500)
  }
  return true
}

async function handleVaultCommand(url: URL, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  if (!isPostRoute(url, req, '/api/vault/command')) return false
  try {
    const context = vaultCommandContext(await readJsonBody<VaultCommandPayload>(req))
    if (!context) {
      sendJson(res, { error: 'Invalid vault command' }, 400)
      return true
    }

    const response = runVaultCommand(context)
    sendJson(res, response.payload, response.statusCode)
  } catch (err: unknown) {
    sendCaughtError(res, err, 'Vault command failed')
  }
  return true
}

async function handleVaultApiRequest(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
  const url = new URL(req.url ?? '/', `http://${req.headers.host}`)
  const handlers = [
    () => Promise.resolve(handleVaultPing(url, res)),
    () => handleVaultCommand(url, req, res),
    () => handleVaultList(url, req, res),
    () => handleVaultContent(url, req, res),
    () => handleVaultAllContent(url, req, res),
    () => handleVaultEntry(url, req, res),
    () => handleVaultSearch(url, req, res),
    () => handleVaultSave(url, req, res),
    () => handleVaultRename(url, req, res),
    () => handleVaultRenameFilename(url, req, res),
    () => handleVaultDelete(url, req, res),
  ]

  for (const handler of handlers) {
    if (await handler()) return true
  }

  return false
}

function vaultApiPlugin(): Plugin {
  return {
    name: 'vault-api',
    configureServer(server) {
      server.middlewares.use(async (req, res, next) => {
        if (await handleVaultApiRequest(req, res)) return
        next()
      })
    },
  }
}

// --- Proxy helpers ---

function readRequestBody(req: IncomingMessage): Promise<string> {
  return new Promise((resolve) => {
    let body = ''
    req.on('data', (chunk: Buffer) => { body += chunk.toString() })
    req.on('end', () => resolve(body))
  })
}

async function readJsonBody<T>(req: IncomingMessage): Promise<T> {
  return JSON.parse(await readRequestBody(req)) as T
}

function isPostRoute(url: URL, req: IncomingMessage, pathname: string): boolean {
  return url.pathname === pathname && req.method === 'POST'
}

function sendCaughtError(res: ServerResponse, err: unknown, fallback: string): void {
  sendJson(res, { error: err instanceof Error ? err.message : fallback }, 500)
}

/** WebSocket proxy info endpoint — tells the frontend where the MCP bridge is */
function mcpBridgeInfoPlugin(): Plugin {
  return {
    name: 'mcp-bridge-info',
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        if (req.url !== '/api/mcp/info') return next()
        res.setHeader('Content-Type', 'application/json')
        res.end(JSON.stringify({
          wsUrl: `ws://localhost:${process.env.MCP_WS_PORT || 9710}`,
          available: true,
        }))
      })
    },
  }
}

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss(), vaultApiPlugin(), mcpBridgeInfoPlugin()],
  cacheDir: process.env.TOLARIA_VITE_CACHE_DIR,

  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },

  // Inject the demo-vault-v2 path in local dev only — production Tauri builds and
  // CI must resolve the default vault path at runtime via the backend to avoid
  // baking the CI runner's absolute path into the distributed bundle.
  define: {
    ...(process.env.CI || (process.env.TAURI_PLATFORM && !process.env.TAURI_DEBUG)
      ? {}
      : { __DEMO_VAULT_PATH__: JSON.stringify(path.resolve(__dirname, 'demo-vault-v2')) }),
  },

  // Prevent vite from obscuring Rust errors
  clearScreen: false,

  // Tauri expects a fixed port
  server: {
    port: 5202,
    strictPort: true,
    allowedHosts: true,
    watch: {
      ignored: devServerWatchIgnored,
    },
  },

  // Env variables starting with TAURI_ are exposed to the frontend
  envPrefix: ['VITE_', 'TAURI_'],

  build: {
    // Tauri uses Chromium on Windows and WebKit on macOS/Linux
    target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
    // Don't minify for debug builds
    minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
    // Produce sourcemaps for debug builds
    sourcemap: !!process.env.TAURI_DEBUG,
  },

  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    // The full jsdom suite is heavy enough that unconstrained worker fan-out can
    // starve UI tests on local dev machines. Keep the default hook path stable,
    // while still allowing CI or one-off runs to opt into a different cap.
    maxWorkers: process.env.VITEST_MAX_WORKERS ?? 4,
    testTimeout: 10_000,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      // Keep coverage temp files off the mounted workspace to avoid flaky
      // read-after-write races when Vitest re-reads its own coverage shards.
      reportsDirectory: vitestCoverageDirectory,
      include: ['src/**/*.{ts,tsx}'],
      exclude: [
        'src/**/*.{test,spec}.{ts,tsx}',
        'src/test/**',
        'src/mock-tauri.ts',
        'src/main.tsx',
        'src/types.ts',
        'src/hooks/useMcpBridge.ts',
        'src/hooks/useAiAgent.ts',
        'src/utils/ai-chat.ts',
        'src/utils/ai-agent.ts',
        'src/components/ui/dropdown-menu.tsx',
        'src/components/ui/scroll-area.tsx',
        'src/components/ui/select.tsx',
        'src/components/ui/separator.tsx',
        'src/components/ui/tabs.tsx',
        'src/components/ui/tooltip.tsx',
        'src/components/ui/card.tsx',
      ],
      thresholds: {
        lines: 70,
        functions: 70,
        branches: 70,
        statements: 70,
      },
    },
  },
})
