/**
 * PineScript → swym DSL converter (Tier 1 subset).
 *
 * Handles:
 *   - OHLCV fields (open, high, low, close, volume) + bar offsets (close[1])
 *   - ta.highest/lowest/sma/ema/rsi/stdev
 *   - ta.crossover / ta.crossunder
 *   - Arithmetic (+, -, *, /) and comparisons (>, <, >=, <=, ==)
 *   - Boolean (and, or, not)
 *   - strategy.entry() / strategy.close() with explicit `when=` or inside `if` blocks
 *   - Variable declarations (inline substitution during transform)
 *   - strategy() header (extracts default_qty_value)
 *
 * Unsupported constructs produce warnings (non-fatal) rather than errors.
 */

// ---------------------------------------------------------------------------
// Public API types
// ---------------------------------------------------------------------------

export interface SwymExpr {
  kind: string;
  [key: string]: unknown;
}

export interface SwymCondition {
  kind: string;
  [key: string]: unknown;
}

export interface SwymAction {
  side: 'buy' | 'sell';
  quantity: string;
}

export interface SwymRule {
  comment?: string;
  when: SwymCondition;
  then: SwymAction;
}

export interface ConvertWarning {
  line: number;
  message: string;
}

export interface ConvertResult {
  rules: SwymRule[];
  interval: string | null;
  quantity: string;
  warnings: ConvertWarning[];
}

// ---------------------------------------------------------------------------
// Tokenizer
// ---------------------------------------------------------------------------

type TokKind =
  | 'NUMBER'
  | 'STRING'
  | 'IDENT'
  | 'DOT'
  | 'LPAREN'
  | 'RPAREN'
  | 'LBRACKET'
  | 'RBRACKET'
  | 'COMMA'
  | 'ASSIGN'
  | 'OP' // +, -, *, /, >, <, >=, <=, ==, !=
  | 'AND'
  | 'OR'
  | 'NOT'
  | 'EOF';

interface Token {
  kind: TokKind;
  value: string;
  line: number;
}

function tokenizeExpr(src: string, lineNo: number): Token[] {
  const tokens: Token[] = [];
  let i = 0;
  while (i < src.length) {
    if (/\s/.test(src[i])) {
      i++;
      continue;
    }
    // String literal
    if (src[i] === '"' || src[i] === "'") {
      const q = src[i];
      let j = i + 1;
      while (j < src.length && src[j] !== q) j++;
      tokens.push({ kind: 'STRING', value: src.slice(i + 1, j), line: lineNo });
      i = j + 1;
      continue;
    }
    // Number
    if (/[0-9]/.test(src[i])) {
      let j = i;
      while (j < src.length && /[0-9.]/.test(src[j])) j++;
      tokens.push({ kind: 'NUMBER', value: src.slice(i, j), line: lineNo });
      i = j;
      continue;
    }
    // Two-char operators
    const two = src.slice(i, i + 2);
    if (['>=', '<=', '==', '!=', ':='].includes(two)) {
      if (two === ':=') {
        tokens.push({ kind: 'ASSIGN', value: two, line: lineNo });
      } else {
        tokens.push({ kind: 'OP', value: two, line: lineNo });
      }
      i += 2;
      continue;
    }
    // Single-char
    if ('+-*/><'.includes(src[i])) {
      tokens.push({ kind: 'OP', value: src[i], line: lineNo });
      i++;
      continue;
    }
    if (src[i] === '=') {
      tokens.push({ kind: 'ASSIGN', value: '=', line: lineNo });
      i++;
      continue;
    }
    if (src[i] === '(') {
      tokens.push({ kind: 'LPAREN', value: '(', line: lineNo });
      i++;
      continue;
    }
    if (src[i] === ')') {
      tokens.push({ kind: 'RPAREN', value: ')', line: lineNo });
      i++;
      continue;
    }
    if (src[i] === '[') {
      tokens.push({ kind: 'LBRACKET', value: '[', line: lineNo });
      i++;
      continue;
    }
    if (src[i] === ']') {
      tokens.push({ kind: 'RBRACKET', value: ']', line: lineNo });
      i++;
      continue;
    }
    if (src[i] === ',') {
      tokens.push({ kind: 'COMMA', value: ',', line: lineNo });
      i++;
      continue;
    }
    if (src[i] === '.') {
      tokens.push({ kind: 'DOT', value: '.', line: lineNo });
      i++;
      continue;
    }
    // Identifier / keyword
    if (/[a-zA-Z_]/.test(src[i])) {
      let j = i;
      while (j < src.length && /[a-zA-Z0-9_]/.test(src[j])) j++;
      const word = src.slice(i, j);
      if (word === 'and') tokens.push({ kind: 'AND', value: word, line: lineNo });
      else if (word === 'or') tokens.push({ kind: 'OR', value: word, line: lineNo });
      else if (word === 'not') tokens.push({ kind: 'NOT', value: word, line: lineNo });
      else tokens.push({ kind: 'IDENT', value: word, line: lineNo });
      i = j;
      continue;
    }
    // Skip unknown
    i++;
  }
  tokens.push({ kind: 'EOF', value: '', line: lineNo });
  return tokens;
}

// ---------------------------------------------------------------------------
// AST types
// ---------------------------------------------------------------------------

type AstExpr =
  | { type: 'number'; value: string; line: number }
  | { type: 'string'; value: string; line: number }
  | { type: 'ident'; name: string; line: number }
  | { type: 'dot_access'; object: string; member: string; line: number }
  | {
      type: 'func_call';
      func: string;
      args: AstExpr[];
      kwargs: Record<string, AstExpr>;
      line: number;
    }
  | { type: 'subscript'; expr: AstExpr; index: AstExpr; line: number }
  | { type: 'bin_op'; op: string; left: AstExpr; right: AstExpr; line: number }
  | { type: 'unary'; op: string; expr: AstExpr; line: number }
  | { type: 'bool_op'; op: 'and' | 'or'; left: AstExpr; right: AstExpr; line: number };

// ---------------------------------------------------------------------------
// Expression parser (recursive descent)
// ---------------------------------------------------------------------------

class ExprParser {
  private toks: Token[];
  private pos = 0;

  constructor(toks: Token[]) {
    this.toks = toks;
  }

  private peek(): Token {
    return this.toks[this.pos];
  }
  private advance(): Token {
    return this.toks[this.pos++];
  }
  private eat(kind: TokKind): Token {
    const t = this.peek();
    if (t.kind !== kind) throw new Error(`Expected ${kind} got ${t.kind} ("${t.value}")`);
    return this.advance();
  }

  parse(): AstExpr {
    return this.parseBoolOr();
  }

  private parseBoolOr(): AstExpr {
    let left = this.parseBoolAnd();
    while (this.peek().kind === 'OR') {
      const line = this.peek().line;
      this.advance();
      const right = this.parseBoolAnd();
      left = { type: 'bool_op', op: 'or', left, right, line };
    }
    return left;
  }

  private parseBoolAnd(): AstExpr {
    let left = this.parseNot();
    while (this.peek().kind === 'AND') {
      const line = this.peek().line;
      this.advance();
      const right = this.parseNot();
      left = { type: 'bool_op', op: 'and', left, right, line };
    }
    return left;
  }

  private parseNot(): AstExpr {
    if (this.peek().kind === 'NOT') {
      const line = this.peek().line;
      this.advance();
      const expr = this.parseNot();
      return { type: 'unary', op: 'not', expr, line };
    }
    return this.parseComparison();
  }

  private parseComparison(): AstExpr {
    const left = this.parseAddSub();
    const opTok = this.peek();
    if (opTok.kind === 'OP' && ['>', '<', '>=', '<=', '==', '!='].includes(opTok.value)) {
      this.advance();
      const right = this.parseAddSub();
      return { type: 'bin_op', op: opTok.value, left, right, line: opTok.line };
    }
    return left;
  }

  private parseAddSub(): AstExpr {
    let left = this.parseMulDiv();
    while (this.peek().kind === 'OP' && ['+', '-'].includes(this.peek().value)) {
      const op = this.peek().value;
      const line = this.peek().line;
      this.advance();
      left = { type: 'bin_op', op, left, right: this.parseMulDiv(), line };
    }
    return left;
  }

  private parseMulDiv(): AstExpr {
    let left = this.parseUnary();
    while (this.peek().kind === 'OP' && ['*', '/'].includes(this.peek().value)) {
      const op = this.peek().value;
      const line = this.peek().line;
      this.advance();
      left = { type: 'bin_op', op, left, right: this.parseUnary(), line };
    }
    return left;
  }

  private parseUnary(): AstExpr {
    if (this.peek().kind === 'OP' && this.peek().value === '-') {
      const line = this.peek().line;
      this.advance();
      return { type: 'unary', op: '-', expr: this.parsePrimary(), line };
    }
    return this.parsePrimary();
  }

  private parsePrimary(): AstExpr {
    const tok = this.peek();

    if (tok.kind === 'NUMBER') {
      this.advance();
      return { type: 'number', value: tok.value, line: tok.line };
    }

    if (tok.kind === 'STRING') {
      this.advance();
      return { type: 'string', value: tok.value, line: tok.line };
    }

    if (tok.kind === 'LPAREN') {
      this.advance();
      const expr = this.parse();
      if (this.peek().kind === 'RPAREN') this.advance();
      return expr;
    }

    if (tok.kind === 'IDENT') {
      this.advance();
      const name = tok.value;

      // Dot access: name.member or name.member(...)
      if (this.peek().kind === 'DOT') {
        this.advance();
        const member = this.eat('IDENT').value;
        if (this.peek().kind === 'LPAREN') {
          const { args, kwargs } = this.parseCallArgs();
          let result: AstExpr = {
            type: 'func_call',
            func: `${name}.${member}`,
            args,
            kwargs,
            line: tok.line,
          };
          if (this.peek().kind === 'LBRACKET') {
            result = this.parseSubscript(result);
          }
          return result;
        }
        return { type: 'dot_access', object: name, member, line: tok.line };
      }

      // Function call: name(...)
      if (this.peek().kind === 'LPAREN') {
        const { args, kwargs } = this.parseCallArgs();
        let result: AstExpr = {
          type: 'func_call',
          func: name,
          args,
          kwargs,
          line: tok.line,
        };
        if (this.peek().kind === 'LBRACKET') {
          result = this.parseSubscript(result);
        }
        return result;
      }

      // Subscript: name[N]
      if (this.peek().kind === 'LBRACKET') {
        return this.parseSubscript({ type: 'ident', name, line: tok.line });
      }

      return { type: 'ident', name, line: tok.line };
    }

    // Fallback: skip unknown token
    this.advance();
    return { type: 'number', value: '0', line: tok.line };
  }

  private parseSubscript(base: AstExpr): AstExpr {
    this.advance(); // consume [
    const index = this.parse();
    if (this.peek().kind === 'RBRACKET') this.advance();
    return { type: 'subscript', expr: base, index, line: base.line };
  }

  parseCallArgs(): { args: AstExpr[]; kwargs: Record<string, AstExpr> } {
    this.eat('LPAREN');
    const args: AstExpr[] = [];
    const kwargs: Record<string, AstExpr> = {};
    while (this.peek().kind !== 'RPAREN' && this.peek().kind !== 'EOF') {
      // Keyword arg: name = expr
      if (
        this.peek().kind === 'IDENT' &&
        this.toks[this.pos + 1]?.kind === 'ASSIGN'
      ) {
        const key = this.advance().value;
        this.advance(); // consume =
        kwargs[key] = this.parse();
      } else {
        args.push(this.parse());
      }
      if (this.peek().kind === 'COMMA') this.advance();
    }
    if (this.peek().kind === 'RPAREN') this.advance();
    return { args, kwargs };
  }
}

function parseExpr(src: string, lineNo: number): AstExpr | null {
  const s = src.trim();
  if (!s) return null;
  try {
    const toks = tokenizeExpr(s, lineNo);
    return new ExprParser(toks).parse();
  } catch {
    return null;
  }
}

// ---------------------------------------------------------------------------
// Line-level pre-processor
// ---------------------------------------------------------------------------

interface SourceLine {
  lineNo: number;
  indent: number;
  text: string; // trimmed, comment-stripped
}

function preprocessSource(source: string): SourceLine[] {
  return source
    .split('\n')
    .map((raw, i) => {
      // Strip inline comment (but not inside strings — good enough for our use)
      const commentIdx = raw.indexOf('//');
      const stripped = commentIdx >= 0 ? raw.slice(0, commentIdx) : raw;
      const trimmed = stripped.trimStart();
      return {
        lineNo: i + 1,
        indent: stripped.length - trimmed.length,
        text: trimmed.trimEnd(),
      };
    })
    .filter((l) => l.text.length > 0);
}

// Parsed top-level "statements" after grouping if blocks
interface StrategyCallStmt {
  kind: 'strategy_header' | 'strategy_entry' | 'strategy_close';
  args: AstExpr[];
  kwargs: Record<string, AstExpr>;
  line: number;
}

interface VarDeclStmt {
  kind: 'var_decl';
  name: string;
  rhs: AstExpr;
  line: number;
}

interface IfBlockStmt {
  kind: 'if_block';
  condition: AstExpr;
  body: Array<StrategyCallStmt | VarDeclStmt>;
  line: number;
}

type ParsedStmt = StrategyCallStmt | VarDeclStmt | IfBlockStmt | { kind: 'ignored'; line: number };

// ---------------------------------------------------------------------------
// Line classifier
// ---------------------------------------------------------------------------

function classifyLine(line: SourceLine): ParsedStmt | null {
  const { text, lineNo } = line;

  // strategy(...) header — no dot after strategy
  const stratHeaderMatch = text.match(/^strategy\s*\(/);
  if (stratHeaderMatch) {
    return parseStrategyCall('strategy_header', `strategy${text.slice(text.indexOf('('))}`, lineNo);
  }

  // strategy.entry(...)
  if (/^strategy\.entry\s*\(/.test(text)) {
    return parseStrategyCall('strategy_entry', text, lineNo);
  }

  // strategy.close(...) or strategy.exit(...)
  if (/^strategy\.(close|exit)\s*\(/.test(text)) {
    return parseStrategyCall('strategy_close', text, lineNo);
  }

  // Variable declaration: name = expr or name := expr
  // Must not start with 'if', 'for', 'while', etc.
  const varMatch = text.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:?=\s*(.+)$/);
  if (varMatch && !['if', 'for', 'while', 'var', 'varip'].includes(varMatch[1])) {
    // Skip `var float x = na` style declarations — just extract the name and rhs
    const rhs = parseExpr(varMatch[2], lineNo);
    if (rhs) {
      return { kind: 'var_decl', name: varMatch[1], rhs, line: lineNo };
    }
  }

  // `var type name = ...` style
  const varTypedMatch = text.match(/^var\s+\w+\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/);
  if (varTypedMatch) {
    const rhs = parseExpr(varTypedMatch[2], lineNo);
    if (rhs) {
      return { kind: 'var_decl', name: varTypedMatch[1], rhs, line: lineNo };
    }
  }

  // if block (handled by caller via indent tracking)
  if (/^if\s+/.test(text)) return null; // caller handles this

  return { kind: 'ignored', line: lineNo };
}

function parseStrategyCall(
  kind: 'strategy_header' | 'strategy_entry' | 'strategy_close',
  text: string,
  lineNo: number,
): StrategyCallStmt | { kind: 'ignored'; line: number } {
  // Extract the argument list from the parens
  const parenStart = text.indexOf('(');
  if (parenStart < 0) return { kind: 'ignored', line: lineNo };
  const argsStr = text.slice(parenStart); // "(arg1, arg2, key=val)"
  try {
    const toks = tokenizeExpr(argsStr, lineNo);
    const parser = new ExprParser(toks);
    const { args, kwargs } = parser.parseCallArgs();
    return { kind, args, kwargs, line: lineNo };
  } catch {
    return { kind: 'ignored', line: lineNo };
  }
}

function groupStatements(lines: SourceLine[]): ParsedStmt[] {
  const result: ParsedStmt[] = [];
  let i = 0;

  while (i < lines.length) {
    const line = lines[i];

    if (/^if\s+/.test(line.text)) {
      // Parse the condition
      const condSrc = line.text.replace(/^if\s+/, '');
      const cond = parseExpr(condSrc, line.lineNo);
      if (!cond) {
        i++;
        continue;
      }

      // Collect body lines (greater indentation)
      const bodyLines: SourceLine[] = [];
      i++;
      while (i < lines.length && lines[i].indent > line.indent) {
        bodyLines.push(lines[i]);
        i++;
      }

      const body: Array<StrategyCallStmt | VarDeclStmt> = [];
      for (const bl of bodyLines) {
        const stmt = classifyLine(bl);
        if (
          stmt &&
          (stmt.kind === 'strategy_entry' ||
            stmt.kind === 'strategy_close' ||
            stmt.kind === 'var_decl')
        ) {
          body.push(stmt as StrategyCallStmt | VarDeclStmt);
        }
      }

      result.push({ kind: 'if_block', condition: cond, body, line: line.lineNo });
      continue;
    }

    const stmt = classifyLine(line);
    if (stmt) result.push(stmt);
    i++;
  }

  return result;
}

// ---------------------------------------------------------------------------
// Transformer: AST → swym DSL
// ---------------------------------------------------------------------------

const OHLCV_FIELDS = new Set(['open', 'high', 'low', 'close', 'volume']);

const TA_FUNC_MAP: Record<string, string> = {
  highest: 'highest',
  lowest: 'lowest',
  sma: 'sma',
  ema: 'ema',
  rsi: 'rsi',
  stdev: 'std_dev',
  dev: 'std_dev',
};

const ARITH_OP_MAP: Record<string, string> = {
  '+': 'add',
  '-': 'sub',
  '*': 'mul',
  '/': 'div',
};

class Transformer {
  private vars = new Map<string, AstExpr>();
  readonly warnings: ConvertWarning[] = [];

  warn(line: number, msg: string) {
    this.warnings.push({ line, message: msg });
  }

  setVar(name: string, expr: AstExpr) {
    this.vars.set(name, expr);
  }

  private resolve(expr: AstExpr, depth = 0): AstExpr {
    if (depth > 20) return expr; // prevent infinite loops
    if (expr.type === 'ident' && this.vars.has(expr.name)) {
      return this.resolve(this.vars.get(expr.name)!, depth + 1);
    }
    return expr;
  }

  transformExpr(raw: AstExpr): SwymExpr | null {
    const expr = this.resolve(raw);

    switch (expr.type) {
      case 'number':
        return { kind: 'literal', value: expr.value };

      case 'ident': {
        if (OHLCV_FIELDS.has(expr.name)) {
          return { kind: 'field', field: expr.name };
        }
        this.warn(expr.line, `Unknown identifier '${expr.name}' in expression — skipped`);
        return null;
      }

      case 'subscript': {
        const base = this.resolve(expr.expr);
        const idxExpr = this.resolve(expr.index);
        const offset = idxExpr.type === 'number' ? parseInt(idxExpr.value, 10) : null;
        if (offset === null) {
          this.warn(expr.line, 'Non-literal subscript index — skipped');
          return null;
        }
        if (base.type === 'ident' && OHLCV_FIELDS.has(base.name)) {
          return { kind: 'field', field: base.name, offset };
        }
        if (base.type === 'func_call') {
          return this.transformFuncExpr(base, offset);
        }
        this.warn(expr.line, 'Subscript on unsupported expression — skipped');
        return null;
      }

      case 'func_call':
        return this.transformFuncExpr(expr, 0);

      case 'bin_op': {
        if (ARITH_OP_MAP[expr.op]) {
          const left = this.transformExpr(expr.left);
          const right = this.transformExpr(expr.right);
          if (!left || !right) return null;
          return { kind: 'bin_op', op: ARITH_OP_MAP[expr.op], left, right };
        }
        this.warn(expr.line, `Comparison '${expr.op}' cannot be used as numeric expression`);
        return null;
      }

      case 'unary': {
        if (expr.op === '-') {
          const inner = this.transformExpr(expr.expr);
          if (!inner) return null;
          if (inner.kind === 'literal') {
            return { kind: 'literal', value: '-' + (inner.value as string) };
          }
          return {
            kind: 'bin_op',
            op: 'sub',
            left: { kind: 'literal', value: '0' },
            right: inner,
          };
        }
        return null;
      }

      default:
        this.warn(raw.line, `Unsupported expression type — skipped`);
        return null;
    }
  }

  private transformFuncExpr(
    expr: AstExpr & { type: 'func_call' },
    extraOffset: number,
  ): SwymExpr | null {
    const { func, args, line } = expr;

    if (!func.startsWith('ta.')) {
      this.warn(line, `Non-ta function '${func}' used as expression — skipped`);
      return null;
    }

    const taFn = func.slice(3);
    const mappedFn = TA_FUNC_MAP[taFn];

    if (!mappedFn) {
      this.warn(line, `ta.${taFn}() is not supported in the swym DSL — skipped`);
      return null;
    }

    if (args.length < 2) {
      this.warn(line, `ta.${taFn}() requires at least 2 arguments`);
      return null;
    }

    const fieldArg = this.resolve(args[0]);
    const periodArg = this.resolve(args[1]);

    if (fieldArg.type !== 'ident' || !OHLCV_FIELDS.has(fieldArg.name)) {
      this.warn(
        line,
        `ta.${taFn}() first argument must be an OHLCV field — nested functions are not supported`,
      );
      return null;
    }
    if (periodArg.type !== 'number') {
      this.warn(line, `ta.${taFn}() period must be a numeric literal`);
      return null;
    }

    const result: SwymExpr = {
      kind: 'func',
      name: mappedFn,
      field: fieldArg.name,
      period: parseInt(periodArg.value, 10),
    };
    if (extraOffset > 0) result.offset = extraOffset;
    return result;
  }

  transformCondition(raw: AstExpr): SwymCondition | null {
    const expr = this.resolve(raw);

    switch (expr.type) {
      case 'bool_op': {
        const left = this.transformCondition(expr.left);
        const right = this.transformCondition(expr.right);
        if (!left && !right) return null;
        if (!left) return right;
        if (!right) return left;
        return expr.op === 'and'
          ? { kind: 'all_of', conditions: [left, right] }
          : { kind: 'any_of', conditions: [left, right] };
      }

      case 'unary': {
        if (expr.op === 'not') {
          const inner = this.transformCondition(expr.expr);
          if (!inner) return null;
          return { kind: 'not', condition: inner };
        }
        break;
      }

      case 'func_call': {
        const { func, args, line } = expr;
        if (func === 'ta.crossover' || func === 'ta.crossunder') {
          if (args.length < 2) {
            this.warn(line, `${func}() requires 2 arguments`);
            return null;
          }
          const left = this.transformExpr(args[0]);
          const right = this.transformExpr(args[1]);
          if (!left || !right) return null;
          return {
            kind: func === 'ta.crossover' ? 'cross_over' : 'cross_under',
            left,
            right,
          };
        }
        this.warn(line, `Function call '${func}' used as condition — skipped`);
        return null;
      }

      case 'bin_op': {
        const CMP_OPS = ['>', '<', '>=', '<=', '==', '!='];
        if (CMP_OPS.includes(expr.op)) {
          const left = this.transformExpr(expr.left);
          const right = this.transformExpr(expr.right);
          if (!left || !right) return null;
          if (expr.op === '!=') {
            return { kind: 'not', condition: { kind: 'compare', left, op: '==', right } };
          }
          return { kind: 'compare', left, op: expr.op, right };
        }
        this.warn(expr.line, `Arithmetic '${expr.op}' used as condition — skipped`);
        return null;
      }

      case 'ident': {
        // User variable used as a condition — try to resolve
        if (this.vars.has(expr.name)) {
          return this.transformCondition(this.vars.get(expr.name)!);
        }
        this.warn(expr.line, `Unknown identifier '${expr.name}' used as condition — skipped`);
        return null;
      }

      default:
        break;
    }

    this.warn(raw.line, `Cannot convert expression to a condition — skipped`);
    return null;
  }
}

// ---------------------------------------------------------------------------
// Main entry point
// ---------------------------------------------------------------------------

/** Extract candle interval hint from `strategy.entry` / `strategy.close` comments or source. */
function detectInterval(source: string): string | null {
  // Look for common interval mentions in comments or variable names
  const patterns: [RegExp, string][] = [
    [/["']1m["']/, '1m'],
    [/["']5m["']/, '5m'],
    [/["']15m["']/, '15m'],
    [/["']1h["']|["']60m["']/, '1h'],
    [/["']4h["']|["']240m["']/, '4h'],
    [/["']1d["']|["']D["']/, '1d'],
  ];
  for (const [re, iv] of patterns) {
    if (re.test(source)) return iv;
  }
  return null;
}

/**
 * Convert a PineScript source string to a swym DSL rule array.
 *
 * Returns the generated rules, detected candle interval (if any),
 * default quantity, and any conversion warnings.
 */
export function convertPineScript(source: string): ConvertResult {
  const warnings: ConvertWarning[] = [];
  const rules: SwymRule[] = [];
  const interval = detectInterval(source);
  let quantity = '0.001';

  const lines = preprocessSource(source);
  const stmts = groupStatements(lines);

  const transformer = new Transformer();

  // First pass: collect variable bindings and extract strategy() header info
  for (const stmt of stmts) {
    if (stmt.kind === 'var_decl') {
      transformer.setVar(stmt.name, stmt.rhs);
    }
    if (stmt.kind === 'strategy_header') {
      const qty = stmt.kwargs['default_qty_value'];
      if (qty?.type === 'number') quantity = qty.value;
    }
    // Also handle var_decls inside if blocks if they appear
  }

  // Map from entry label → direction, for pairing with close calls
  const entryLabels = new Map<string, 'long' | 'short'>();

  function processEntry(
    stmt: StrategyCallStmt,
    implicitCond: AstExpr | null,
  ) {
    const { args, kwargs, line } = stmt;
    const labelArg = args[0];
    const dirArg = args[1];
    const whenArg = kwargs['when'] ?? implicitCond;
    const qtyArg = kwargs['qty'];

    if (!labelArg || labelArg.type !== 'string') {
      warnings.push({ line, message: 'strategy.entry(): label must be a string' });
      return;
    }
    const label = labelArg.value;

    let direction: 'long' | 'short' = 'long';
    if (dirArg) {
      const dirName =
        dirArg.type === 'dot_access'
          ? dirArg.member
          : dirArg.type === 'ident'
            ? dirArg.name
            : '';
      if (dirName === 'short') direction = 'short';
    }
    entryLabels.set(label, direction);

    const positionCond: SwymCondition = { kind: 'position', state: 'flat' };
    const conditions: SwymCondition[] = [positionCond];

    if (whenArg) {
      const cond = transformer.transformCondition(whenArg);
      if (cond) conditions.push(cond);
    }

    const when: SwymCondition =
      conditions.length === 1 ? conditions[0] : { kind: 'all_of', conditions };

    let ruleQty = quantity;
    if (qtyArg?.type === 'number') ruleQty = qtyArg.value;

    rules.push({
      comment: `${direction === 'long' ? 'Enter long' : 'Enter short'}: ${label}`,
      when,
      then: { side: direction === 'long' ? 'buy' : 'sell', quantity: ruleQty },
    });
  }

  function processClose(
    stmt: StrategyCallStmt,
    implicitCond: AstExpr | null,
  ) {
    const { args, kwargs, line } = stmt;
    const labelArg = args[0];
    const whenArg = kwargs['when'] ?? implicitCond;
    const qtyArg = kwargs['qty'];

    if (!labelArg || labelArg.type !== 'string') {
      warnings.push({ line, message: 'strategy.close(): label must be a string' });
      return;
    }
    const label = labelArg.value;
    const direction = entryLabels.get(label) ?? 'long';
    const posState = direction === 'short' ? 'short' : 'long';

    const positionCond: SwymCondition = { kind: 'position', state: posState };
    const conditions: SwymCondition[] = [positionCond];

    if (whenArg) {
      const cond = transformer.transformCondition(whenArg);
      if (cond) conditions.push(cond);
    }

    const when: SwymCondition =
      conditions.length === 1 ? conditions[0] : { kind: 'all_of', conditions };

    let ruleQty = quantity;
    if (qtyArg?.type === 'number') ruleQty = qtyArg.value;

    rules.push({
      comment: `Exit ${posState}: ${label}`,
      when,
      then: { side: posState === 'long' ? 'sell' : 'buy', quantity: ruleQty },
    });
  }

  // Second pass: process entry/close calls
  for (const stmt of stmts) {
    if (stmt.kind === 'strategy_entry') {
      processEntry(stmt, null);
    } else if (stmt.kind === 'strategy_close') {
      processClose(stmt, null);
    } else if (stmt.kind === 'if_block') {
      for (const bodyStmt of stmt.body) {
        if (bodyStmt.kind === 'strategy_entry') {
          processEntry(bodyStmt, stmt.condition);
        } else if (bodyStmt.kind === 'strategy_close') {
          processClose(bodyStmt, stmt.condition);
        }
      }
    }
  }

  // Propagate warnings from transformer
  warnings.push(...transformer.warnings);

  return { rules, interval, quantity, warnings };
}
