import { readdir, readFile, stat, lstat, realpath } from 'fs/promises'
import { existsSync } from 'fs'
import { dirname, join, sep } from 'path'
import { resolveWithin } from '@/lib/paths'
import { config } from '@/lib/config'

const DOC_ROOT_CANDIDATES = ['docs', 'knowledge-base', 'knowledge', 'memory']

export interface DocsTreeNode {
  path: string
  name: string
  type: 'file' | 'directory'
  size?: number
  modified?: number
  children?: DocsTreeNode[]
}

function normalizeRelativePath(value: string): string {
  return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '')
}

function isWithinBase(base: string, candidate: string): boolean {
  if (candidate === base) return true
  return candidate.startsWith(base + sep)
}

async function resolveSafePath(baseDir: string, relativePath: string): Promise<string> {
  const baseReal = await realpath(baseDir)
  const fullPath = resolveWithin(baseDir, relativePath)

  let parentReal: string
  try {
    parentReal = await realpath(dirname(fullPath))
  } catch (err) {
    const code = (err as NodeJS.ErrnoException).code
    if (code === 'ENOENT') throw new Error('Parent directory not found')
    throw err
  }

  if (!isWithinBase(baseReal, parentReal)) {
    throw new Error('Path escapes base directory (symlink)')
  }

  try {
    const st = await lstat(fullPath)
    if (st.isSymbolicLink()) throw new Error('Symbolic links are not allowed')
    const fileReal = await realpath(fullPath)
    if (!isWithinBase(baseReal, fileReal)) {
      throw new Error('Path escapes base directory (symlink)')
    }
  } catch (err) {
    const code = (err as NodeJS.ErrnoException).code
    if (code !== 'ENOENT') throw err
  }

  return fullPath
}

function allowedRoots(baseDir: string): string[] {
  const candidateRoots = DOC_ROOT_CANDIDATES.filter((root) => existsSync(join(baseDir, root)))
  if (candidateRoots.length > 0) return candidateRoots

  const fromConfig = (config.memoryAllowedPrefixes || [])
    .map((prefix) => normalizeRelativePath(prefix).replace(/\/$/, ''))
    .filter((prefix) => prefix.length > 0)
    .filter((prefix) => existsSync(join(baseDir, prefix)))

  return fromConfig
}

export function listDocsRoots(): string[] {
  const baseDir = config.memoryDir
  if (!baseDir || !existsSync(baseDir)) return []
  return allowedRoots(baseDir)
}

export function isDocsPathAllowed(relativePath: string): boolean {
  const normalized = normalizeRelativePath(relativePath)
  if (!normalized) return false

  const baseDir = config.memoryDir
  if (!baseDir || !existsSync(baseDir)) return false

  const roots = allowedRoots(baseDir)
  if (roots.length === 0) return false

  return roots.some((root) => normalized === root || normalized.startsWith(`${root}/`))
}

async function buildTreeFrom(dirPath: string, relativeBase: string): Promise<DocsTreeNode[]> {
  const items = await readdir(dirPath, { withFileTypes: true })
  const nodes: DocsTreeNode[] = []

  for (const item of items) {
    if (item.isSymbolicLink()) continue
    const fullPath = join(dirPath, item.name)
    const relativePath = normalizeRelativePath(join(relativeBase, item.name))

    try {
      const info = await stat(fullPath)
      if (item.isDirectory()) {
        const children = await buildTreeFrom(fullPath, relativePath)
        nodes.push({
          path: relativePath,
          name: item.name,
          type: 'directory',
          modified: info.mtime.getTime(),
          children,
        })
      } else if (item.isFile()) {
        nodes.push({
          path: relativePath,
          name: item.name,
          type: 'file',
          size: info.size,
          modified: info.mtime.getTime(),
        })
      }
    } catch {
      // Ignore unreadable files
    }
  }

  return nodes.sort((a, b) => {
    if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
    return a.name.localeCompare(b.name)
  })
}

export async function getDocsTree(): Promise<DocsTreeNode[]> {
  const baseDir = config.memoryDir
  if (!baseDir || !existsSync(baseDir)) return []

  const roots = allowedRoots(baseDir)
  const tree: DocsTreeNode[] = []

  for (const root of roots) {
    const rootPath = join(baseDir, root)
    try {
      const info = await stat(rootPath)
      if (!info.isDirectory()) continue
      tree.push({
        path: root,
        name: root,
        type: 'directory',
        modified: info.mtime.getTime(),
        children: await buildTreeFrom(rootPath, root),
      })
    } catch {
      // Ignore unreadable roots
    }
  }

  return tree
}

export async function readDocsContent(relativePath: string): Promise<{ content: string; size: number; modified: number; path: string }> {
  if (!isDocsPathAllowed(relativePath)) {
    throw new Error('Path not allowed')
  }

  const baseDir = config.memoryDir
  if (!baseDir || !existsSync(baseDir)) {
    throw new Error('Docs directory not configured')
  }

  const safePath = await resolveSafePath(baseDir, relativePath)
  const content = await readFile(safePath, 'utf-8')
  const info = await stat(safePath)

  return {
    content,
    size: info.size,
    modified: info.mtime.getTime(),
    path: normalizeRelativePath(relativePath),
  }
}

function isSearchable(name: string): boolean {
  return name.endsWith('.md') || name.endsWith('.txt')
}

export async function searchDocs(query: string, limit = 100): Promise<Array<{ path: string; name: string; matches: number }>> {
  const baseDir = config.memoryDir
  if (!baseDir || !existsSync(baseDir)) return []

  const roots = allowedRoots(baseDir)
  if (roots.length === 0) return []

  const q = query.trim().toLowerCase()
  if (!q) return []

  const results: Array<{ path: string; name: string; matches: number }> = []

  const searchFile = async (fullPath: string, relativePath: string) => {
    try {
      const info = await stat(fullPath)
      if (info.size > 1_000_000) return
      const content = (await readFile(fullPath, 'utf-8')).toLowerCase()
      let count = 0
      let idx = content.indexOf(q)
      while (idx !== -1) {
        count += 1
        idx = content.indexOf(q, idx + q.length)
      }
      if (count > 0) {
        results.push({
          path: normalizeRelativePath(relativePath),
          name: relativePath.split('/').pop() || relativePath,
          matches: count,
        })
      }
    } catch {
      // Ignore unreadable files
    }
  }

  const searchDir = async (fullDir: string, relativeDir: string) => {
    const items = await readdir(fullDir, { withFileTypes: true })
    for (const item of items) {
      if (item.isSymbolicLink()) continue
      const itemFull = join(fullDir, item.name)
      const itemRel = normalizeRelativePath(join(relativeDir, item.name))
      if (item.isDirectory()) {
        await searchDir(itemFull, itemRel)
      } else if (item.isFile() && isSearchable(item.name.toLowerCase())) {
        await searchFile(itemFull, itemRel)
      }
    }
  }

  for (const root of roots) {
    const rootPath = join(baseDir, root)
    try {
      await searchDir(rootPath, root)
    } catch {
      // Ignore unreadable roots
    }
  }

  return results.sort((a, b) => b.matches - a.matches).slice(0, Math.max(1, Math.min(limit, 200)))
}
