const { Rng } = require('./rng');
const { compileExpression } = require('./dsl');
const { extractOperator, extractNumber } = require('./parameter_extractors');

const SUPPORTED_OPERATOR_IDS = new Set([
  'add',
  'subtract',
  'absolute',
  'multiply',
  'divide',
  'floor',
  'ceil',
  'round',
  'mod',
  'sqrt',
  'power',
  'sum',
  'product',
  'dot',
  'norm',
  'matrix_add',
  'transpose',
  'trace',
  'matrix_mul',
  'determinant',
  'integral',
  'derivative',
  'partial_derivative',
  'gradient',
  'rank',
  'series',
  'laplace_transform',
  'fourier_transform',
  'inverse',
  'eigenvalue',
  'laplacian_operator',
  'limit',
  'cross',
]);

function asInteger(number) {
  return Math.trunc(Number(number) || 0);
}

function divisorExpr(number) {
  return `(abs(${number}) + 1)`;
}

function modulusExpr(number) {
  return `(abs(${number}) + 2)`;
}

function coeffExpr(number, max) {
  return `((abs(${number}) mod ${max}) + 1)`;
}

function wrap(expr) {
  return `(${expr})`;
}

function pickTemplate(templates, rng) {
  return rng.pick(templates);
}

function makeTemplates(operator, number) {
  const n = asInteger(number);
  const d = divisorExpr(n);
  const m = modulusExpr(n);
  const c7 = coeffExpr(n, 7);
  const c11 = coeffExpr(n, 11);
  const c13 = coeffExpr(n, 13);
  const c17 = coeffExpr(n, 17);
  const c19 = coeffExpr(n, 19);
  const c23 = coeffExpr(n, 23);
  const c29 = coeffExpr(n, 29);
  const c31 = coeffExpr(n, 31);
  const c37 = coeffExpr(n, 37);

  return {
    add: [
      (expr) => `${wrap(expr)} + ${n}`,
      (expr) => `${n} + ${wrap(expr)}`,
    ],
    subtract: [
      (expr) => `${wrap(expr)} - ${n}`,
      (expr) => `${n} - ${wrap(expr)}`,
    ],
    absolute: [
      (expr) => `abs(${wrap(expr)} - ${n})`,
      (expr) => `abs(${wrap(expr)} + ${n})`,
    ],
    multiply: [
      (expr) => `${wrap(expr)} * (${n} + if(${n} == 0, 1, 0))`,
      (expr) => `(${n} + if(${n} == 0, 1, 0)) * ${wrap(expr)}`,
    ],
    divide: [
      (expr) => `floor(${wrap(expr)} / ${d})`,
      (expr) => `floor(${n} / (abs(${wrap(expr)}) + 1))`,
    ],
    floor: [
      (expr) => `floor((${wrap(expr)} + ${n}) / ${d})`,
      (expr) => `floor(${wrap(expr)} / ${d}) + ${n}`,
    ],
    ceil: [
      (expr) => `ceil((${wrap(expr)} + ${n}) / ${d})`,
      (expr) => `ceil(${wrap(expr)} / ${d}) + ${n}`,
    ],
    round: [
      (expr) => `round((${wrap(expr)} + ${n}) / ${d})`,
      (expr) => `round(${wrap(expr)} / ${d}) + ${n}`,
    ],
    mod: [
      (expr) => `${wrap(expr)} mod ${m}`,
      (expr) => `(${wrap(expr)} + ${n}) mod ${m}`,
    ],
    sqrt: [
      (expr) => `sqrt(abs(${wrap(expr)}) + abs(${n}))`,
      (expr) => `sqrt(abs(${wrap(expr)} - ${n}) + 1)`,
    ],
    power: [
      (expr) => `((abs(${wrap(expr)}) + (${n} mod 11)) mod N) ^ 2`,
      (expr) => `((abs(${wrap(expr)}) + (${n} mod 7)) mod N) ^ 2 + ${n}`,
    ],
    sum: [
      (expr) => `${wrap(expr)} + (${wrap(expr)} + ${n}) + (${wrap(expr)} + ${n * 2})`,
      (expr) => `${wrap(expr)} + floor((${wrap(expr)} + ${n}) / ${d})`,
    ],
    product: [
      (expr) => `${wrap(expr)} * ((${n} mod ${m}) + 1)`,
      (expr) => `(${wrap(expr)} + 1) * ((${n} mod ${m}) + 1)`,
    ],
    dot: [
      (expr) => `${wrap(expr)} * ${c19} + x * ${c23} + y * ${c29}`,
      (expr) => `x * ${c19} + y * ${c29} + ${wrap(expr)} + ${n}`,
    ],
    norm: [
      (expr) => `abs(${wrap(expr)}) + abs(x - ${n}) + abs(y - ${n})`,
      (expr) => `sqrt(abs(${wrap(expr)} * ${wrap(expr)}) + abs(x * x) + abs(y * y) + abs(${n}))`,
    ],
    matrix_add: [
      (expr) => `${wrap(expr)} + ${c31}*x + ${c37}*y`,
      (expr) => `${wrap(expr)} + (${c31} + ${c37}) + ${n}`,
    ],
    transpose: [
      (expr) => `${wrap(expr)} + ${c31}*y + ${c37}*x`,
      (expr) => `${wrap(expr)} + (y - x) * ${c13} + ${n}`,
    ],
    trace: [
      (expr) => `${wrap(expr)} + ${c31}*x + ${c31}*y`,
      (expr) => `${wrap(expr)} + (${c31} * (x + y)) + ${n}`,
    ],
    matrix_mul: [
      (expr) => `${wrap(expr)} + ${c11}*x + ${c13}*y + ${c17}*x*y`,
      (expr) => `(${c11}*x + ${c13}*y + ${wrap(expr)} + ${n})`,
    ],
    determinant: [
      (expr) => `${wrap(expr)} + (${c13}*x*y - ${c17}*y*x) + ${n}`,
      (expr) => `${wrap(expr)} + (${c13}*x*x - ${c17}*y*y)`,
    ],
    integral: [
      (expr) => `${wrap(expr)} + floor((x + y + ${n}) / ${d})`,
      (expr) => `${wrap(expr)} + floor((x*x + y*y + ${n}) / ${d})`,
    ],
    derivative: [
      (expr) => `${wrap(expr)} + ${c17} * if((x + y + ${n}) mod 2 == 0, x, y)`,
      (expr) => `${wrap(expr)} + floor((${wrap(expr)} + x + ${n}) / ${d})`,
    ],
    partial_derivative: [
      (expr) => `${wrap(expr)} + ${c19} * if((x - y + ${n}) mod 2 == 0, y, x)`,
      (expr) => `${wrap(expr)} + floor((${wrap(expr)} + y + ${n}) / ${d})`,
    ],
    gradient: [
      (expr) => `${wrap(expr)} + abs(${c17}*x - ${c19}*y) + ${n}`,
      (expr) => `${wrap(expr)} + abs((x + 1) - x) + abs((y + 1) - y) + ${n}`,
    ],
    rank: [
      (expr) => `${wrap(expr)} + if(((${c13}*x + ${c17}*y + ${n}) mod ${m}) == 0, 1, 2)`,
      (expr) => `${wrap(expr)} + if(x == y, 1, 2) + ${c11} + ${n}`,
    ],
    series: [
      (expr) => `${wrap(expr)} + (x mod ${m}) + ((x + y + ${n}) mod ${m}) + ((x + 2*y + ${n}) mod ${m})`,
      (expr) => `${wrap(expr)} + floor((x + y + ${n}) / ${d}) + floor((x + y + ${n * 2}) / ${d})`,
    ],
    laplace_transform: [
      (expr) => `${wrap(expr)} + floor((${wrap(expr)} + ${n}) / ${d}) + ((x + y + ${n}) mod ${m})`,
      (expr) => `${wrap(expr)} + floor((x + ${n}) / ${d}) + floor((y + ${n}) / ${d})`,
    ],
    fourier_transform: [
      (expr) => `${wrap(expr)} + table(x + y + ${n}, 0, 1, 0, -1, 0, 1, 0, -1)`,
      (expr) => `${wrap(expr)} + table(x - y + ${n}, 1, 0, -1, 0, 1, 0, -1, 0)`,
    ],
    inverse: [
      (expr) => `${wrap(expr)} + floor(${m} / (abs(${wrap(expr)}) + 1))`,
      (expr) => `${wrap(expr)} + floor(${m} / (abs(x - y) + 1))`,
    ],
    eigenvalue: [
      (expr) => `${wrap(expr)} + floor(((${c17}*x + ${c19}*y) + ${n}) / ${d})`,
      (expr) => `${wrap(expr)} + (${c17}*x + ${c17}*y) + ${n}`,
    ],
    laplacian_operator: [
      (expr) => `${wrap(expr)} + abs((x + 1) + (x - 1) + (y + 1) + (y - 1) - 2*x - 2*y + ${n})`,
      (expr) => `${wrap(expr)} + abs(2*x + 2*y - 4*floor((x + y + ${n}) / 2))`,
    ],
    limit: [
      (expr) => `${wrap(expr)} + floor((${wrap(expr)} + ${n}) / (${d} + 1))`,
      (expr) => `${wrap(expr)} + floor((${n} + x + y) / (${d} + 1))`,
    ],
    cross: [
      (expr) => `${wrap(expr)} + (${c17}*x - ${c19}*y) + ${n}`,
      (expr) => `${wrap(expr)} + (${c17}*x*y - ${c19}*y*x)`,
    ],
  }[operator.id];
}

function trimRange(text, start, end) {
  let left = start;
  let right = end;
  while (left < right && /\s/.test(text[left])) left += 1;
  while (right > left && /\s/.test(text[right - 1])) right -= 1;
  return { start: left, end: right };
}

function isUnaryMinus(text, index) {
  if (text[index] !== '-') return false;
  const before = text.slice(0, index).trim();
  if (!before) return true;
  return /[+\-*/^(,]$/.test(before);
}

function isWordBoundary(text, index) {
  return index < 0 || index >= text.length || !/[A-Za-z0-9_]/.test(text[index]);
}

function collectInsertionTargets(expression) {
  const text = expression.trim();
  const targets = [{ start: 0, end: text.length, kind: 'whole', text }];
  const seen = new Set(['0:' + text.length]);

  function addTarget(start, end, kind) {
    const trimmed = trimRange(text, start, end);
    if (trimmed.end <= trimmed.start) return;
    const key = `${trimmed.start}:${trimmed.end}`;
    if (seen.has(key)) return;
    seen.add(key);
    targets.push({
      start: trimmed.start,
      end: trimmed.end,
      kind,
      text: text.slice(trimmed.start, trimmed.end),
    });
  }

  let depth = 0;
  for (let index = 0; index < text.length; index += 1) {
    const char = text[index];
    if (char === '(') {
      depth += 1;
      continue;
    }
    if (char === ')') {
      depth = Math.max(0, depth - 1);
      continue;
    }
    if (depth !== 0) continue;

    const maybeMod = text.slice(index, index + 3).toLowerCase();
    if (maybeMod === 'mod' && isWordBoundary(text, index - 1) && isWordBoundary(text, index + 3)) {
      addTarget(0, index, 'left');
      addTarget(index + 3, text.length, 'right');
      index += 2;
      continue;
    }

    if ('+-*/^'.includes(char) && !isUnaryMinus(text, index)) {
      addTarget(0, index, 'left');
      addTarget(index + 1, text.length, 'right');
    }
  }

  return targets;
}

function injectOnce({ expression, operator, number, seed = 'parameter-injector' }) {
  if (!SUPPORTED_OPERATOR_IDS.has(operator.id)) throw new Error(`Unsupported operator for injection: ${operator.id}`);
  const templates = makeTemplates(operator, number.value);
  if (!templates || !templates.length) throw new Error(`No injection templates for operator: ${operator.id}`);
  const sourceExpression = expression.trim();
  const rng = new Rng(seed);
  const template = pickTemplate(templates, rng);
  const target = rng.pick(collectInsertionTargets(sourceExpression));
  const inserted = wrap(template(target.text));
  const nextExpression = `${sourceExpression.slice(0, target.start)}${inserted}${sourceExpression.slice(target.end)}`;
  return {
    expression: nextExpression,
    operator,
    number,
    insertion_target: {
      kind: target.kind,
      text: target.text,
      start: target.start,
      end: target.end,
    },
    injection_seed: seed,
  };
}

function isNonConstantExpression(expression, n = 40) {
  const fn = compileExpression(expression);
  const values = new Set();
  for (const [x, y] of [
    [0, 0],
    [1, 2],
    [7, 11],
    [Math.floor(n / 2), Math.floor(n / 2)],
    [n - 1, n - 1],
  ]) {
    values.add(fn(x, y, n));
  }
  return values.size > 1;
}

function injectParameter({
  expression,
  seed = 'parameter-injector',
  operator = null,
  number = null,
  minOperatorScore = 1,
  maxOperatorScore = 1000,
  numberMin = 1,
  numberMax = 1000,
  maxAttempts = 20,
  gridSize = 40,
} = {}) {
  if (typeof expression !== 'string' || !expression.trim()) throw new Error('expression is required');
  compileExpression(expression);

  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const localSeed = `${seed}:attempt:${attempt}`;
    const pickedOperator =
      operator ||
      extractOperator({
        seed: `${localSeed}:operator`,
        minScore: minOperatorScore,
        maxScore: maxOperatorScore,
      });
    const pickedNumber =
      number ||
      extractNumber({
        seed: `${localSeed}:number`,
        min: numberMin,
        max: numberMax,
      });
    const injected = injectOnce({
      expression,
      operator: pickedOperator,
      number: pickedNumber,
      seed: `${localSeed}:template`,
    });

    try {
      compileExpression(injected.expression);
      if (!isNonConstantExpression(injected.expression, gridSize)) continue;
      return {
        ...injected,
        attempts: attempt + 1,
        total_difficulty_score: pickedOperator.difficulty_score + pickedNumber.difficulty_score,
      };
    } catch (_error) {
      // Try another deterministic template/operator/number combination.
    }
  }

  throw new Error('Failed to inject a safe non-constant parameter expression.');
}

function injectParameters({
  expression,
  seed = 'parameter-injector',
  count = 1,
  minOperatorScore = 1,
  maxOperatorScore = 1000,
  numberMin = 1,
  numberMax = 1000,
  gridSize = 40,
} = {}) {
  let current = expression;
  const steps = [];
  for (let index = 0; index < count; index += 1) {
    const result = injectParameter({
      expression: current,
      seed: `${seed}:step:${index}`,
      minOperatorScore,
      maxOperatorScore,
      numberMin,
      numberMax,
      gridSize,
    });
    current = result.expression;
    steps.push(result);
  }
  return {
    expression: current,
    steps,
    total_difficulty_score: steps.reduce((sum, step) => sum + step.total_difficulty_score, 0),
  };
}

function injectParametersWithPlan({
  expression,
  seed = 'parameter-injector',
  parameterPlan,
  gridSize = 40,
} = {}) {
  if (!parameterPlan) throw new Error('parameterPlan is required');
  return injectParameters({
    expression,
    seed,
    count: parameterPlan.parameter_count,
    minOperatorScore: parameterPlan.operator_score_min,
    maxOperatorScore: parameterPlan.operator_score_max,
    numberMin: parameterPlan.number_score_min,
    numberMax: parameterPlan.number_score_max,
    gridSize,
  });
}

module.exports = {
  SUPPORTED_OPERATOR_IDS,
  injectParameter,
  injectParameters,
  injectParametersWithPlan,
};
