import { readFile, readdir, stat } from 'node:fs/promises'
import { dirname, join, parse, relative } from 'node:path'
import {
  CODE_LOCAL_MD,
  CODE_MD,
  CODE_MD_IN_CLAUDE_DIR,
  MAX_MEMORY_CHARACTER_COUNT,
  MEMORY_INSTRUCTION_PROMPT,
  RULES_DIR,
} from './constants.js'
import type { MemoryFileInfo, MemoryType } from './types.js'

type LoadOptions = {
  projectRoot: string
  excludes?: string[]
  maxChars?: number
}

/** 简单 frontmatter 剥离（paths 行忽略，全文作为 content） */
function stripFrontmatter(raw: string): { content: string; globs?: string[] } {
  if (!raw.startsWith('---')) {
    return { content: raw }
  }
  const end = raw.indexOf('---', 3)
  if (end === -1) {
    return { content: raw }
  }
  const body = raw.slice(end + 3).replace(/^\n/, '')
  const fm = raw.slice(3, end)
  const pathsMatch = fm.match(/^paths:\s*(.+)$/m)
  if (!pathsMatch) {
    return { content: body }
  }
  const paths = pathsMatch[1]
    .split(',')
    .map(p => p.trim())
    .filter(p => p.length > 0 && p !== '**')
  return { content: body, globs: paths.length > 0 ? paths : undefined }
}

function normalizePath(p: string): string {
  return p.replaceAll('\\', '/')
}

/** 路径是否被 exclude  glob 命中（简单前缀/包含匹配） */
function isExcluded(filePath: string, excludes: string[]): boolean {
  if (excludes.length === 0) return false
  const norm = normalizePath(filePath)
  for (const pat of excludes) {
    const p = normalizePath(pat.trim())
    if (!p) continue
    if (p.includes('*')) {
      if (norm.includes(p.replace(/\*\*/g, '').replace(/\*/g, ''))) return true
    } else if (norm === p || norm.endsWith(p)) {
      return true
    }
  }
  return false
}

async function fileExists(path: string): Promise<boolean> {
  try {
    await stat(path)
    return true
  } catch {
    return false
  }
}

async function readMemoryFile(
  filePath: string,
  type: MemoryType,
): Promise<MemoryFileInfo | null> {
  try {
    const raw = await readFile(filePath, 'utf8')
    const trimmed = raw.trim()
    if (!trimmed) return null
    const { content, globs } = stripFrontmatter(raw)
    if (!content.trim()) return null
    return { path: filePath, type, content, globs }
  } catch {
    return null
  }
}

/** 递归加载 .claude/rules 下无条件 .md */
async function loadRulesDir(
  rulesDir: string,
  type: MemoryType,
  processed: Set<string>,
): Promise<MemoryFileInfo[]> {
  const out: MemoryFileInfo[] = []
  let entries: import('node:fs').Dirent[]
  try {
    entries = await readdir(rulesDir, { withFileTypes: true })
  } catch {
    return out
  }
  for (const e of entries) {
    const p = join(rulesDir, e.name)
    if (e.isDirectory()) {
      out.push(...(await loadRulesDir(p, type, processed)))
    } else if (e.isFile() && e.name.endsWith('.md')) {
      const key = normalizePath(p)
      if (processed.has(key)) continue
      processed.add(key)
      const info = await readMemoryFile(p, type)
      if (info && !info.globs) out.push(info)
    }
  }
  return out
}

/**
 * 从 projectRoot 向上遍历，加载 code.md / code.local.md / .claude/code.md / rules。
 * 业务语义：替代宿主 claudemd.getMemoryFiles + getClaudeMds，仅认 code.md 文件名。
 */
export async function collectMemoryFiles(
  projectRoot: string,
  excludes: string[] = [],
): Promise<MemoryFileInfo[]> {
  const result: MemoryFileInfo[] = []
  const processed = new Set<string>()

  const dirs: string[] = []
  let cur = projectRoot
  const root = parse(cur).root
  while (cur !== root) {
    dirs.push(cur)
    cur = dirname(cur)
  }
  dirs.reverse()

  for (const dir of dirs) {
    const candidates: { path: string; type: MemoryType }[] = [
      { path: join(dir, CODE_MD), type: 'Project' },
      { path: join(dir, '.claude', 'code.md'), type: 'Project' },
      { path: join(dir, CODE_LOCAL_MD), type: 'Local' },
    ]
    for (const { path, type } of candidates) {
      if (isExcluded(path, excludes)) continue
      const key = normalizePath(path)
      if (processed.has(key)) continue
      if (!(await fileExists(path))) continue
      processed.add(key)
      const info = await readMemoryFile(path, type)
      if (info) result.push(info)
    }
    const rulesPath = join(dir, RULES_DIR)
    result.push(...(await loadRulesDir(rulesPath, 'Project', processed)))
  }

  return result
}

export function formatCodeMd(memoryFiles: MemoryFileInfo[]): string {
  const parts: string[] = []
  for (const file of memoryFiles) {
    const content = file.content.trim()
    if (!content) continue
    const desc =
      file.type === 'Project'
        ? ' (project instructions, checked into the codebase)'
        : " (user's private project instructions, not checked in)"
    parts.push(`Contents of ${file.path}${desc}:\n\n${content}`)
  }
  if (parts.length === 0) return ''
  return `${MEMORY_INSTRUCTION_PROMPT}\n\n${parts.join('\n\n')}`
}

/**
 * 加载并格式化 code.md 记忆文本。
 */
export async function loadCodeMd(options: LoadOptions): Promise<string> {
  const files = await collectMemoryFiles(
    options.projectRoot,
    options.excludes ?? [],
  )
  let text = formatCodeMd(files)
  const max = options.maxChars ?? MAX_MEMORY_CHARACTER_COUNT
  if (text.length > max) {
    text = `${text.slice(0, max)}\n\n[truncated: code memory exceeded ${max} characters]`
  }
  return text
}

/** inspect 用：是否已有根 code.md */
export async function hasRootCodeMd(projectRoot: string): Promise<boolean> {
  return fileExists(join(projectRoot, CODE_MD))
}

export function getCodeMdPath(projectRoot: string): string {
  return join(projectRoot, CODE_MD)
}
