/**
 * VL Workflow Engine — Expression Evaluator
 * Ported from Go: workflow/expression.go
 * Spec: v3.15
 *
 * Syntax:
 *   =expr        → evaluate expression
 *   ==literal    → escape: return "=literal"
 *   plain string → literal
 *   $var         → variable reference
 *   $var.field   → property access
 *   $var[0]      → array index
 *   .length      → array/string length
 *   SYSVAR.name  → system variable
 *   _local       → local variable
 */

class ExpressionEvaluator {
  constructor(ctx) {
    this.ctx = ctx;
  }

  /**
   * Evaluate a value — if string starting with "=", evaluate as expression.
   * Non-strings returned as-is. "==X" returns "=X".
   */
  evaluateValue(value) {
    if (typeof value !== 'string') return value;
    if (!value.startsWith('=')) return value;
    if (value.startsWith('==')) return value.slice(1); // escape
    return this.evaluate(value.slice(1));
  }

  /**
   * Evaluate an expression string (without "=" prefix).
   */
  evaluate(expr) {
    expr = expr.trim();
    if (!expr) return null;

    // Ternary: a ? b : c
    const ternary = this._findTernary(expr);
    if (ternary) {
      const cond = this.evaluate(ternary.cond);
      return toBool(cond) ? this.evaluate(ternary.ifTrue) : this.evaluate(ternary.ifFalse);
    }

    // Logical OR: a || b
    let idx = this._findOperator(expr, '||');
    if (idx >= 0) {
      const left = this.evaluate(expr.slice(0, idx));
      if (toBool(left)) return left;
      return this.evaluate(expr.slice(idx + 2));
    }

    // Logical AND: a && b
    idx = this._findOperator(expr, '&&');
    if (idx >= 0) {
      const left = this.evaluate(expr.slice(0, idx));
      if (!toBool(left)) return left;
      return this.evaluate(expr.slice(idx + 2));
    }

    // Comparison: ===, !==, ==, !=, >=, <=, >, <
    // NOTE: === and !== must come before == and != to avoid partial matching
    for (const op of ['===', '!==', '==', '!=', '>=', '<=', '>', '<']) {
      idx = this._findOperator(expr, op);
      if (idx >= 0) {
        const left = this.evaluate(expr.slice(0, idx));
        const right = this.evaluate(expr.slice(idx + op.length));
        return this._compare(left, right, op);
      }
    }

    // Arithmetic: +, -
    for (const op of ['+', '-']) {
      idx = this._findOperatorReverse(expr, op);
      if (idx > 0) {
        const left = this.evaluate(expr.slice(0, idx));
        const right = this.evaluate(expr.slice(idx + 1));
        if (op === '+' && (typeof left === 'string' || typeof right === 'string')) {
          return String(left ?? '') + String(right ?? '');
        }
        return op === '+' ? toFloat(left) + toFloat(right) : toFloat(left) - toFloat(right);
      }
    }

    // Multiply, divide: *, /
    for (const op of ['*', '/']) {
      idx = this._findOperatorReverse(expr, op);
      if (idx > 0) {
        const left = this.evaluate(expr.slice(0, idx));
        const right = this.evaluate(expr.slice(idx + 1));
        return op === '*' ? toFloat(left) * toFloat(right) : toFloat(left) / toFloat(right);
      }
    }

    // Unary NOT: !expr
    if (expr.startsWith('!')) {
      return !toBool(this.evaluate(expr.slice(1)));
    }

    // Parenthesized: (expr)
    if (expr.startsWith('(') && this._findMatchingParen(expr, 0) === expr.length - 1) {
      return this.evaluate(expr.slice(1, -1));
    }

    // Literals
    if (expr === 'true') return true;
    if (expr === 'false') return false;
    if (expr === 'null' || expr === 'nil') return null;

    // Number
    if (/^-?\d+(\.\d+)?$/.test(expr)) {
      return expr.includes('.') ? parseFloat(expr) : parseInt(expr, 10);
    }

    // String literal: "..." or '...'
    if ((expr.startsWith('"') && expr.endsWith('"')) || (expr.startsWith("'") && expr.endsWith("'"))) {
      return expr.slice(1, -1);
    }

    // JSON array: [...]
    if (expr.startsWith('[') && expr.endsWith(']')) {
      try { return JSON.parse(expr); } catch { /* fall through to variable */ }
    }

    // JSON object: {...}
    if (expr.startsWith('{') && expr.endsWith('}')) {
      try { return JSON.parse(expr); } catch { /* fall through */ }
    }

    // Variable reference with path navigation
    return this._resolveVariable(expr);
  }

  /**
   * Recursively evaluate all expressions inside maps and arrays.
   */
  evaluateDeep(value) {
    if (value === null || value === undefined) return value;
    if (typeof value === 'string') return this.evaluateValue(value);
    if (Array.isArray(value)) return value.map(v => this.evaluateDeep(v));
    if (typeof value === 'object') {
      const result = {};
      for (const [k, v] of Object.entries(value)) {
        result[k] = this.evaluateDeep(v);
      }
      return result;
    }
    return value;
  }

  /**
   * Set a variable by path: "$var.field[0].nested"
   */
  setVariable(path, value) {
    path = path.trim();
    // Simple case: no path navigation
    if (!path.includes('.') && !path.includes('[')) {
      value = this._applyTypeConversion(path, value);
      this.ctx.setVariable(path, value);
      return;
    }
    // Complex path: navigate and set
    const segments = this._parsePath(path);
    if (segments.length === 0) throw new Error(`Invalid path: ${path}`);

    const rootName = segments[0];
    let root = this.ctx.getVariable(rootName);

    if (segments.length === 1) {
      value = this._applyTypeConversion(rootName, value);
      this.ctx.setVariable(rootName, value);
      return;
    }

    // Auto-create root if null
    if (root == null) {
      root = typeof segments[1] === 'number' ? [] : {};
      this.ctx.setVariable(rootName, root);
    }

    let current = root;
    for (let i = 1; i < segments.length - 1; i++) {
      const seg = segments[i];
      const nextSeg = segments[i + 1];
      if (typeof seg === 'number') {
        // Array index — auto-grow
        while (Array.isArray(current) && current.length <= seg) current.push(null);
        if (current[seg] == null) current[seg] = typeof nextSeg === 'number' ? [] : {};
        current = current[seg];
      } else {
        if (current[seg] == null) current[seg] = typeof nextSeg === 'number' ? [] : {};
        current = current[seg];
      }
    }

    const lastSeg = segments[segments.length - 1];
    if (typeof lastSeg === 'number') {
      while (Array.isArray(current) && current.length <= lastSeg) current.push(null);
      current[lastSeg] = value;
    } else {
      current[lastSeg] = value;
    }
  }

  // ─── Internal ──────────────────────────────────────────────

  _resolveVariable(expr) {
    const segments = this._parsePath(expr);
    if (segments.length === 0) return undefined;

    // SYSVAR.xxx: reconstruct the full key for getVariable
    let startIdx = 1;
    let value;
    if (segments[0] === 'SYSVAR' && segments.length >= 2) {
      value = this.ctx.getVariable('SYSVAR.' + segments[1]);
      startIdx = 2;
    } else {
      value = this.ctx.getVariable(segments[0]);
    }
    for (let i = startIdx; i < segments.length; i++) {
      if (value == null) return undefined;
      const seg = segments[i];
      if (seg === 'length') {
        if (Array.isArray(value)) return value.length;
        if (typeof value === 'string') return value.length;
      }
      if (typeof seg === 'number') {
        value = Array.isArray(value) ? value[seg] : (value[seg] ?? undefined);
      } else {
        value = value[seg] ?? undefined;
      }
    }
    return value;
  }

  _parsePath(path) {
    const segments = [];
    let i = 0;
    let current = '';

    while (i < path.length) {
      const ch = path[i];
      if (ch === '.') {
        if (current) segments.push(current);
        current = '';
        i++;
      } else if (ch === '[') {
        if (current) segments.push(current);
        current = '';
        const end = path.indexOf(']', i);
        if (end < 0) throw new Error(`Unmatched [ in path: ${path}`);
        const inside = path.slice(i + 1, end).trim();
        // Number index or expression
        if (/^\d+$/.test(inside)) {
          segments.push(parseInt(inside, 10));
        } else if ((inside.startsWith('"') && inside.endsWith('"')) || (inside.startsWith("'") && inside.endsWith("'"))) {
          segments.push(inside.slice(1, -1));
        } else {
          // Evaluate expression as index
          const idx = this.evaluate(inside);
          segments.push(typeof idx === 'number' ? idx : String(idx));
        }
        i = end + 1;
      } else {
        current += ch;
        i++;
      }
    }
    if (current) segments.push(current);
    return segments;
  }

  _applyTypeConversion(varName, value) {
    const types = this.ctx.varTypes || {};
    const type = types[varName];
    if (!type || value == null) return value;

    if (type === 'OBJECT' && typeof value === 'string') {
      try { return JSON.parse(value); } catch { return value; }
    }
    if (type.startsWith('[') && typeof value === 'string') {
      try { return JSON.parse(value); } catch { return value; }
    }
    if (type === 'STRING' && typeof value !== 'string') {
      return typeof value === 'object' ? JSON.stringify(value) : String(value);
    }
    return value;
  }

  _findTernary(expr) {
    let depth = 0;
    for (let i = 0; i < expr.length; i++) {
      const ch = expr[i];
      if (ch === '(' || ch === '[' || ch === '{') depth++;
      else if (ch === ')' || ch === ']' || ch === '}') depth--;
      else if (ch === '"' || ch === "'") { i = this._skipString(expr, i); continue; }
      else if (ch === '?' && depth === 0) {
        // Find matching :
        let d2 = 0;
        for (let j = i + 1; j < expr.length; j++) {
          const c2 = expr[j];
          if (c2 === '(' || c2 === '[' || c2 === '{') d2++;
          else if (c2 === ')' || c2 === ']' || c2 === '}') d2--;
          else if (c2 === '"' || c2 === "'") { j = this._skipString(expr, j); continue; }
          else if (c2 === ':' && d2 === 0) {
            return { cond: expr.slice(0, i), ifTrue: expr.slice(i + 1, j), ifFalse: expr.slice(j + 1) };
          }
        }
      }
    }
    return null;
  }

  _findOperator(expr, op) {
    let depth = 0;
    for (let i = 0; i <= expr.length - op.length; i++) {
      const ch = expr[i];
      if (ch === '(' || ch === '[' || ch === '{') depth++;
      else if (ch === ')' || ch === ']' || ch === '}') depth--;
      else if (ch === '"' || ch === "'") { i = this._skipString(expr, i); continue; }
      else if (depth === 0 && expr.slice(i, i + op.length) === op) {
        // Disambiguate: don't match partial operators
        if (op === '==' && i > 0 && '!><'.includes(expr[i - 1])) continue;
        if (op === '===' && i > 0 && '!'.includes(expr[i - 1])) continue;
        // Don't match == when === exists at this position
        if (op === '==' && i + 2 < expr.length && expr[i + 2] === '=') continue;
        if (op === '!=' && i + 2 < expr.length && expr[i + 2] === '=') continue;
        return i;
      }
    }
    return -1;
  }

  _findOperatorReverse(expr, op) {
    let depth = 0;
    // Pre-scan: build a set of positions inside string literals so we can skip them
    const inString = new Uint8Array(expr.length);
    for (let s = 0; s < expr.length; s++) {
      if (expr[s] === '"' || expr[s] === "'") {
        const end = this._skipString(expr, s);
        for (let k = s; k <= end; k++) inString[k] = 1;
        s = end;
      }
    }
    for (let i = expr.length - 1; i >= 1; i--) {
      if (inString[i]) continue;
      const ch = expr[i];
      if (ch === ')' || ch === ']' || ch === '}') depth++;
      else if (ch === '(' || ch === '[' || ch === '{') depth--;
      else if (depth === 0 && expr[i] === op) {
        // For - : skip if previous char is an operator (unary minus)
        if (op === '-') {
          const prev = expr[i - 1];
          if ('(+-*/=<>!&|,'.includes(prev)) continue;
        }
        return i;
      }
    }
    return -1;
  }

  _skipString(expr, start) {
    const quote = expr[start];
    for (let i = start + 1; i < expr.length; i++) {
      if (expr[i] === '\\') { i++; continue; }
      if (expr[i] === quote) return i;
    }
    return expr.length - 1;
  }

  _findMatchingParen(expr, start) {
    let depth = 0;
    for (let i = start; i < expr.length; i++) {
      if (expr[i] === '(') depth++;
      else if (expr[i] === ')') { depth--; if (depth === 0) return i; }
      else if (expr[i] === '"' || expr[i] === "'") i = this._skipString(expr, i);
    }
    return -1;
  }

  _compare(left, right, op) {
    switch (op) {
      case '===': return left === right;
      case '!==': return left !== right;
      case '==': return left == right; // eslint-disable-line eqeqeq
      case '!=': return left != right; // eslint-disable-line eqeqeq
      case '>=': return toFloat(left) >= toFloat(right);
      case '<=': return toFloat(left) <= toFloat(right);
      case '>':  return toFloat(left) > toFloat(right);
      case '<':  return toFloat(left) < toFloat(right);
    }
    return false;
  }
}

// ─── Helpers ─────────────────────────────────────────────────

function toBool(val) {
  if (val == null) return false;
  if (typeof val === 'boolean') return val;
  if (typeof val === 'number') return val !== 0;
  if (typeof val === 'string') return val !== '';
  return true;
}

function toFloat(val) {
  if (typeof val === 'number') return val;
  if (typeof val === 'string') { const n = Number(val); return isNaN(n) ? 0 : n; }
  if (typeof val === 'boolean') return val ? 1 : 0;
  return 0;
}

module.exports = { ExpressionEvaluator, toBool, toFloat };
