const fs = require('fs');
const crypto = require('crypto');
const { makeParams, evaluateStep, answerExpressions } = require('../src/lib/blackbox');

const ASSET_FILE = 'MazeX_formula_assets.json';
const MODEL_FILE = 'MazeX_difficulty_model.json';
const OUT_JSON = 'MazeX_formula_difficulty_scores.json';
const OUT_MD = 'MazeX_formula_difficulty_scores.md';
const SCORE_GRID_SIZE = 40;

const CATEGORY_ABSTRACTION = {
  mathematics: 520,
  physics: 760,
  chemistry: 640,
  biology: 680,
  computer_science: 760,
  graph_networks: 700,
  board_games: 700,
  information_encoding: 680,
  language_symbols: 820,
  music_sound_signal: 620,
  image_vision: 560,
  geography_navigation_maps: 460,
  engineering_mechanics_circuits: 560,
  economics_game_systems: 720,
  topology_spatial_relations: 840,
  probability_statistics: 680,
  state_memory_dynamics: 860,
  composite_systems: 960,
  domain: 500,
};

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function countMatches(text, regex) {
  return (text.match(regex) || []).length;
}

function unique(values) {
  return Array.from(new Set(values.filter(Boolean)));
}

function normalizeCategory(value) {
  const text = String(value || '')
    .normalize('NFKD')
    .replace(/[^\x00-\x7F]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '_')
    .replace(/^_+|_+$/g, '');
  return text || 'domain';
}

function difficultyBand(score) {
  return `D${clamp(Math.ceil(score / 1000), 1, 10)}`;
}

function scoringStep(asset) {
  return {
    ...asset,
    layer_no: Math.max(1, Number(asset.unlock_layer || 1)),
    step_no: 1,
    step_count: 30,
    target_difficulty: Number(asset.difficulty_max || 1),
    parameter_seed: `formula-score:${asset.asset_id}`,
    formula_seed: `formula-score-formula:${asset.asset_id}`,
    verification_seed: `formula-score-verify:${asset.asset_id}`,
    structure_bundle: [asset.primary_structure, ...(asset.secondary_structure || [])],
    composition_count: 1,
  };
}

function behaviorHash(step, params, gridSize) {
  const hash = crypto.createHash('sha256');
  for (let y = 0; y < gridSize; y += 1) {
    for (let x = 0; x < gridSize; x += 1) {
      const output = evaluateStep(step, params, x, y, gridSize);
      hash.update(`${output.u},${output.v};`);
    }
  }
  return hash.digest('hex');
}

function instantiateAsset(asset) {
  const step = scoringStep(asset);
  const params = makeParams(step, SCORE_GRID_SIZE);
  const formula = answerExpressions(step, params);
  const hash = behaviorHash(step, params, SCORE_GRID_SIZE);
  return { asset, step, params, formula, behavior_hash: hash };
}

function analyzeFormulaInstance(instance) {
  const text = `${instance.formula.u || ''} ${instance.formula.v || ''}`;
  const parameterNames = unique(Object.keys(instance.params || {}));
  const parameterValues = Object.values(instance.params || {});
  const numericValues = parameterValues
    .flatMap((value) => (Array.isArray(value) ? value : [value]))
    .filter((value) => Number.isFinite(Number(value)));

  return {
    text_length: text.length,
    parameter_count: parameterNames.length,
    unique_parameter_value_count: unique(numericValues.map(String)).length,
    if_count: countMatches(text, /\bif\s*\(/g),
    mod_count: countMatches(text, /\bmod\b/g),
    floor_count: countMatches(text, /\bfloor\s*\(/g),
    abs_count: countMatches(text, /\babs\s*\(/g),
    function_count: countMatches(text, /\b(if|floor|ceil|round|sqrt|log|min|max|abs|sin|cos|tan|table)\s*\(/g),
    comparison_count: countMatches(text, /(==|!=|>=|<=|>|<)/g),
    multiplication_count: countMatches(text, /\*/g),
    nonlinear_count:
      countMatches(text, /\bx\s*\*\s*x\b/g) +
      countMatches(text, /\by\s*\*\s*y\b/g) +
      countMatches(text, /\bx\s*\*\s*y\b/g) +
      countMatches(text, /\by\s*\*\s*x\b/g),
    table_count: countMatches(text, /\btable\s*\(/g),
    operator_count: countMatches(text, /[+\-*/]/g),
  };
}

function scoreInstanceGroup(group, model) {
  const representative = group.instances
    .slice()
    .sort(
      (a, b) =>
        Number(b.asset.difficulty_max || 0) - Number(a.asset.difficulty_max || 0) ||
        String(a.asset.asset_id).localeCompare(String(b.asset.asset_id)),
    )[0];
  const asset = representative.asset;
  const analysis = analyzeFormulaInstance(representative);
  const structureBase = Number(model.structure_base_score?.[asset.primary_structure] || 0);
  const maxStructureBase = Math.max(...Object.values(model.structure_base_score || {}).map(Number), 1);
  const categories = unique(group.instances.map((item) => normalizeCategory(item.asset.source_category)));
  const categoryScore = Math.max(...categories.map((category) => CATEGORY_ABSTRACTION[category] || 500), 500);
  const highStructureBonus = /^S(3[0-7]|2[4-9])_/.test(String(asset.primary_structure || '')) ? 1 : 0;

  const structure_score = Math.round((structureBase / maxStructureBase) * 3000);
  const expression_score = Math.round(
    clamp(
      analysis.text_length * 1.05 +
        analysis.operator_count * 24 +
        analysis.function_count * 70 +
        analysis.nonlinear_count * 130 +
        analysis.multiplication_count * 16 +
        analysis.table_count * 160,
      0,
      1800,
    ),
  );
  const branch_discrete_score = Math.round(
    clamp(
      analysis.if_count * 350 +
        analysis.comparison_count * 135 +
        analysis.mod_count * 72 +
        analysis.floor_count * 92 +
        analysis.abs_count * 56,
      0,
      1300,
    ),
  );
  const parameter_score = Math.round(
    clamp(
      analysis.parameter_count * 105 +
        analysis.unique_parameter_value_count * 28 +
        (analysis.parameter_count >= 10 ? 240 : 0) +
        (analysis.parameter_count >= 18 ? 220 : 0),
      0,
      1400,
    ),
  );
  const semantic_score = Math.round(clamp(categoryScore + highStructureBonus * 110, 0, 1000));
  const behavior_score = Math.round(
    clamp(
      Math.log2(SCORE_GRID_SIZE * SCORE_GRID_SIZE) * 38 +
        analysis.mod_count * 26 +
        analysis.if_count * 82 +
        analysis.nonlinear_count * 48 +
        highStructureBonus * 150,
      0,
      700,
    ),
  );
  const answer_pressure_score = Math.round(
    clamp(
      analysis.text_length * 0.58 +
        analysis.parameter_count * 34 +
        analysis.if_count * 120 +
        analysis.function_count * 30,
      0,
      800,
    ),
  );

  const raw =
    structure_score +
    expression_score +
    branch_discrete_score +
    parameter_score +
    semantic_score +
    behavior_score +
    answer_pressure_score;
  const score_10000 = clamp(Math.round(raw), 1, 10000);

  return {
    formula_instance_id: `MXI-${group.index.toString().padStart(5, '0')}`,
    behavior_hash: group.behavior_hash,
    score_grid_size: SCORE_GRID_SIZE,
    score_10000,
    difficulty_band: difficultyBand(score_10000),
    same_behavior_asset_count: group.instances.length,
    representative_asset_id: asset.asset_id,
    source_category: normalizeCategory(asset.source_category),
    primary_structure: asset.primary_structure,
    secondary_structure: asset.secondary_structure || [],
    parameter_seed: representative.step.parameter_seed,
    parameters: representative.params,
    formula_instance: representative.formula,
    same_behavior_asset_sample: group.instances.slice(0, 12).map((item) => item.asset.asset_id),
    covered_categories: categories.sort((a, b) => a.localeCompare(b)),
    score_components: {
      structure_score,
      expression_score,
      branch_discrete_score,
      parameter_score,
      semantic_score,
      behavior_score,
      answer_pressure_score,
    },
    formula_metrics: analysis,
  };
}

function makeMarkdown(doc) {
  const lines = [
    '# MazeX Formula Difficulty Score Table',
    '',
    '## 1. Scoring Definition',
    '',
    'This table does not modify the formula asset library. Each asset is materialized with deterministic parameters, evaluated across the full scoring grid, and grouped by complete behavior.',
    '',
    '```text',
    'same formula = identical A(x,y) -> B(u,v) output across the full grid',
    'different parameters are materialized before full-domain behavior comparison',
    'difficulty score = 1 - 10000',
    'scoring object = unique behavior instance after materialization',
    '```',
    '',
    '## 2. Summary',
    '',
    '```text',
    `source assets: ${doc.source_asset_count}`,
    `unique formula instances: ${doc.unique_formula_instance_count}`,
    `same-behavior asset count: ${doc.same_behavior_asset_count}`,
    `score grid: ${doc.score_grid_size} x ${doc.score_grid_size}`,
    `max score: ${doc.max_score}`,
    `min score: ${doc.min_score}`,
    '```',
    '',
    '## 3. Difficulty Distribution',
    '',
    '| Band | Formula Instances |',
    '|---|---:|',
    ...Object.entries(doc.band_counts).map(([band, count]) => `| ${band} | ${count} |`),
    '',
    '## 4. Formula Instance Scores',
    '',
    '| Rank | Instance ID | Score | Band | Same-Behavior Assets | Primary Structure | Representative Asset | Parameter Seed |',
    '|---:|---|---:|---|---:|---|---|---|',
    ...doc.formulas.map(
      (item, index) =>
        `| ${index + 1} | ${item.formula_instance_id} | ${item.score_10000} | ${item.difficulty_band} | ${item.same_behavior_asset_count} | ${item.primary_structure} | ${item.representative_asset_id} | ${item.parameter_seed} |`,
    ),
    '',
  ];
  return `${lines.join('\n')}\n`;
}

function main() {
  const assetDoc = JSON.parse(fs.readFileSync(ASSET_FILE, 'utf8'));
  const model = JSON.parse(fs.readFileSync(MODEL_FILE, 'utf8'));
  const groups = new Map();

  for (const asset of assetDoc.assets || []) {
    if (!asset.enabled || asset.asset_status !== 'official_refined_formula' || asset.official_formula !== true) continue;
    const instance = instantiateAsset(asset);
    if (!groups.has(instance.behavior_hash)) {
      groups.set(instance.behavior_hash, { behavior_hash: instance.behavior_hash, instances: [] });
    }
    groups.get(instance.behavior_hash).instances.push(instance);
  }

  const formulas = Array.from(groups.values())
    .map((group, index) => scoreInstanceGroup({ ...group, index: index + 1 }, model))
    .sort(
      (a, b) =>
        b.score_10000 - a.score_10000 ||
        b.same_behavior_asset_count - a.same_behavior_asset_count ||
        a.behavior_hash.localeCompare(b.behavior_hash),
    )
    .map((item, index) => ({ ...item, rank: index + 1 }));

  const bandCounts = {};
  for (const item of formulas) bandCounts[item.difficulty_band] = (bandCounts[item.difficulty_band] || 0) + 1;
  const orderedBands = Object.fromEntries(
    Array.from({ length: 10 }, (_, index) => `D${index + 1}`).map((band) => [band, bandCounts[band] || 0]),
  );

  const out = {
    version: 'mazex-formula-instance-difficulty-score-2',
    score_range: [1, 10000],
    duplicate_definition: 'same full-domain behavior after deterministic parameters are materialized',
    source_asset_count: assetDoc.assets.length,
    score_grid_size: SCORE_GRID_SIZE,
    unique_formula_instance_count: formulas.length,
    same_behavior_asset_count: assetDoc.assets.length - formulas.length,
    max_score: Math.max(...formulas.map((item) => item.score_10000)),
    min_score: Math.min(...formulas.map((item) => item.score_10000)),
    band_counts: orderedBands,
    formulas,
  };

  fs.writeFileSync(OUT_JSON, `${JSON.stringify(out, null, 2)}\n`, 'utf8');
  fs.writeFileSync(OUT_MD, makeMarkdown(out), 'utf8');
  console.log(
    JSON.stringify(
      {
        source_asset_count: out.source_asset_count,
        unique_formula_instance_count: out.unique_formula_instance_count,
        same_behavior_asset_count: out.same_behavior_asset_count,
        score_grid_size: out.score_grid_size,
        max_score: out.max_score,
        min_score: out.min_score,
        band_counts: out.band_counts,
      },
      null,
      2,
    ),
  );
}

main();
