import fs from 'node:fs';
import path from 'node:path';

const COMPONENT_SECTION_KEYS = [
  ['description', '说明'],
  ['props', 'Props'],
  ['rules', '规则'],
  ['examples', '示例'],
  ['usage', '用法'],
  ['tokens', 'Token'],
  ['keywords', '关键词'],
  ['import', '导入方式'],
  ['starter', 'Starter'],
  ['config', '配置'],
  ['states', '状态'],
];

const SOURCE_ROOTS = ['src', 'app', 'pages', 'components'];

/**
 * @param {string} pkgRoot
 * @param {string} skillName
 */
export function resolveSkillDir(pkgRoot, skillName) {
  const candidates = [
    path.join(pkgRoot, 'skills', skillName),
    path.join(pkgRoot, 'skills', 'tfds'),
    path.join(pkgRoot, 'skills', 'TFDS'),
  ];
  return candidates.find((dir) => fs.existsSync(dir)) || path.join(pkgRoot, 'skills', skillName);
}

/**
 * @param {string} dir
 * @param {string} skillName
 */
export function loadLocalComponentsIndex(pkgRoot, skillName) {
  const skillDir = resolveSkillDir(pkgRoot, skillName);
  const indexPath = path.join(skillDir, 'components.index.json');
  if (!fs.existsSync(indexPath)) return null;
  try {
    return JSON.parse(fs.readFileSync(indexPath, 'utf8'));
  } catch {
    return null;
  }
}

/**
 * @param {string} packageName
 * @param {string} version
 * @param {string} skillName
 */
export async function fetchPublishedComponentsIndex(packageName, version, skillName) {
  const url = `https://unpkg.com/${packageName}@${version}/skills/${skillName}/components.index.json`;
  const response = await fetch(url, {
    headers: { accept: 'application/json' },
    signal: AbortSignal.timeout(20_000),
  });
  if (!response.ok) {
    throw new Error(`无法拉取 ${packageName}@${version} 组件索引（${response.status}）`);
  }
  return response.json();
}

/**
 * @param {unknown} indexJson
 */
export function getComponentEntryMap(indexJson) {
  const entries = Array.isArray(indexJson?.components) ? indexJson.components : [];
  return new Map(
    entries.map((entry) => {
      const key = entry?.name || entry?.id;
      return [key, entry];
    }),
  );
}

/**
 * @param {Record<string, unknown>|undefined} oldEntry
 * @param {Record<string, unknown>|undefined} nextEntry
 */
export function getChangedEntrySections(oldEntry, nextEntry) {
  const changed = COMPONENT_SECTION_KEYS.filter(
    ([key]) => JSON.stringify(oldEntry?.[key]) !== JSON.stringify(nextEntry?.[key]),
  ).map(([, label]) => label);
  return changed.length > 0 ? changed : ['组件索引'];
}

/**
 * @param {unknown} oldIndex
 * @param {unknown} nextIndex
 */
export function diffComponentIndexes(oldIndex, nextIndex) {
  const oldMap = getComponentEntryMap(oldIndex);
  const nextMap = getComponentEntryMap(nextIndex);
  const allNames = new Set([...oldMap.keys(), ...nextMap.keys()]);

  /** @type {Array<{ name: string, kind?: string, changeType: string, changedSections: string[], description?: string }>} */
  const changes = [];

  for (const name of [...allNames].sort((a, b) => String(a).localeCompare(String(b)))) {
    const oldEntry = oldMap.get(name);
    const nextEntry = nextMap.get(name);

    if (!oldEntry && nextEntry) {
      changes.push({
        name,
        kind: nextEntry.kind,
        changeType: 'added',
        changedSections: ['新增组件/模板'],
        description: nextEntry.description,
      });
      continue;
    }
    if (oldEntry && !nextEntry) {
      changes.push({
        name,
        kind: oldEntry.kind,
        changeType: 'removed',
        changedSections: ['已移除'],
        description: oldEntry.description,
      });
      continue;
    }
    if (JSON.stringify(oldEntry) !== JSON.stringify(nextEntry)) {
      changes.push({
        name,
        kind: nextEntry.kind,
        changeType: 'updated',
        changedSections: getChangedEntrySections(oldEntry, nextEntry),
        description: nextEntry.description,
      });
    }
  }

  return {
    generatedAtFrom: oldIndex?.generatedAt || null,
    generatedAtTo: nextIndex?.generatedAt || null,
    summaryFrom: oldIndex?.summary || null,
    summaryTo: nextIndex?.summary || null,
    changes,
    updated: changes.filter((c) => c.changeType === 'updated'),
    added: changes.filter((c) => c.changeType === 'added'),
    removed: changes.filter((c) => c.changeType === 'removed'),
  };
}

/**
 * @param {string} projectRoot
 * @param {string} packageName
 */
export function scanUsedTfdsImports(projectRoot, packageName) {
  const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const namedImport = new RegExp(
    `import\\s*\\{([^}]+)\\}\\s*from\\s*['"]${escaped}['"]`,
    'g',
  );
  const defaultImport = new RegExp(
    `import\\s+(\\w+)\\s+from\\s*['"]${escaped}['"]`,
    'g',
  );

  /** @type {Map<string, Set<string>>} */
  const usage = new Map();

  const files = SOURCE_ROOTS.flatMap((rel) => listSourceFiles(path.join(projectRoot, rel)));

  for (const file of files) {
    let content;
    try {
      content = fs.readFileSync(file, 'utf8');
    } catch {
      continue;
    }
    const relFile = path.relative(projectRoot, file);

    let match;
    while ((match = namedImport.exec(content))) {
      for (const rawName of match[1].split(',')) {
        const importedName = rawName.trim().split(/\s+as\s+/i)[0]?.trim();
        if (!importedName) continue;
        if (!usage.has(importedName)) usage.set(importedName, new Set());
        usage.get(importedName).add(relFile);
      }
    }
    while ((match = defaultImport.exec(content))) {
      const importedName = match[1]?.trim();
      if (!importedName) continue;
      if (!usage.has(importedName)) usage.set(importedName, new Set());
      usage.get(importedName).add(relFile);
    }
  }

  return usage;
}

/**
 * @param {string} dir
 */
function listSourceFiles(dir, results = []) {
  if (!fs.existsSync(dir)) return results;
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    if (entry.name.startsWith('.') || ['node_modules', 'dist', 'build', 'coverage'].includes(entry.name)) {
      continue;
    }
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      listSourceFiles(fullPath, results);
      continue;
    }
    if (/\.(jsx?|tsx?)$/.test(entry.name)) {
      results.push(fullPath);
    }
  }
  return results;
}

/**
 * @param {object} options
 * @param {string} options.projectRoot
 * @param {string} options.packageName
 * @param {string} options.skillName
 * @param {string} options.localVersion
 * @param {string} options.latestVersion
 * @param {unknown} options.localIndex
 * @param {unknown} [options.latestIndex]
 */
export async function analyzeUpgradeImpact({
  projectRoot,
  packageName,
  skillName,
  localVersion,
  latestVersion,
  localIndex,
  latestIndex,
}) {
  let remoteIndex = latestIndex;
  let indexFetchError = null;

  if (!remoteIndex && latestVersion && localVersion !== latestVersion) {
    try {
      remoteIndex = await fetchPublishedComponentsIndex(packageName, latestVersion, skillName);
    } catch (error) {
      indexFetchError = error?.message || String(error);
    }
  }

  const packageChanges =
    localIndex && remoteIndex
      ? diffComponentIndexes(localIndex, remoteIndex)
      : {
          generatedAtFrom: localIndex?.generatedAt || null,
          generatedAtTo: null,
          summaryFrom: localIndex?.summary || null,
          summaryTo: null,
          changes: [],
          updated: [],
          added: [],
          removed: [],
          indexFetchError,
        };

  if (indexFetchError) {
    packageChanges.indexFetchError = indexFetchError;
  }

  const usage = scanUsedTfdsImports(projectRoot, packageName);
  const usedSymbols = [...usage.keys()].sort((a, b) => a.localeCompare(b));

  const changeByName = new Map(packageChanges.changes.map((c) => [c.name, c]));

  /** @type {Array<{ name: string, changeType: string, changedSections: string[], usedInFiles: string[], description?: string }>} */
  const affectedComponents = [];

  for (const symbol of usedSymbols) {
    const change = changeByName.get(symbol);
    if (!change) continue;
    affectedComponents.push({
      name: symbol,
      changeType: change.changeType,
      changedSections: change.changedSections,
      description: change.description,
      usedInFiles: [...(usage.get(symbol) || [])].sort(),
    });
  }

  const affectedFiles = [
    ...new Set(affectedComponents.flatMap((c) => c.usedInFiles)),
  ].sort();

  const hasExistingUsage = usedSymbols.length > 0;
  const hasProjectImpact = affectedComponents.length > 0;

  let summary;
  if (!hasExistingUsage) {
    summary =
      '项目源码中暂未检测到从本包 import 的已有组件。升级 npm 后通常只更新依赖、主题与 Skill，不会自动改动业务页面。';
  } else if (!hasProjectImpact) {
    summary = `项目已使用 ${usedSymbols.join('、')}，但新版组件索引中这些符号的规则/Props/示例未发现变化。升级后已有页面视觉与交互预期应保持稳定；npm 升级不会自动修改页面源码。`;
  } else {
    summary =
      `项目中有 ${affectedFiles.length} 个文件使用了会在新版本中变化的 TFDS 组件/模板。` +
      '升级 npm 包**不会自动重写**这些页面，但若你之后按新版规范重新生成或手动迁移，下列组件的样式/规则/Props 可能不一致，需你确认后再改。';
  }

  return {
    hasExistingUsage,
    usedSymbols,
    affectedComponents,
    affectedFiles,
    hasProjectImpact,
    willAutoModifyPages: false,
    summary,
    packageChanges,
  };
}
