const { Rng } = require('./rng');
const { compileExpression } = require('./dsl');
const {
  extractDynamicParameter,
  selectDynamicParametersForStep,
} = require('./dynamic_parameter_extractors');
const {
  dynamicVariableMap,
  dynamicVariableName,
  materializeDynamicExpression,
} = require('./dynamic_context');

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

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 dynamicTemplates(param) {
  if (param.family === 'CHAIN_STATE') {
    const seed = dynamicVariableName(param, 'seed');
    const modulus = dynamicVariableName(param, 'modulus');
    return [
      (expr) => `${wrap(expr)} + ${seed}`,
      (expr) => `(${wrap(expr)} + ${seed}) mod (abs(${modulus}) + 2)`,
    ];
  }
  if (param.family === 'PERIODIC_CYCLE') {
    const phase = dynamicVariableName(param, 'phase');
    const value = dynamicVariableName(param, 'value');
    const amplitude = dynamicVariableName(param, 'amplitude');
    return [
      (expr) => `${wrap(expr)} + ${value}`,
      (expr) => `${wrap(expr)} + ${amplitude} * ${phase}`,
    ];
  }
  if (param.family === 'TIME_CLOCK') {
    const bucket = dynamicVariableName(param, 'bucket');
    const window = dynamicVariableName(param, 'window');
    const scaled = dynamicVariableName(param, 'scaled');
    return [
      (expr) => `${wrap(expr)} + ${bucket}`,
      (expr) => `${wrap(expr)} + if(${window} == 0, ${scaled}, ${bucket})`,
    ];
  }
  if (param.family === 'HALF_LIFE_DECAY') {
    const decay = dynamicVariableName(param, 'decay');
    const bucket = dynamicVariableName(param, 'bucket');
    const initial = dynamicVariableName(param, 'initial');
    return [
      (expr) => `${wrap(expr)} + ${decay}`,
      (expr) => `${wrap(expr)} + floor(${initial} / (2 ^ min(${bucket}, 12)))`,
    ];
  }
  return [(expr) => expr];
}

function compileMaterialized(dynamicExpression, dynamicParameters, context, gridSize) {
  const map = dynamicVariableMap(dynamicParameters, context, gridSize);
  const materialized = materializeDynamicExpression(dynamicExpression, map);
  compileExpression(materialized);
  return { materialized_expression: materialized, dynamic_variable_map: map };
}

function expressionChangesAcrossContexts(dynamicExpression, dynamicParameters, gridSize) {
  const outputs = new Set();
  for (const elapsedSeconds of [0, 45, 120, 600]) {
    const { materialized_expression: materialized } = compileMaterialized(
      dynamicExpression,
      dynamicParameters,
      { elapsedSeconds },
      gridSize,
    );
    const fn = compileExpression(materialized);
    outputs.add(fn(7, 11, gridSize));
  }
  return outputs.size > 1;
}

function injectDynamicParameter({
  expression,
  seed = 'dynamic-injector',
  dynamicParameter = null,
  existingDynamicParameters = [],
  layerNo = 301,
  context = {},
  gridSize = 40,
  maxAttempts = 20,
} = {}) {
  if (typeof expression !== 'string' || !expression.trim()) throw new Error('expression is required');
  try {
    compileExpression(expression);
  } catch (_error) {
    compileMaterialized(expression, existingDynamicParameters, context, gridSize);
  }

  const param =
    dynamicParameter ||
    extractDynamicParameter({
      seed: `${seed}:dynamic-param`,
      layerNo,
    });
  const templates = dynamicTemplates(param);
  const sourceExpression = expression.trim();

  let lastError = null;
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const rng = new Rng(`${seed}:attempt:${attempt}`);
    const target = rng.pick(collectInsertionTargets(sourceExpression));
    const template = rng.pick(templates);
    const inserted = wrap(template(target.text));
    const dynamicExpression = `${sourceExpression.slice(0, target.start)}${inserted}${sourceExpression.slice(target.end)}`;
    const dynamicParameters = [...existingDynamicParameters, param];

    try {
      const compiled = compileMaterialized(dynamicExpression, dynamicParameters, context, gridSize);
      return {
        expression: dynamicExpression,
        ...compiled,
        dynamic_parameter: param,
        dynamic_parameters: dynamicParameters,
        insertion_target: {
          kind: target.kind,
          text: target.text,
          start: target.start,
          end: target.end,
        },
        dynamic_changes_across_contexts: expressionChangesAcrossContexts(dynamicExpression, dynamicParameters, gridSize),
        attempts: attempt + 1,
        total_difficulty_score: param.difficulty_score,
      };
    } catch (error) {
      lastError = error;
      // Try another target/template combination.
    }
  }

  throw new Error(`Failed to inject a safe dynamic parameter expression: ${lastError?.message || 'unknown error'}`);
}

function injectDynamicParameters({
  expression,
  seed = 'dynamic-injector',
  layerNo = 301,
  stepNo = 1,
  stepCount = 1,
  band = 'main',
  context = {},
  gridSize = 40,
  dynamicParameters = null,
} = {}) {
  let current = expression;
  const selected =
    dynamicParameters ||
    selectDynamicParametersForStep({
      layerNo,
      stepNo,
      stepCount,
      band,
      seed,
    }).dynamic_parameters;
  const steps = [];
  const activeDynamicParameters = [];

  for (let index = 0; index < selected.length; index += 1) {
    const result = injectDynamicParameter({
      expression: current,
      seed: `${seed}:step:${index}`,
      dynamicParameter: selected[index],
      existingDynamicParameters: activeDynamicParameters,
      layerNo,
      context,
      gridSize,
    });
    current = result.expression;
    activeDynamicParameters.push(selected[index]);
    steps.push(result);
  }

  const map = dynamicVariableMap(selected, context, gridSize);
  return {
    expression: current,
    materialized_expression: materializeDynamicExpression(current, map),
    dynamic_parameters: selected,
    dynamic_variable_map: map,
    steps,
    total_difficulty_score: selected.reduce((sum, param) => sum + param.difficulty_score, 0),
  };
}

module.exports = {
  injectDynamicParameter,
  injectDynamicParameters,
};
