import { execFileSync, spawnSync } from 'node:child_process'
import { existsSync, lstatSync } from 'node:fs'
import { resolve } from 'node:path'
import process from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import * as prettier from 'prettier'

const DEFAULT_BASE_REFS = ['refs/remotes/upstream/v2', 'refs/remotes/origin/v2', 'refs/heads/v2']
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024
const PRETTIER_ARGUMENT_BUDGET = 24_000
const prettierCliPath = fileURLToPath(new URL('../node_modules/prettier/bin/prettier.cjs', import.meta.url))

/** 解析 Git 的 NUL 分隔输出，保留文件名中的空格、换行和通配符字符。 */
function parseNullDelimited(output) {
  return output.toString('utf8').split('\0').filter(Boolean)
}

/** 运行只返回仓库相对路径的 Git 查询。 */
function gitPaths(cwd, args) {
  return parseNullDelimited(
    execFileSync('git', args, {
      cwd,
      encoding: 'buffer',
      maxBuffer: GIT_OUTPUT_MAX_BUFFER,
    }),
  )
}

/** 尝试把引用解析为提交 SHA；完整引用可避免分支与标签同名时产生歧义。 */
function tryResolveCommit(cwd, ref) {
  const result = spawnSync('git', ['rev-parse', '--verify', '--quiet', '--end-of-options', `${ref}^{commit}`], {
    cwd,
    encoding: 'utf8',
  })
  return result.status === 0 ? result.stdout.trim() : undefined
}

/** 把外部引用解析为提交 SHA，后续 Git diff 不再解释用户提供的选项样式字符串。 */
function resolveCommit(cwd, ref) {
  const commit = tryResolveCommit(cwd, ref)
  if (commit) return commit
  throw new Error(`无法解析 Git 提交引用：${ref}`)
}

/** 判断两个提交能否进行三点比较。 */
function hasMergeBase(cwd, baseCommit, headCommit) {
  return spawnSync('git', ['merge-base', baseCommit, headCommit], { cwd, stdio: 'ignore' }).status === 0
}

/** 为本地命令选择与当前提交具有共同祖先的 v2 基线。 */
function resolveDefaultBase(cwd, headCommit) {
  for (const ref of DEFAULT_BASE_REFS) {
    const commit = tryResolveCommit(cwd, ref)
    if (commit && hasMergeBase(cwd, commit, headCommit)) return commit
  }

  throw new Error('无法定位可比较的 v2 基线，请通过 --base <ref> 显式指定比较起点。')
}

/**
 * 收集当前分支相对基线的文件集合。
 * 显式 base/head 模式只读取提交区间，默认本地模式还会合并暂存区、工作区和未跟踪文件。
 */
function collectChangedPaths({ cwd, base, head }) {
  if (Boolean(base) !== Boolean(head)) {
    throw new Error('--base 与 --head 必须一起使用。')
  }

  const explicitRange = Boolean(base)
  const headCommit = resolveCommit(cwd, head ?? 'HEAD')
  const baseCommit = base ? resolveCommit(cwd, base) : resolveDefaultBase(cwd, headCommit)
  if (!hasMergeBase(cwd, baseCommit, headCommit)) {
    throw new Error('Git 比较起点与终点没有共同祖先，无法执行三点比较。')
  }
  const paths = new Set(
    gitPaths(cwd, ['diff', '--name-only', '--diff-filter=ACMRT', '-z', `${baseCommit}...${headCommit}`]),
  )

  if (!explicitRange) {
    for (const filePath of gitPaths(cwd, ['diff', '--name-only', '--diff-filter=ACMRT', '-z', 'HEAD'])) {
      paths.add(filePath)
    }
    for (const filePath of gitPaths(cwd, ['ls-files', '--others', '--exclude-standard', '-z'])) {
      paths.add(filePath)
    }
  }

  return [...paths]
    .filter(filePath => {
      const absolutePath = resolve(cwd, filePath)
      if (!existsSync(absolutePath)) return false
      try {
        return lstatSync(absolutePath).isFile()
      } catch {
        return false
      }
    })
    .sort()
}

/** 使用仓库 ignore 文件和 Prettier parser 推断筛出真正受格式化工具管理的文件。 */
async function selectPrettierFiles(filePaths, cwd) {
  const ignorePaths = ['.gitignore', '.prettierignore'].map(filePath => resolve(cwd, filePath)).filter(existsSync)
  const selected = []

  for (const filePath of filePaths) {
    const absolutePath = resolve(cwd, filePath)
    const fileInfo = await prettier.getFileInfo(absolutePath)
    if (!fileInfo.inferredParser) continue

    let ignored = false
    for (const ignorePath of ignorePaths) {
      if ((await prettier.getFileInfo(absolutePath, { ignorePath })).ignored) {
        ignored = true
        break
      }
    }

    if (!ignored) selected.push(filePath)
  }

  return selected
}

/** 按保守的参数长度预算拆批，避免大型 PR 超过操作系统命令行上限。 */
function batchPaths(filePaths) {
  const batches = []
  let batch = []
  let length = 0

  for (const filePath of filePaths) {
    if (batch.length > 0 && length + filePath.length + 1 > PRETTIER_ARGUMENT_BUDGET) {
      batches.push(batch)
      batch = []
      length = 0
    }
    batch.push(filePath)
    length += filePath.length + 1
  }

  if (batch.length > 0) batches.push(batch)
  return batches
}

/** 通过参数数组调用项目内 Prettier，避免 shell 展开或拼接特殊文件名。 */
function runPrettier(filePaths, mode, cwd) {
  if (filePaths.length === 0) {
    console.log('没有需要 Prettier 处理的变更文件。')
    return
  }

  for (const batch of batchPaths(filePaths)) {
    const result = spawnSync(process.execPath, [prettierCliPath, mode, '--ignore-unknown', '--', ...batch], {
      cwd,
      stdio: 'inherit',
    })
    if (result.status !== 0) {
      throw new Error(`Prettier ${mode} 失败，退出码：${result.status ?? 'unknown'}`)
    }
  }
}

/** 解析面向开发者和 CI 的稳定命令行契约。 */
function parseArguments(args) {
  const options = { action: undefined, base: undefined, head: undefined }

  for (let index = 0; index < args.length; index += 1) {
    const argument = args[index]
    if (['--check', '--write', '--list'].includes(argument)) {
      if (options.action) throw new Error('只能指定 --check、--write 或 --list 中的一种操作。')
      options.action = argument
      continue
    }
    if (argument === '--base' || argument === '--head') {
      const value = args[index + 1]
      if (!value) throw new Error(`${argument} 缺少引用值。`)
      options[argument.slice(2)] = value
      index += 1
      continue
    }
    throw new Error(`未知参数：${argument}`)
  }

  if (!options.action) throw new Error('必须指定 --check、--write 或 --list。')
  return options
}

/** 执行变更文件选择与格式化；显式引用模式为 CI 提供确定输入。 */
async function main() {
  const cwd = process.cwd()
  const { action, base, head } = parseArguments(process.argv.slice(2))
  const changedPaths = collectChangedPaths({ cwd, base, head })
  const selectedPaths = await selectPrettierFiles(changedPaths, cwd)

  if (action === '--list') {
    process.stdout.write(`${JSON.stringify(selectedPaths)}\n`)
    return
  }

  runPrettier(selectedPaths, action, cwd)
}

if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
  main().catch(error => {
    console.error(error instanceof Error ? error.message : error)
    process.exitCode = 1
  })
}
