const { mod } = require('./blackbox');

const ALLOWED_IDENTIFIERS = new Set([
  'x',
  'y',
  'N',
  'if',
  'iff',
  'abs',
  'floor',
  'ceil',
  'round',
  'sqrt',
  'min',
  'max',
  'sin',
  'cos',
  'tan',
  'log',
  'PI',
  'E',
  'table',
]);
const MAX_EXPRESSION_LENGTH = 100000;

function toJsExpression(expr) {
  if (typeof expr !== 'string' || expr.length < 1 || expr.length > MAX_EXPRESSION_LENGTH) {
    throw new Error(`Expression length must be between 1 and ${MAX_EXPRESSION_LENGTH} characters`);
  }
  let text = expr.trim();
  text = text.replace(/\bmod\b/gi, '%');
  text = text.replace(/\^/g, '**');
  text = text.replace(/\bif\s*\(/g, 'iff(');

  if (!/^[0-9a-zA-Z_+\-*/%().,<>=!&| \t]*$/.test(text)) {
    throw new Error('Expression contains disallowed characters');
  }

  const identifiers = text.match(/[A-Za-z_][A-Za-z0-9_]*/g) || [];
  for (const id of identifiers) {
    if (!ALLOWED_IDENTIFIERS.has(id)) {
      throw new Error(`Disallowed identifier: ${id}`);
    }
  }

  return text
    .replace(/\babs\b/g, 'Math.abs')
    .replace(/\bfloor\b/g, 'Math.floor')
    .replace(/\bceil\b/g, 'Math.ceil')
    .replace(/\bround\b/g, 'Math.round')
    .replace(/\bsqrt\b/g, 'Math.sqrt')
    .replace(/\bmin\b/g, 'Math.min')
    .replace(/\bmax\b/g, 'Math.max')
    .replace(/\bsin\b/g, 'Math.sin')
    .replace(/\bcos\b/g, 'Math.cos')
    .replace(/\btan\b/g, 'Math.tan')
    .replace(/\blog\b/g, 'Math.log')
    .replace(/\bPI\b/g, 'Math.PI')
    .replace(/\bE\b/g, 'Math.E');
}

function compileExpression(expr) {
  const js = toJsExpression(expr);
  // eslint-disable-next-line no-new-func
  const fn = new Function(
    'x',
    'y',
    'N',
    'iff',
    'table',
    `"use strict"; const value = (${js}); if (!Number.isFinite(Number(value))) throw new Error("Expression result is not a finite number"); return Number(value);`,
  );
  return (x, y, N) => {
    const raw = fn(
      x,
      y,
      N,
      (condition, a, b) => (condition ? a : b),
      (index, ...values) => values[((Math.trunc(index) % values.length) + values.length) % values.length],
    );
    return mod(Math.round(raw), N);
  };
}

function compileAnswer(answer) {
  if (!answer || typeof answer.u !== 'string' || typeof answer.v !== 'string') {
    throw new Error('Answer must include string expressions for u and v');
  }
  return {
    u: compileExpression(answer.u),
    v: compileExpression(answer.v),
  };
}

module.exports = { compileAnswer, compileExpression };
