/** Utility functions for parsing wikilink syntax: [[target|display]] */

import type { VaultEntry } from '../types'
import { slugifyNoteStem } from './noteSlug'
import { workspaceForEntry, workspacePathForEntry } from './workspaces'

export type AbsoluteNotePath = string
export type NoteTitleOrTarget = string
export type VaultPath = string
export type WikilinkReference = string
export type WikilinkTarget = string

/** Extracts the target path from a wikilink reference (strips [[ ]] and display text). */
export function wikilinkTarget(ref: WikilinkReference): WikilinkTarget {
  const inner = ref.replace(/^\[\[|\]\]$/g, '')
  const pipeIdx = inner.indexOf('|')
  return pipeIdx !== -1 ? inner.slice(0, pipeIdx) : inner
}

/** Extracts the display label from a wikilink reference. Falls back to humanised path stem. */
export function wikilinkDisplay(ref: WikilinkReference): string {
  const inner = ref.replace(/^\[\[|\]\]$/g, '')
  const pipeIdx = inner.indexOf('|')
  if (pipeIdx !== -1) return inner.slice(pipeIdx + 1)
  const last = inner.split('/').pop() ?? inner
  return last.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
}

function stripWindowsExtendedPathPrefix(path: AbsoluteNotePath | VaultPath): string {
  return path
    .replace(/^\\\\\?\\UNC\\/i, '//')
    .replace(/^\\\\\?\\/, '')
}

function normalizeFilesystemPath(path: AbsoluteNotePath | VaultPath): string {
  return stripWindowsExtendedPathPrefix(path)
    .replace(/\\/g, '/')
    .replace(/\/+$/g, '')
}

function withoutMarkdownExtension(pathStem: WikilinkTarget): WikilinkTarget {
  return pathStem.replace(/\.md$/i, '')
}

/** Extract the vault-relative path stem (no leading slash, no .md extension). */
export function relativePathStem(absolutePath: AbsoluteNotePath, vaultPath: VaultPath): WikilinkTarget {
  const normalizedAbsolutePath = normalizeFilesystemPath(absolutePath)
  const normalizedVaultPath = normalizeFilesystemPath(vaultPath)
  const prefix = normalizedVaultPath.endsWith('/') ? normalizedVaultPath : normalizedVaultPath + '/'
  if (normalizedAbsolutePath.toLowerCase().startsWith(prefix.toLowerCase())) {
    return withoutMarkdownExtension(normalizedAbsolutePath.slice(prefix.length))
  }
  // Fallback: just the filename stem
  const filename = normalizedAbsolutePath.split('/').pop() ?? normalizedAbsolutePath
  return withoutMarkdownExtension(filename)
}

/** Slugify a human-readable title into the canonical wikilink filename stem. */
export const slugifyWikilinkTarget = slugifyNoteStem

function shouldPrefixWorkspaceAlias(entryAlias?: string, sourceAlias?: string): boolean {
  return !!entryAlias && !!sourceAlias && entryAlias !== sourceAlias
}

/** Build the canonical wikilink target for a vault entry. */
export function canonicalWikilinkTargetForEntry(entry: VaultEntry, vaultPath: VaultPath, sourceEntry?: VaultEntry): WikilinkTarget {
  const entryWorkspace = workspaceForEntry(entry)
  const sourceWorkspace = sourceEntry ? workspaceForEntry(sourceEntry) : null
  const entryVaultPath = workspacePathForEntry(entry) ?? vaultPath
  const localTarget = relativePathStem(entry.path, entryVaultPath)
  const entryAlias = entryWorkspace?.alias
  if (shouldPrefixWorkspaceAlias(entryAlias, sourceWorkspace?.alias)) {
    return `${entryAlias}/${localTarget}`
  }
  return localTarget
}

/** Resolve a user-facing title/path input to the canonical wikilink target. */
export function canonicalWikilinkTargetForTitle(
  titleOrTarget: NoteTitleOrTarget,
  entries: VaultEntry[],
  vaultPath: VaultPath,
  sourceEntry?: VaultEntry,
): WikilinkTarget {
  const trimmed = titleOrTarget.trim()
  const resolved = resolveEntry(entries, trimmed, sourceEntry)
  return resolved
    ? canonicalWikilinkTargetForEntry(resolved, vaultPath, sourceEntry)
    : trimmed.includes('/')
      ? trimmed.replace(/^\/+/, '').replace(/\.md$/, '')
      : slugifyWikilinkTarget(trimmed)
}

/** Wrap a target in wikilink syntax. */
export function formatWikilinkRef(target: WikilinkTarget): WikilinkReference {
  return `[[${target}]]`
}

interface ResolutionKey {
  exactTarget: string
  workspaceAlias: string | null
  targetWithoutWorkspace: string
  lastSegment: string
  pathSuffixes: string[]
  humanizedTarget: string | null
}

interface IndexedResolutionEntry {
  aliases: string[]
  entry: VaultEntry
  filenameStem: string
  normalizedPath: string
  title: string
  workspaceAlias: string | null
}

interface ResolutionIndex {
  entries: IndexedResolutionEntry[]
  entriesByWorkspaceAlias: Map<string, IndexedResolutionEntry[]>
  resolutionCache: Map<string, VaultEntry | null>
  workspaceAliases: Set<string>
}

type EntryMatcher = (entry: IndexedResolutionEntry, resolutionKey: ResolutionKey) => boolean

const resolutionIndexesByEntries = new WeakMap<VaultEntry[], ResolutionIndex>()

function buildResolutionKey(rawTarget: WikilinkTarget, knownWorkspaceAliases: Set<string> = new Set()): ResolutionKey {
  const exactTarget = rawTarget.includes('|') ? rawTarget.split('|')[0] : rawTarget
  const normalizedTarget = exactTarget.toLowerCase()
  const segments = exactTarget.split('/').filter(Boolean)
  const candidateWorkspaceAlias = segments.length > 1 ? segments[0].toLowerCase() : null
  const workspaceAlias = candidateWorkspaceAlias && knownWorkspaceAliases.has(candidateWorkspaceAlias)
    ? candidateWorkspaceAlias
    : null
  const targetWithoutWorkspace = workspaceAlias ? segments.slice(1).join('/') : exactTarget
  const normalizedLocalTarget = targetWithoutWorkspace.toLowerCase()
  const normalizedPathTarget = normalizedLocalTarget.replace(/^\/+/, '')
  const pathSuffixes = normalizedPathTarget.includes('/')
    ? [`/${normalizedPathTarget}`, ...normalizedPathTarget.endsWith('.md') ? [] : [`/${normalizedPathTarget}.md`]]
    : []
  const lastSegment = targetWithoutWorkspace.includes('/') ? (targetWithoutWorkspace.split('/').pop() ?? targetWithoutWorkspace).toLowerCase() : normalizedLocalTarget
  const humanizedTarget = lastSegment.replace(/-/g, ' ')

  return {
    exactTarget: normalizedTarget,
    workspaceAlias,
    targetWithoutWorkspace: normalizedLocalTarget,
    lastSegment,
    pathSuffixes,
    humanizedTarget: humanizedTarget === normalizedLocalTarget ? null : humanizedTarget,
  }
}

function workspaceAliasForEntry(entry: VaultEntry | undefined): string | null {
  return entry ? (workspaceForEntry(entry)?.alias.toLowerCase() ?? null) : null
}

function buildIndexedResolutionEntry(entry: VaultEntry): IndexedResolutionEntry {
  return {
    aliases: entry.aliases.map((alias) => alias.toLowerCase()),
    entry,
    filenameStem: entry.filename.replace(/\.md$/, '').toLowerCase(),
    normalizedPath: normalizeFilesystemPath(entry.path).toLowerCase(),
    title: entry.title.toLowerCase(),
    workspaceAlias: workspaceAliasForEntry(entry),
  }
}

function buildResolutionIndex(entries: VaultEntry[]): ResolutionIndex {
  const indexedEntries = entries.map(buildIndexedResolutionEntry)
  const entriesByWorkspaceAlias = new Map<string, IndexedResolutionEntry[]>()
  const workspaceAliases = new Set<string>()

  for (const entry of indexedEntries) {
    if (!entry.workspaceAlias) continue
    workspaceAliases.add(entry.workspaceAlias)
    const workspaceEntries = entriesByWorkspaceAlias.get(entry.workspaceAlias) ?? []
    workspaceEntries.push(entry)
    entriesByWorkspaceAlias.set(entry.workspaceAlias, workspaceEntries)
  }

  return {
    entries: indexedEntries,
    entriesByWorkspaceAlias,
    resolutionCache: new Map(),
    workspaceAliases,
  }
}

function resolutionIndexForEntries(entries: VaultEntry[]): ResolutionIndex {
  const cached = resolutionIndexesByEntries.get(entries)
  if (cached) return cached

  const index = buildResolutionIndex(entries)
  resolutionIndexesByEntries.set(entries, index)
  return index
}

function resolutionCacheKey(resolutionKey: ResolutionKey, sourceWorkspaceAlias: string | null): string {
  return `${sourceWorkspaceAlias ?? ''}\n${resolutionKey.exactTarget}`
}

function findIndexedEntry(entries: IndexedResolutionEntry[], resolutionKey: ResolutionKey, matcher: EntryMatcher): VaultEntry | undefined {
  for (const entry of entries) {
    if (matcher(entry, resolutionKey)) return entry.entry
  }
  return undefined
}

function findPrioritizedEntry(
  index: ResolutionIndex,
  resolutionKey: ResolutionKey,
  sourceWorkspaceAlias: string | null,
  matcher: EntryMatcher,
): VaultEntry | undefined {
  if (resolutionKey.workspaceAlias) {
    return findIndexedEntry(index.entriesByWorkspaceAlias.get(resolutionKey.workspaceAlias) ?? [], resolutionKey, matcher)
  }
  if (!sourceWorkspaceAlias) return findIndexedEntry(index.entries, resolutionKey, matcher)

  const workspaceMatch = findIndexedEntry(index.entriesByWorkspaceAlias.get(sourceWorkspaceAlias) ?? [], resolutionKey, matcher)
  if (workspaceMatch) return workspaceMatch

  for (const entry of index.entries) {
    if (entry.workspaceAlias === sourceWorkspaceAlias) continue
    if (matcher(entry, resolutionKey)) return entry.entry
  }
  return undefined
}

function matchesPathSuffix(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
  return resolutionKey.pathSuffixes.some((pathSuffix) => entry.normalizedPath.endsWith(pathSuffix))
}

function matchesFilename(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
  return entry.filenameStem === resolutionKey.exactTarget
    || entry.filenameStem === resolutionKey.targetWithoutWorkspace
    || entry.filenameStem === resolutionKey.lastSegment
}

function matchesAlias(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
  return entry.aliases.some((alias) => (
    alias === resolutionKey.exactTarget || alias === resolutionKey.targetWithoutWorkspace
  ))
}

function matchesTitle(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
  return entry.title === resolutionKey.exactTarget
    || entry.title === resolutionKey.targetWithoutWorkspace
    || entry.title === resolutionKey.lastSegment
}

function matchesHumanizedTitle(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
  return !!resolutionKey.humanizedTarget && entry.title === resolutionKey.humanizedTarget
}

function resolveEntryFromIndex(
  index: ResolutionIndex,
  resolutionKey: ResolutionKey,
  sourceWorkspaceAlias: string | null,
): VaultEntry | undefined {
  return (
    (resolutionKey.pathSuffixes.length > 0
      ? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesPathSuffix)
      : undefined)
    ?? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesFilename)
    ?? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesAlias)
    ?? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesTitle)
    ?? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesHumanizedTitle)
  )
}

/**
 * Unified wikilink resolution: find the VaultEntry matching a wikilink target.
 * Handles pipe syntax, case-insensitive matching.
 * Resolution order (multi-pass, global priority):
 *   1. Path-suffix match (for path-style targets like "docs/adr/0031-foo")
 *   2. Filename stem match (strongest for flat vaults)
 *   3. Alias match
 *   4. Exact title match
 *   5. Humanized title match (kebab-case → words)
 */
export function resolveEntry(entries: VaultEntry[], rawTarget: WikilinkTarget, sourceEntry?: VaultEntry): VaultEntry | undefined {
  const index = resolutionIndexForEntries(entries)
  const resolutionKey = buildResolutionKey(rawTarget, index.workspaceAliases)
  const sourceWorkspaceAlias = workspaceAliasForEntry(sourceEntry)
  const cacheKey = resolutionCacheKey(resolutionKey, sourceWorkspaceAlias)
  if (index.resolutionCache.has(cacheKey)) return index.resolutionCache.get(cacheKey) ?? undefined

  const resolved = resolveEntryFromIndex(index, resolutionKey, sourceWorkspaceAlias)
  index.resolutionCache.set(cacheKey, resolved ?? null)
  return resolved
}
