const { sha256 } = require('./hash');

function mod(value, n) {
  return ((Math.trunc(value) % n) + n) % n;
}

function hashInt(input) {
  return Number(BigInt(`0x${sha256(input).replace(/^0x/, '').slice(0, 14)}`) % BigInt(Number.MAX_SAFE_INTEGER));
}

function buildDynamicContext({
  layerNo = 1,
  stepNo = 1,
  elapsedSeconds = 0,
  sampleIndex = 0,
  verificationIndex = 0,
  previousBlockHash = 'genesis',
  previousAnswerHash = 'none',
  solverAddress = 'anonymous',
} = {}) {
  const elapsed = Math.max(0, Math.trunc(Number(elapsedSeconds || 0)));
  return {
    layer_no: Math.max(1, Math.trunc(Number(layerNo || 1))),
    step_no: Math.max(1, Math.trunc(Number(stepNo || 1))),
    elapsed_seconds: elapsed,
    sample_index: Math.max(0, Math.trunc(Number(sampleIndex || 0))),
    verification_index: Math.max(0, Math.trunc(Number(verificationIndex || 0))),
    previous_block_hash: String(previousBlockHash || 'genesis'),
    previous_answer_hash: String(previousAnswerHash || 'none'),
    solver_address: String(solverAddress || 'anonymous'),
  };
}

function dynamicVariableName(param, suffix) {
  return `dyn_${param.family.toLowerCase()}_${suffix}`;
}

function resolveDynamicParameter(param, contextInput = {}, N = 40) {
  const context = buildDynamicContext(contextInput);
  const config = param.config || {};
  const tickSize = Math.max(1, Math.trunc(Number(config.tick_size_seconds || config.half_life_seconds || 60)));
  const tick = Math.floor(context.elapsed_seconds / tickSize);

  if (param.family === 'CHAIN_STATE') {
    const modulus = Math.max(2, Math.trunc(Number(config.modulus || 31)));
    const chainInput = {
      layer_no: context.layer_no,
      step_no: context.step_no,
      previous_block_hash: context.previous_block_hash,
      previous_answer_hash: context.previous_answer_hash,
      salt: config.salt || '',
    };
    const value = mod(hashInt(chainInput), modulus);
    return {
      family: param.family,
      variables: {
        [dynamicVariableName(param, 'seed')]: value,
        [dynamicVariableName(param, 'modulus')]: modulus,
      },
      context_values: { chain_seed: value, modulus },
    };
  }

  if (param.family === 'PERIODIC_CYCLE') {
    const period = Math.max(2, Math.trunc(Number(config.period || 8)));
    const phase = mod(tick + Number(config.phase_shift || 0), period);
    const table = Array.isArray(config.table) && config.table.length ? config.table : [0, 1, 0, -1];
    const value = Number(table[mod(phase, table.length)] || 0);
    const amplitude = Math.max(1, Math.trunc(Number(config.amplitude || 1)));
    return {
      family: param.family,
      variables: {
        [dynamicVariableName(param, 'phase')]: phase,
        [dynamicVariableName(param, 'value')]: value,
        [dynamicVariableName(param, 'amplitude')]: amplitude,
      },
      context_values: { tick, phase, period, value, amplitude },
    };
  }

  if (param.family === 'TIME_CLOCK') {
    const scale = Math.max(1, Math.trunc(Number(config.scale || 1)));
    const windowSize = Math.max(1, Math.trunc(Number(config.window_size || 4)));
    const bucket = tick;
    const window = mod(bucket, windowSize);
    const scaled = mod(bucket * scale + context.step_no + context.layer_no, Math.max(2, N));
    return {
      family: param.family,
      variables: {
        [dynamicVariableName(param, 'bucket')]: bucket,
        [dynamicVariableName(param, 'window')]: window,
        [dynamicVariableName(param, 'scaled')]: scaled,
      },
      context_values: { tick, bucket, window, scaled, scale, tick_size_seconds: tickSize },
    };
  }

  if (param.family === 'HALF_LIFE_DECAY') {
    const initial = Math.max(1, Math.trunc(Number(config.initial_value || 128)));
    const floorValue = Math.max(0, Math.trunc(Number(config.floor_value || 0)));
    const decayBucket = Math.min(30, tick);
    const value = Math.max(floorValue, Math.floor(initial / 2 ** decayBucket));
    return {
      family: param.family,
      variables: {
        [dynamicVariableName(param, 'decay')]: value,
        [dynamicVariableName(param, 'bucket')]: decayBucket,
        [dynamicVariableName(param, 'initial')]: initial,
      },
      context_values: {
        decay_bucket: decayBucket,
        decay_value: value,
        initial_value: initial,
        half_life_seconds: tickSize,
      },
    };
  }

  return { family: param.family, variables: {}, context_values: {} };
}

function resolveDynamicParameters(params = [], contextInput = {}, N = 40) {
  return params.map((param) => ({
    parameter: param,
    ...resolveDynamicParameter(param, contextInput, N),
  }));
}

function dynamicVariableMap(params = [], contextInput = {}, N = 40) {
  return Object.assign(
    {},
    ...resolveDynamicParameters(params, contextInput, N).map((resolved) => resolved.variables),
  );
}

function materializeDynamicExpression(expression, variableMap = {}) {
  let text = String(expression || '');
  for (const [name, value] of Object.entries(variableMap).sort((a, b) => b[0].length - a[0].length)) {
    text = text.replace(new RegExp(`\\b${name}\\b`, 'g'), String(Math.trunc(Number(value) || 0)));
  }
  return text;
}

module.exports = {
  buildDynamicContext,
  dynamicVariableName,
  dynamicVariableMap,
  materializeDynamicExpression,
  resolveDynamicParameter,
  resolveDynamicParameters,
};
