import { create, all, type EvalFunction } from 'mathjs';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const math = create(all, { number: 'number', precision: 14 } as any);

function buildScope(variables: Record<string, string>): Record<string, unknown> {
  const scope: Record<string, unknown> = {};
  for (const [name, expr] of Object.entries(variables)) {
    if (!expr.trim()) continue;
    try {
      scope[name] = math.evaluate(expr);
    } catch {
      // skip invalid variables
    }
  }
  return scope;
}

export interface EvalResult {
  result: string;
  error: boolean;
  assignedName?: string;
  assignedValue?: string;
}

export function evaluateExpression(
  expression: string,
  variables: Record<string, string> = {}
): EvalResult {
  const trimmed = expression.trim();
  if (!trimmed) return { result: '', error: false };

  try {
    const scope = buildScope(variables);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const raw = math.evaluate(trimmed, scope as any);
    const formatted = formatValue(raw);

    // Detect assignment: "name = expr" or "name(args) = expr"
    const assignMatch = trimmed.match(/^([a-zA-Z_]\w*)\s*(?:\([^)]*\))?\s*=/);
    if (assignMatch) {
      return {
        result: formatted,
        error: false,
        assignedName: assignMatch[1],
        assignedValue: formatted,
      };
    }

    return { result: formatted, error: false };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    return { result: msg.replace(/^Error: /, ''), error: true };
  }
}

function formatValue(value: unknown): string {
  if (value === null || value === undefined) return 'null';
  if (typeof value === 'boolean') return String(value);
  if (typeof value === 'number') {
    if (!isFinite(value)) return value > 0 ? 'Infinity' : '-Infinity';
    if (value === 0) return '0';
    const abs = Math.abs(value);
    if (abs >= 1e13 || (abs < 1e-7 && abs > 0)) {
      return value.toExponential(6).replace(/\.?0+(e)/, '$1');
    }
    return parseFloat(value.toPrecision(12)).toString();
  }
  try {
    return math.format(value as never, { precision: 10 });
  } catch {
    return String(value);
  }
}

// Compilation cache for fast repeated graph evaluation
const compileCache = new Map<string, EvalFunction>();

function getCompiled(expr: string): EvalFunction | null {
  if (!compileCache.has(expr)) {
    try {
      compileCache.set(expr, math.compile(expr) as EvalFunction);
    } catch {
      return null;
    }
    if (compileCache.size > 200) {
      compileCache.delete(compileCache.keys().next().value!);
    }
  }
  return compileCache.get(expr) ?? null;
}

export function evaluateAt(
  expression: string,
  x: number,
  variables: Record<string, string>
): number {
  const compiled = getCompiled(expression);
  if (!compiled) return NaN;
  try {
    const scope = buildScope(variables);
    const result = compiled.evaluate({ ...scope, x });
    return typeof result === 'number' && isFinite(result) ? result : NaN;
  } catch {
    return NaN;
  }
}

export interface GraphPoint {
  x: number;
  y: number;
}

export function sampleFunction(
  expression: string,
  xMin: number,
  xMax: number,
  numPoints: number,
  variables: Record<string, string> = {}
): Array<GraphPoint | null> {
  if (!expression.trim()) return [];

  const compiled = getCompiled(expression);
  if (!compiled) return [];

  const scope = buildScope(variables);
  const points: Array<GraphPoint | null> = [];
  const step = (xMax - xMin) / numPoints;
  let prevY: number | null = null;
  const jumpThreshold = Math.abs(xMax - xMin) * 4;

  for (let i = 0; i <= numPoints; i++) {
    const x = xMin + i * step;
    let y: number;
    try {
      const r = compiled.evaluate({ ...scope, x });
      y = typeof r === 'number' ? r : NaN;
    } catch {
      y = NaN;
    }

    if (!isFinite(y) || isNaN(y)) {
      if (points.length > 0 && points[points.length - 1] !== null) {
        points.push(null);
      }
      prevY = null;
    } else {
      if (prevY !== null && Math.abs(y - prevY) > jumpThreshold) {
        points.push(null);
      }
      points.push({ x, y });
      prevY = y;
    }
  }

  return points;
}
