#!/usr/bin/env node

import {
  existsSync,
  readFileSync,
  writeFileSync,
} from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';

import {
  parseFrontmatter,
  validateFrontendState,
  validateFrontendTransition,
} from './validate-changelog-frontmatter.mjs';

export const PATH_ALIAS_MANIFEST = 'changelog-path-aliases.json';
export const PATH_ALIAS_SCHEMA = 'hl-changelog-path-aliases/v1';

const CONTROLLED_ROOTS = new Set(['changelogs-v2', 'changelogs-v2-mp']);
const FRONTEND_FIELDS = [
  'frontend_status',
  'frontend_owner',
  'frontend_ref',
  'target_release',
  'verified_at',
];

function aliasError(code, inputPath, message) {
  return { code, path: inputPath, message };
}

export function normalizeRepoPath(value, label = 'path') {
  if (typeof value !== 'string' || value.length === 0 || /[\0\r\n]/.test(value)) {
    throw new Error(`${label} 必须是非空且不含控制字符的 Git 路径`);
  }
  if (value.includes('\\') || path.posix.isAbsolute(value) || /^[A-Za-z]:/.test(value)) {
    throw new Error(`${label} 必须使用仓库内 POSIX 相对路径`);
  }
  const normalized = path.posix.normalize(value);
  if (normalized !== value || normalized === '.' || normalized.startsWith('../')) {
    throw new Error(`${label} 不能包含 .、.. 或非规范路径片段`);
  }
  const [root] = normalized.split('/');
  if (!CONTROLLED_ROOTS.has(root) || !normalized.endsWith('.md')) {
    throw new Error(`${label} 必须指向受控 changelog Markdown`);
  }
  return normalized;
}

export function parsePathAliasManifest(text) {
  let value;
  try {
    value = JSON.parse(String(text));
  } catch (error) {
    throw new Error(`alias manifest 不是合法 JSON: ${error.message}`);
  }
  if (value?.schema !== PATH_ALIAS_SCHEMA || !Array.isArray(value.aliases)) {
    throw new Error(`alias manifest 必须使用 ${PATH_ALIAS_SCHEMA} 且包含 aliases 数组`);
  }

  const seenAliases = new Set();
  const aliases = value.aliases.map((entry, index) => {
    if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
      throw new Error(`aliases[${index}] 必须是对象`);
    }
    const ticket = String(entry.ticket ?? '');
    if (!/^[1-9]\d*$/.test(ticket)) {
      throw new Error(`aliases[${index}].ticket 必须是正整数`);
    }
    const alias = normalizeRepoPath(entry.alias, `aliases[${index}].alias`);
    const canonical = normalizeRepoPath(entry.canonical, `aliases[${index}].canonical`);
    if (alias === canonical) {
      throw new Error(`aliases[${index}] 的 alias 与 canonical 不能相同`);
    }
    if (seenAliases.has(alias)) {
      throw new Error(`alias 重复: ${alias}`);
    }
    if (typeof entry.reason !== 'string' || entry.reason.trim() === '') {
      throw new Error(`aliases[${index}].reason 不能为空`);
    }
    seenAliases.add(alias);
    return { ticket, alias, canonical, reason: entry.reason.trim() };
  });

  for (const entry of aliases) {
    if (seenAliases.has(entry.canonical)) {
      throw new Error(`暂不允许 alias 链或环: ${entry.alias} -> ${entry.canonical}`);
    }
  }
  return { schema: PATH_ALIAS_SCHEMA, aliases };
}

export function loadPathAliasManifest(root = process.cwd()) {
  const manifestPath = path.join(root, PATH_ALIAS_MANIFEST);
  if (!existsSync(manifestPath)) {
    return { schema: PATH_ALIAS_SCHEMA, aliases: [] };
  }
  return parsePathAliasManifest(readFileSync(manifestPath, 'utf8'));
}

export function resolveChangelogPath(inputPath, manifest) {
  const normalized = normalizeRepoPath(inputPath);
  return manifest.aliases.find(({ alias }) => alias === normalized)?.canonical ?? normalized;
}

function absoluteRepoPath(root, repoPath) {
  return path.join(root, ...repoPath.split('/'));
}

function readDocument(root, repoPath) {
  const absolute = absoluteRepoPath(root, repoPath);
  if (!existsSync(absolute)) {
    return { error: aliasError('E_ALIAS_MISSING', repoPath, '文件不存在') };
  }
  const content = readFileSync(absolute, 'utf8');
  const { metadata } = parseFrontmatter(content);
  if (!metadata) {
    return { error: aliasError('E_ALIAS_FRONTMATTER', repoPath, '缺少 YAML Front Matter') };
  }
  return { absolute, content, metadata };
}

export function validatePathAliases(root = process.cwd(), manifest = loadPathAliasManifest(root)) {
  const errors = [];
  for (const entry of manifest.aliases) {
    const aliasDocument = readDocument(root, entry.alias);
    const canonicalDocument = readDocument(root, entry.canonical);
    if (aliasDocument.error) {
      errors.push(aliasDocument.error);
      continue;
    }
    if (canonicalDocument.error) {
      errors.push(canonicalDocument.error);
      continue;
    }

    for (const [repoPath, metadata] of [
      [entry.alias, aliasDocument.metadata],
      [entry.canonical, canonicalDocument.metadata],
    ]) {
      if (metadata.schema !== 'hl-changelog/v2') {
        errors.push(aliasError('E_ALIAS_SCHEMA', repoPath, '兼容组文档必须使用 hl-changelog/v2'));
      }
      if (metadata.ticket !== entry.ticket) {
        errors.push(aliasError(
          'E_ALIAS_TICKET',
          repoPath,
          `ticket=${metadata.ticket || '(空)'}，manifest ticket=${entry.ticket}`,
        ));
      }
    }
    if (aliasDocument.metadata.canonical_path !== entry.canonical) {
      errors.push(aliasError(
        'E_ALIAS_TARGET',
        entry.alias,
        `canonical_path 必须等于 ${entry.canonical}`,
      ));
    }
    for (const field of FRONTEND_FIELDS) {
      if ((aliasDocument.metadata[field] ?? '') !== (canonicalDocument.metadata[field] ?? '')) {
        errors.push(aliasError(
          'E_ALIAS_STATE',
          entry.alias,
          `${field} 与 canonical 不一致`,
        ));
      }
    }
  }
  return { checkedCount: manifest.aliases.length, errors };
}

function shanghaiDate(now = new Date()) {
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'Asia/Shanghai',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(now);
  const values = Object.fromEntries(parts.map(({ type, value }) => [type, value]));
  return `${values.year}-${values.month}-${values.day}`;
}

function replaceFrontmatterFields(content, fields) {
  const normalized = String(content).replaceAll('\r\n', '\n');
  const end = normalized.indexOf('\n---\n', 4);
  if (!normalized.startsWith('---\n') || end < 0) {
    throw new Error('文档缺少可写的 YAML Front Matter');
  }
  const lines = normalized.slice(4, end).split('\n');
  const remaining = new Set(Object.keys(fields));
  const updated = lines.map((line) => {
    const separator = line.indexOf(':');
    if (separator < 0) {
      return line;
    }
    const key = line.slice(0, separator).trim();
    if (!remaining.has(key)) {
      return line;
    }
    remaining.delete(key);
    return `${key}: ${JSON.stringify(String(fields[key] ?? ''))}`;
  });
  if (remaining.size > 0) {
    throw new Error(`frontmatter 缺少待更新字段: ${[...remaining].join(', ')}`);
  }
  return `---\n${updated.join('\n')}\n---\n${normalized.slice(end + 5)}`;
}

export function transitionAliasedFrontendState(
  root,
  inputPath,
  {
    status,
    owner,
    frontendRef,
    targetRelease,
    verifiedAt,
    reason = '',
  },
  { now = new Date(), write = false } = {},
) {
  const manifest = loadPathAliasManifest(root);
  const before = validatePathAliases(root, manifest);
  if (before.errors.length > 0) {
    throw new Error(`alias manifest 校验失败: ${before.errors.map(({ message }) => message).join('；')}`);
  }
  const canonical = resolveChangelogPath(inputPath, manifest);
  const canonicalDocument = readDocument(root, canonical);
  if (canonicalDocument.error) {
    throw new Error(`${canonicalDocument.error.path}: ${canonicalDocument.error.message}`);
  }
  const current = canonicalDocument.metadata.frontend_status;
  const transitionErrors = validateFrontendTransition(current, status, reason);
  if (transitionErrors.length > 0) {
    throw new Error(transitionErrors.join('；'));
  }

  const fields = {
    frontend_status: status,
    frontend_owner: owner ?? canonicalDocument.metadata.frontend_owner ?? '',
    frontend_ref: frontendRef ?? canonicalDocument.metadata.frontend_ref ?? '',
    target_release: targetRelease ?? canonicalDocument.metadata.target_release ?? '',
    verified_at: verifiedAt ?? canonicalDocument.metadata.verified_at ?? '',
    updated_at: shanghaiDate(now),
  };
  const stateErrors = validateFrontendState(fields);
  if (stateErrors.length > 0) {
    throw new Error(stateErrors.join('；'));
  }

  const group = [
    canonical,
    ...manifest.aliases
      .filter((entry) => entry.canonical === canonical)
      .map((entry) => entry.alias),
  ];
  const outputs = group.map((repoPath) => {
    const document = readDocument(root, repoPath);
    if (document.error) {
      throw new Error(`${document.error.path}: ${document.error.message}`);
    }
    const content = replaceFrontmatterFields(document.content, fields);
    return {
      repoPath,
      absolute: document.absolute,
      content,
      changed: content !== document.content.replaceAll('\r\n', '\n'),
    };
  });
  if (write) {
    for (const output of outputs.filter(({ changed }) => changed)) {
      writeFileSync(output.absolute, output.content, 'utf8');
    }
    const after = validatePathAliases(root, manifest);
    if (after.errors.length > 0) {
      throw new Error(`写入后 alias 状态不一致: ${after.errors.map(({ message }) => message).join('；')}`);
    }
  }
  return {
    canonical,
    paths: group,
    changedPaths: outputs.filter(({ changed }) => changed).map(({ repoPath }) => repoPath),
    metadata: fields,
    write,
  };
}

function parseCli(argv) {
  if (argv.length === 0 || (argv.length === 1 && argv[0] === '--check')) {
    return { command: 'check' };
  }
  if (argv[0] === '--resolve' && argv.length === 2) {
    return { command: 'resolve', path: argv[1] };
  }
  if (argv[0] !== '--transition') {
    throw new Error('用法: --check | --resolve <path> | --transition <path> <status> [options]');
  }
  if (argv.length < 3) {
    throw new Error('--transition 需要 path 和 status');
  }
  const options = { command: 'transition', path: argv[1], status: argv[2], write: false };
  const mapping = new Map([
    ['--owner', 'owner'],
    ['--frontend-ref', 'frontendRef'],
    ['--target-release', 'targetRelease'],
    ['--verified-at', 'verifiedAt'],
    ['--reason', 'reason'],
  ]);
  for (let index = 3; index < argv.length; index += 1) {
    const flag = argv[index];
    if (flag === '--write') {
      options.write = true;
      continue;
    }
    const key = mapping.get(flag);
    const value = argv[index + 1];
    if (!key || value === undefined) {
      throw new Error(`不支持或不完整的参数: ${flag}`);
    }
    options[key] = value;
    index += 1;
  }
  return options;
}

export function main(argv = process.argv.slice(2), root = process.cwd()) {
  try {
    const options = parseCli(argv);
    const manifest = loadPathAliasManifest(root);
    if (options.command === 'check') {
      const result = validatePathAliases(root, manifest);
      if (result.errors.length > 0) {
        for (const error of result.errors) {
          console.error(`[${error.code}] ${error.path}: ${error.message}`);
        }
        console.error(`FAIL: ${result.errors.length} path alias error(s).`);
        return 1;
      }
      console.log(`PASS: validated ${result.checkedCount} changelog path alias(es).`);
      return 0;
    }
    if (options.command === 'resolve') {
      console.log(resolveChangelogPath(options.path, manifest));
      return 0;
    }
    const result = transitionAliasedFrontendState(root, options.path, options, {
      write: options.write,
    });
    console.log(JSON.stringify(result, null, 2));
    return 0;
  } catch (error) {
    console.error(`ERROR: ${error instanceof Error ? error.message : String(error)}`);
    return 2;
  }
}

const isCli = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
if (isCli) {
  process.exitCode = main();
}
