<script setup lang="ts">
/**
 * Catch-all route (`/*`) that maps workspace-relative file paths to views.
 *
 * **Classification:**
 * - `/tag/{name}` (no `.md` extension) → TagView (virtual aggregation)
 * - Everything else → PageView (editable editor)
 *
 * **Data flow:**
 * ```
 * Route change → loadContent() → readFile from disk
 *                    ↓
 *               existsOnDisk = true/false
 *                    ↓
 *               loadBacklinks() → SQLite → LINKS to this page title
 *                    ↓
 *               Render <PageView> with content + backlinks
 *                    ↓
 *               User types → 500ms debounce → writeFile → indexFile
 * ```
 *
 * **Key behaviors:**
 * - Non-existent pages show an empty editor. No file is created until content
 *   is typed (guarded by `existsOnDisk` — empty content on a non-existent file
 *   never triggers a write).
 * - Auto-save is debounced at 500ms. Pending saves are flushed on unmount
 *   (component destroy) to prevent data loss on quick navigation.
 * - Rename: physical file rename via `@tauri-apps/plugin-fs`. If the file
 *   doesn't exist yet (no content), just navigates to the new path.
 *
 * @remarks Edge cases & deferred work:
 * - **No mtime check on save.** Auto-save always overwrites the file on disk
 *   regardless of whether an external editor changed it between debounce
 *   triggers. A real fix would compare the file's mtime before writing and
 *   show a conflict dialog if the file was modified externally. Same
 *   limitation exists in `JournalDay.vue`.
 * - **Excerpt `slice()`** uses UTF-16 code units. If a `[[link]]` offset falls
 *   in the middle of a 4-byte Unicode character, the excerpt may be truncated
 *   mid-character. See `PageView.vue`'s `formatExcerpt` — same pattern.
 * - **Block ref scroll target** (`#block-{id}`) is appended to the URL but
 *   the receiving PageView doesn't scroll to the block yet. The hash fragment
 *   is in the URL for future use, but no `onMounted` scroll-to logic exists.
 * - **TagView block ordering** uses index timestamps, not file mtimes.
 *   Blocks from the same file will have identical `modified_at` values.
 */
import { rename } from "@tauri-apps/plugin-fs"
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue"
import { onBeforeRouteUpdate, useRouter } from "vue-router"
import type { Backlink } from "~/components/PageView.vue"
import PageView from "~/components/PageView.vue"
import TagView from "~/components/TagView.vue"
import { useBlockHighlight } from "~/composables/useBlockHighlight"
import { useFileSystem } from "~/composables/useFileSystem"
import { createGraphService } from "~/services/graph"
import { removeFile as removeFileFromIndex } from "~/lib/indexer"
import { markInFlight, useWorkspaceStore } from "~/stores/workspace"

const route = useRoute()
const router = useRouter()
const ws = useWorkspaceStore()
const fs = useFileSystem()

const slug = computed(() => (route.params.slug as string[]) ?? [])
const relativePath = computed(() => slug.value.join("/"))

const isTagView = computed(
  () =>
    relativePath.value.startsWith("tag/") &&
    !relativePath.value.endsWith(".md"),
)

const tagName = computed(() =>
  isTagView.value ? decodeURIComponent(relativePath.value.slice(4)) : "",
)

const filename = computed(() => {
  const parts = relativePath.value.split("/")
  return parts[parts.length - 1] ?? relativePath.value
})

const content = ref("")
const backlinks = ref<Backlink[]>([])
const existsOnDisk = ref(false)

// Extract target block ID from URL hash (#block-{id})
const targetBlockId = computed(() => {
  const match = route.hash.match(/^#block-(.+)$/)
  return match?.[1] ?? null
})

const { highlight } = useBlockHighlight()

function scrollToTargetBlock() {
  if (targetBlockId.value) highlight(targetBlockId.value)
}

let saveTimer: ReturnType<typeof setTimeout> | null = null

watch(content, (val) => {
  if (isTagView.value || !ws.workspacePath) return

  // Don't create a file for empty content on pages that don't exist yet
  if (!val && !existsOnDisk.value) return

  if (saveTimer) clearTimeout(saveTimer)
  saveTimer = setTimeout(async () => {
    const fullPath = `${ws.workspacePath}/${relativePath.value}`
    markInFlight(fullPath)
    try {
      await fs.writeFile(fullPath, val)
      existsOnDisk.value = true
      if (ws.db) {
        await ws.indexFile(relativePath.value, val)
      }
    } catch (e) {
      console.warn("[page] save failed:", e)
    }
  }, 500)
})

// Flush pending save on unmount (user navigates away)
function flushSave() {
  if (saveTimer) {
    clearTimeout(saveTimer)
    saveTimer = null
    if (content.value && ws.workspacePath && !isTagView.value) {
      const rp = relativePath.value
      const fullPath = `${ws.workspacePath}/${rp}`
      markInFlight(fullPath)
      fs.writeFile(fullPath, content.value).catch((e) =>
        console.warn("[page] save on unmount failed:", e),
      )
      if (ws.db) {
        ws.indexFile(rp, content.value).catch((e) =>
          console.warn("[page] index on unmount failed:", e),
        )
      }
    }
  }
}

onUnmounted(flushSave)

async function loadBacklinks() {
  const db = ws.db
  if (!db || isTagView.value) {
    backlinks.value = []
    return
  }

  try {
    const graph = createGraphService(db)
    const pageEntity = await graph.getEntity(relativePath.value)
    const pageTitle = pageEntity?.title ?? filename.value.replace(/\.md$/, "")

    const rows = await db.select<
      { source_page_path: string; content: string; offset_in_block: number }[]
    >(
      `SELECT l.source_page_path, b.content, l.offset_in_block
       FROM links l
       JOIN blocks b ON b.id = l.source_block_id
       WHERE l.link_type = 'page-ref' AND l.target = ?
       ORDER BY b.modified_at DESC
       LIMIT 50`,
      [pageTitle],
    )

    backlinks.value = rows.map((row) => {
      const isJournal = row.source_page_path.startsWith("journals/")

      const sourceName = isJournal
        ? row.source_page_path.replace("journals/", "").replace(".md", "")
        : (row.source_page_path.split("/").pop()?.replace(/\.md$/, "") ??
          row.source_page_path)

      const excerpt = extractExcerpt(row.content, row.offset_in_block)

      return {
        sourceType: isJournal ? ("journal" as const) : ("page" as const),
        sourceName,
        sourcePath: row.source_page_path,
        excerpt,
      }
    })
  } catch (e) {
    console.warn("[page] failed to load backlinks:", e)
    backlinks.value = []
  }
}

function extractExcerpt(content: string, offset: number): string {
  const start = Math.max(0, offset - 40)
  const end = Math.min(content.length, offset + 60)
  let excerpt = content.slice(start, end)
  if (start > 0) excerpt = `…${excerpt}`
  if (end < content.length) excerpt = `${excerpt}…`
  return excerpt
}

async function loadContent() {
  if (isTagView.value) return

  const fullPath = `${ws.workspacePath}/${relativePath.value}`
  try {
    content.value = await fs.readFile(fullPath)
    existsOnDisk.value = true
  } catch (e) {
    console.warn("[page] loadContent failed for", fullPath, e)
    content.value = ""
    existsOnDisk.value = false
  }
  await loadBacklinks()
}

// Load content on initial mount. onMounted ensures route params are resolved
// and the Tauri backend is available before calling readFile.
onMounted(() => {
  loadContent()
})

// React to same-component route changes (catch-all navigation).
// Uses Vue Router's guard rather than a watch on computed refs — eliminates
// timing sensitivity between the reactive flush and the async readFile.
onBeforeRouteUpdate(() => {
  loadContent()
})

let lastHighlightedBlock: string | null = null

// After content is set and editor has rendered, scroll to target block
watch(content, async () => {
  if (!targetBlockId.value || targetBlockId.value === lastHighlightedBlock) return
  lastHighlightedBlock = targetBlockId.value
  await nextTick()
  scrollToTargetBlock()
})

async function handleRename(newName: string) {
  if (!ws.workspacePath) return

  const oldPath = relativePath.value
  const lastSlash = oldPath.lastIndexOf("/")
  const oldDir = lastSlash >= 0 ? oldPath.slice(0, lastSlash + 1) : ""
  const newPath = `${oldDir}${newName}.md`

  // If no content was ever written, there's no file to rename — just navigate
  if (!content.value) {
    await router.push(`/${newPath}`)
    return
  }

  const oldFullPath = `${ws.workspacePath}/${oldPath}`
  const newFullPath = `${ws.workspacePath}/${newPath}`

  try {
    await rename(oldFullPath, newFullPath)

    const updated = content.value.replace(/^# .+/m, `# ${newName}`)
    await fs.writeFile(newFullPath, updated)
    content.value = updated

    if (ws.db) {
      await removeFileFromIndex(ws.db, oldPath)
    }
    await ws.indexFile(newPath, updated)
    await router.push(`/${newPath}`)
  } catch (e) {
    console.warn("[page] rename failed:", e)
  }
}

function handleBacklinkClick(sourcePath: string) {
  router.push(`/${sourcePath}`)
}
</script>

<template>
  <TagView
    v-if="isTagView"
    :tag="tagName"
  />
  <PageView
    v-else
    :path="relativePath"
    :filename="filename"
    :content="content"
    :backlinks="backlinks"
    :target-block-id="targetBlockId"
    @update:content="content = $event"
    @rename="handleRename"
    @backlink-clicked="handleBacklinkClick"
  />
</template>
