/**
 * 清理报表 XML 中未被使用的数据集字段。
 *
 * 「被使用」判定（满足任一即保留该字段）：
 *   1. 单元格直接绑定：<dataset-content dataset="X" field="F" .../>
 *   2. 函数调用：dataset("X", "F") / datasetGroup("X", "F")
 *   3. 同数据集（计算）字段表达式中被引用：[F]
 *   4. 图表 JSON 配置引用：{"dataset":"X","field":"F"}
 *
 * 仅用字符串级删除 <field> 标签（整行删除），不做 XML 重序列化，
 * 避免破坏 CDATA / 原有格式（fast-xml-parser 往返有丢 CDATA 风险）。
 * 字段名 / 数据集名比较统一转小写，避免因大小写变体误删。
 *
 * 用法：
 *   npx tsx scripts/clean-unused-fields.ts                 # 清理默认样例目录（xml），原地覆盖
 *   npx tsx scripts/clean-unused-fields.ts --in <路径>     # 指定 XML 文件或目录
 *   npx tsx scripts/clean-unused-fields.ts --out <目录>    # 输出到新目录，不改原文件
 *   npx tsx scripts/clean-unused-fields.ts --dry-run       # 只统计，不写文件
 */
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(scriptDir, '..');
const DEFAULT_IN = path.join(projectRoot, 'src', 'examples', 'sample-center', 'xml');

/** 是否为直接执行（被 import 时不应触发 main） */
function isMainModule(): boolean {
  return Boolean(
    process.argv[1] &&
      path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)),
  );
}

// ---- 正则 ----
// 注意：不能只用 \b 词边界——`<dataset-content` 中 `-` 也是词边界，
// 会导致 `<dataset` 前缀误匹配 `<dataset-content`。统一用 (?![-\w]) 排除。
const DATASET_BLOCK_RE = /<dataset(?![-\w])[^>]*>[\s\S]*?<\/dataset>/g;
const DATASET_OPEN_RE = /<dataset(?![-\w])[^>]*>/;
const FIELD_TAG_RE = /<field(?![-\w])[^>]*\/>|<field(?![-\w])[^>]*>[\s\S]*?<\/field>/g;
const FIELD_NAME_RE = /<field(?![-\w])[^>]*\bname\s*=\s*["']([^"']+)["']/;
const COMPUTED_RE = /\bcomputed\s*=\s*["']true["']/i;
const CDATA_RE = /<!\[CDATA\[([\s\S]*?)\]\]>/;
const FIELD_REF_RE = /\[([^\[\]]+)\]/g;
const DATASET_CONTENT_RE = /<dataset-content(?![-\w])[^>]*>/g;
const DATASET_FUNC_RE = /dataset(?:Group)?\(\s*["']([^"']+)["']\s*,\s*["']([^"']+)["']\s*\)/g;
// 图表/联动 JSON 配置引用：{"dataset":"X","field":"F"}（[^{}]*? 防止跨对象层级误匹配）
const CHART_FIELD_REF_RE = /"dataset"\s*:\s*"([^"]+)"[^{}]*?"field"\s*:\s*"([^"]+)"/g;

/** 提取标签中指定属性的值（返回原始大小写） */
function attrValue(tag: string, attr: string): string | null {
  const m = tag.match(new RegExp(`\\b${attr}\\s*=\\s*["']([^"']+)["']`));
  return m?.[1] ?? null;
}

/** 全局收集「数据集名 → 被引用的字段名」集合（dataset-content / dataset() / datasetGroup()） */
function collectUsedDatasetFields(xml: string): Set<string> {
  const used = new Set<string>();
  for (const m of xml.matchAll(DATASET_CONTENT_RE)) {
    const ds = attrValue(m[0], 'dataset');
    const f = attrValue(m[0], 'field');
    if (ds && f) used.add(`${ds.toLowerCase()}\u0000${f.toLowerCase()}`);
  }
  for (const m of xml.matchAll(DATASET_FUNC_RE)) {
    const ds = m[1];
    const f = m[2];
    if (ds && f) used.add(`${ds.toLowerCase()}\u0000${f.toLowerCase()}`);
  }
  // 图表 JSON 配置（chart <value> CDATA）中的 dataset/field 引用
  for (const m of xml.matchAll(CHART_FIELD_REF_RE)) {
    const ds = m[1];
    const f = m[2];
    if (ds && f) used.add(`${ds.toLowerCase()}\u0000${f.toLowerCase()}`);
  }
  return used;
}

export interface DatasetCleanStat {
  name: string;
  total: number;
  removed: number;
  removedFields: string[];
}

export interface CleanResult {
  xml: string;
  datasets: DatasetCleanStat[];
}

function cleanDatasetBlock(
  block: string,
  used: Set<string>,
): { cleaned: string; stat: DatasetCleanStat | null } {
  const openTag = DATASET_OPEN_RE.exec(block)?.[0] ?? '';
  const dsName = attrValue(openTag, 'name');
  if (!dsName) return { cleaned: block, stat: null };

  const fields = [...block.matchAll(FIELD_TAG_RE)].map((m) => m[0]);
  const fieldNames = fields.map((tag) => attrValue(tag, 'name') ?? '');

  // 同数据集计算字段表达式中被 [字段名] 引用的集合（转小写比较，避免大小写变体误删）
  const refs = new Set<string>();
  for (const tag of fields) {
    if (!COMPUTED_RE.test(tag)) continue;
    const cdata = tag.match(CDATA_RE);
    if (!cdata) continue;
    for (const r of (cdata[1] ?? '').matchAll(FIELD_REF_RE)) {
      const ref = r[1];
      if (ref) refs.add(ref.toLowerCase());
    }
  }

  let cleaned = block;
  const removedFields: string[] = [];
  fields.forEach((tag, i) => {
    const name = fieldNames[i];
    if (!name) return;
    if (used.has(`${dsName.toLowerCase()}\u0000${name.toLowerCase()}`)) return;
    if (refs.has(name.toLowerCase())) return;

    removedFields.push(name);
    const idx = cleaned.indexOf(tag);
    if (idx < 0) return;
    // 删除 tag 所在整行（含行首缩进与换行）
    let start = idx;
    while (start > 0 && (cleaned[start - 1] === ' ' || cleaned[start - 1] === '\t')) start--;
    let end = idx + tag.length;
    while (end < cleaned.length && cleaned[end] !== '\n' && cleaned[end] !== '\r') end++;
    if (end < cleaned.length) end++;
    cleaned = cleaned.slice(0, start) + cleaned.slice(end);
  });

  if (removedFields.length === 0) return { cleaned: block, stat: null };
  return {
    cleaned,
    stat: { name: dsName, total: fields.length, removed: removedFields.length, removedFields },
  };
}

/** 清理单个报表 XML 中的未使用字段（核心逻辑，供测试与 CLI 共用） */
export function cleanUnusedFields(xml: string): CleanResult {
  const used = collectUsedDatasetFields(xml);
  const datasets: DatasetCleanStat[] = [];
  const outXml = xml.replace(DATASET_BLOCK_RE, (block) => {
    const { cleaned, stat } = cleanDatasetBlock(block, used);
    if (stat) datasets.push(stat);
    return cleaned;
  });
  return { xml: outXml, datasets };
}

// ---- CLI ----
interface CliArgs {
  in?: string;
  out?: string;
  dryRun: boolean;
}

function parseArgs(argv: string[]): CliArgs {
  const args: CliArgs = { dryRun: false };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === '--in' && argv[i + 1]) args.in = argv[++i];
    else if (a === '--out' && argv[i + 1]) args.out = argv[++i];
    else if (a === '--dry-run') args.dryRun = true;
    else if (a === '--help' || a === '-h') {
      console.log(
        [
          '用法: npx tsx scripts/clean-unused-fields.ts [--in <文件|目录>] [--out <目录>] [--dry-run]',
          '  默认 --in 为 src/examples/sample-center/xml（原地覆盖）；',
          '  指定 --out 时输出到新目录（保持子目录结构），不改原文件；',
          '  --dry-run 只统计不写文件。',
        ].join('\n'),
      );
      process.exit(0);
    }
  }
  return args;
}

async function walk(dir: string, rel: string, onFile: (full: string, rel: string) => Promise<void>) {
  const entries = await fs.readdir(dir, { withFileTypes: true });
  for (const ent of entries) {
    const full = path.join(dir, ent.name);
    const nextRel = path.join(rel, ent.name);
    if (ent.isDirectory()) await walk(full, nextRel, onFile);
    else if (ent.isFile() && ent.name.toLowerCase().endsWith('.xml')) await onFile(full, nextRel);
  }
}

async function main(): Promise<void> {
  const args = parseArgs(process.argv.slice(2));
  const inPath = path.resolve(args.in ?? DEFAULT_IN);
  const outRoot = args.out ? path.resolve(args.out) : null;

  let fileCount = 0;
  let removedTotal = 0;
  const stat = await fs.stat(inPath).catch(() => null);
  if (!stat) {
    console.error(`输入不存在: ${inPath}`);
    process.exit(1);
  }

  const handleFile = async (full: string, rel: string): Promise<void> => {
    const xml = await fs.readFile(full, 'utf-8');
    const result = cleanUnusedFields(xml);
    if (result.datasets.length === 0) {
      console.log(`  无未使用字段: ${rel}`);
      return;
    }
    fileCount++;
    for (const d of result.datasets) removedTotal += d.removed;
    console.log(`  ${rel}`);
    for (const d of result.datasets) {
      console.log(
        `    - ${d.name}: 删除 ${d.removed}/${d.total} 字段（${d.removedFields.join('、')}）`,
      );
    }
    if (args.dryRun) return;
    if (outRoot) {
      const outPath = path.join(outRoot, rel);
      await fs.mkdir(path.dirname(outPath), { recursive: true });
      await fs.writeFile(outPath, result.xml, 'utf-8');
    } else {
      await fs.writeFile(full, result.xml, 'utf-8');
    }
  };

  console.log(`输入: ${inPath}`);
  console.log(`模式: ${args.dryRun ? 'dry-run（不写文件）' : outRoot ? `输出到 ${outRoot}` : '原地覆盖'}`);

  if (stat.isDirectory()) await walk(inPath, '', handleFile);
  else await handleFile(inPath, path.basename(inPath));

  console.log('\n==== 汇总 ====');
  console.log(`  有清理的文件: ${fileCount}`);
  console.log(`  删除字段总数: ${removedTotal}`);
}

if (isMainModule()) {
  main().catch((e) => {
    console.error(e);
    process.exit(1);
  });
}
