const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');

const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');

const log = createLogger('format:check:changed');

function runGit(args, cwd, options = {}) {
  const { allowFailure = false, stdio } = options;
  try {
    return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: stdio || 'pipe' }).trim();
  } catch (error) {
    if (allowFailure) return null;
    throw error;
  }
}

function tryReadGithubEvent(eventPath) {
  if (!eventPath) return null;
  try {
    const raw = fs.readFileSync(eventPath, 'utf8');
    return JSON.parse(raw);
  } catch {
    return null;
  }
}

function isAllZerosSha(value) {
  return typeof value === 'string' && /^0{40}$/.test(value);
}

function getDiffRangeFromGithubEvent(event) {
  if (!event || typeof event !== 'object') return null;

  if (event.pull_request && event.pull_request.base && event.pull_request.head) {
    const base = event.pull_request.base.sha;
    const head = event.pull_request.head.sha;
    if (base && head) return { base, head };
  }

  if (event.before && (event.after || event.head_commit)) {
    const base = event.before;
    const head = event.after || (event.head_commit && event.head_commit.id);
    if (base && head && !isAllZerosSha(base)) return { base, head };
  }

  return null;
}

function gitObjectExists(repoRoot, sha) {
  if (!sha) return false;
  const result = runGit(['cat-file', '-e', `${sha}^{commit}`], repoRoot, { allowFailure: true });
  return result !== null;
}

function isShallowRepository(repoRoot) {
  const result = runGit(['rev-parse', '--is-shallow-repository'], repoRoot, { allowFailure: true });
  return result === 'true';
}

function tryFetchMoreHistory(repoRoot) {
  // 仅在 CI 场景兜底：actions/checkout 若是浅克隆，可能缺少 base commit，导致 diff range 失败
  try {
    if (isShallowRepository(repoRoot)) {
      execFileSync('git', ['fetch', '--prune', '--no-tags', '--unshallow'], {
        cwd: repoRoot,
        stdio: 'inherit',
      });
      return true;
    }
  } catch {
    // ignore
  }

  try {
    execFileSync('git', ['fetch', '--prune', '--no-tags', '--depth=200', 'origin'], {
      cwd: repoRoot,
      stdio: 'inherit',
    });
    return true;
  } catch {
    return false;
  }
}

function collectHeadChangedFiles(repoRoot) {
  const output = runGit(
    ['show', '--name-only', '--diff-filter=ACMR', '--pretty=format:', 'HEAD'],
    repoRoot,
    { allowFailure: true }
  );

  if (!output) return [];

  return output
    .split('\n')
    .map((line) => line.trim())
    .filter(Boolean);
}

function collectChangedFiles(repoRoot, range) {
  if (!range) return [];

  const diffArgs = ['diff', '--name-only', '--diff-filter=ACMR', `${range.base}..${range.head}`];

  const baseExists = gitObjectExists(repoRoot, range.base);
  const headExists = gitObjectExists(repoRoot, range.head);
  if (!baseExists || !headExists) {
    log.warn('检测到 diff range 所需提交缺失，尝试补全 git 历史（避免浅克隆导致失败）');
    tryFetchMoreHistory(repoRoot);
  }

  const output = runGit(diffArgs, repoRoot, { allowFailure: true });
  if (!output) {
    log.warn('无法计算 revision range，回退为 HEAD 变更文件（可能仅覆盖最后一次提交）');
    return collectHeadChangedFiles(repoRoot);
  }

  return output
    .split('\n')
    .map((line) => line.trim())
    .filter(Boolean);
}

function collectWorkingTreeChangedFiles(repoRoot) {
  const files = new Set();
  const unstaged = runGit(['diff', '--name-only', '--diff-filter=ACMR', 'HEAD'], repoRoot, {
    allowFailure: true,
  });
  const staged = runGit(['diff', '--cached', '--name-only', '--diff-filter=ACMR'], repoRoot, {
    allowFailure: true,
  });

  [unstaged, staged].forEach((block) => {
    if (!block) return;
    block
      .split('\n')
      .map((line) => line.trim())
      .filter(Boolean)
      .forEach((filePath) => files.add(filePath));
  });

  return Array.from(files).sort();
}

function shouldCheckFile(filePath) {
  const normalized = filePath.split(path.sep).join('/');

  if (normalized === 'package-lock.json') return false;

  // 这些文件历史上未统一为 Prettier 风格；避免为了启用检查产生巨量格式化 diff
  if (normalized === 'src/generator/main.js' || normalized === 'src/runtime/index.js') return false;

  // 与现有 npm scripts 的检查范围对齐：不检查 docs/ 与 templates/
  const allowedRoots = ['src/', 'scripts/', 'test/', '.github/', 'config/'];
  const isRootFile = !normalized.includes('/');
  const hasAllowedRoot = allowedRoots.some((prefix) => normalized.startsWith(prefix));

  const isAllowedPath =
    hasAllowedRoot || (isRootFile && (normalized.endsWith('.md') || normalized.endsWith('.json')));

  if (!isAllowedPath) return false;

  const ext = path.extname(normalized).toLowerCase();
  return ['.js', '.json', '.md', '.yml', '.yaml'].includes(ext);
}

function resolvePrettierBin(repoRoot) {
  const base = path.join(repoRoot, 'node_modules', '.bin', 'prettier');
  if (fs.existsSync(base)) return base;
  if (fs.existsSync(`${base}.cmd`)) return `${base}.cmd`;
  return null;
}

function main() {
  const repoRoot = path.resolve(__dirname, '..');
  const elapsedMs = startTimer();

  log.info('开始');

  const event = tryReadGithubEvent(process.env.GITHUB_EVENT_PATH);
  const range = getDiffRangeFromGithubEvent(event);

  const candidateFiles = range
    ? collectChangedFiles(repoRoot, range)
    : collectWorkingTreeChangedFiles(repoRoot);

  const filesToCheck = candidateFiles.filter(shouldCheckFile);

  if (filesToCheck.length === 0) {
    log.ok('未发现需要检查的文件，跳过');
    return;
  }

  const prettierBin = resolvePrettierBin(repoRoot);
  if (!prettierBin) {
    log.error('未找到 prettier，可先运行 npm ci / npm install');
    process.exitCode = 1;
    return;
  }

  log.info('准备检查文件格式', { files: filesToCheck.length });
  if (isVerbose()) {
    filesToCheck.forEach((filePath) => log.info('待检查', { file: filePath }));
  }

  try {
    execFileSync(prettierBin, ['--check', ...filesToCheck], { cwd: repoRoot, stdio: 'inherit' });
    log.ok('通过', { ms: elapsedMs(), files: filesToCheck.length });
  } catch (error) {
    log.error('未通过', {
      ms: elapsedMs(),
      files: filesToCheck.length,
      exit: error && error.status ? error.status : 1,
    });
    process.exitCode = error && error.status ? error.status : 1;
  }
}

main();
