import { streamResponse } from './llm-provider.js';
import { loadHealthData } from './health.js';
import { loadProfile } from './profile.js';
import { fmt, fmtPct, fmtPctSigned, fmtCompact, fmtCompactSigned, fmtMonths } from './fmt.js';

// ─── Types ────────────────────────────────────────────────────────────────────

export type DialFormat = 'dollar' | 'percent' | 'integer' | 'months' | 'years';

export type DialDef = {
  key: string;
  label: string;
  default: number;
  step: number;
  min?: number;
  max?: number;
  format: DialFormat;
  hint: string;
};

export type OutputDef = {
  label: string;
  expr: string;         // pure JS arithmetic over dial keys — no side effects
  format: DialFormat;
  color?: 'positive' | 'negative' | 'neutral' | 'accent';
  signed?: boolean;     // show explicit +/- prefix (use for deltas and values that can be negative)
};

export type CanvasElement =
  | { type: 'section'; label: string }
  | { type: 'text';    content: string }
  | { type: 'dial';    dial: DialDef }
  | { type: 'output';  output: OutputDef };

export type CanvasSpec = {
  title: string;
  elements: CanvasElement[];
};

// ─── Expression evaluator ─────────────────────────────────────────────────────
// Safe recursive-descent parser — no new Function / eval. Supports the grammar
// documented in the canvas system prompt: number/Infinity literals, dial keys,
// parentheses, unary +/-, arithmetic, comparisons, ternary, Math.{pow,log,abs,
// round,floor,ceil}. Unknown identifiers resolve to NaN; anything outside the
// grammar throws and is caught as NaN.

const MATH_FNS: Record<string, (...a: number[]) => number> = {
  pow: Math.pow, log: Math.log, abs: Math.abs,
  round: Math.round, floor: Math.floor, ceil: Math.ceil,
};

function lex(src: string): string[] {
  const re = /(\d+\.?\d*|\.\d+|Infinity|Math\.[a-z]+|[A-Za-z_]\w*|<=|>=|==|!=|[-+*/()<>?:,])|(\s+)/y;
  const out: string[] = [];
  let i = 0;
  while (i < src.length) {
    re.lastIndex = i;
    const m = re.exec(src);
    if (!m) throw new Error(`invalid token near "${src.slice(i, i + 12)}"`);
    i = re.lastIndex;
    if (m[1] !== undefined) out.push(m[1]);
  }
  return out;
}

function parseEval(toks: string[], scope: Record<string, number>): number {
  let p = 0;
  const peek = () => toks[p];
  const next = () => toks[p++];
  const expect = (t: string) => { if (next() !== t) throw new Error(`expected ${t}`); };

  function ternary(): number {
    const cond = comparison();
    if (peek() === '?') {
      next();
      const a = ternary();
      expect(':');
      const b = ternary();
      return cond !== 0 ? a : b;
    }
    return cond;
  }
  function comparison(): number {
    const l = additive();
    const op = peek();
    if (op === '<' || op === '<=' || op === '>' || op === '>=' || op === '==' || op === '!=') {
      next();
      const r = additive();
      if (op === '<')  return l < r  ? 1 : 0;
      if (op === '<=') return l <= r ? 1 : 0;
      if (op === '>')  return l > r  ? 1 : 0;
      if (op === '>=') return l >= r ? 1 : 0;
      if (op === '==') return l === r ? 1 : 0;
      if (op === '!=') return l !== r ? 1 : 0;
    }
    return l;
  }
  function additive(): number {
    let v = multiplicative();
    while (peek() === '+' || peek() === '-') {
      const op = next();
      v = op === '+' ? v + multiplicative() : v - multiplicative();
    }
    return v;
  }
  function multiplicative(): number {
    let v = unary();
    while (peek() === '*' || peek() === '/') {
      const op = next();
      v = op === '*' ? v * unary() : v / unary();
    }
    return v;
  }
  function unary(): number {
    if (peek() === '-') { next(); return -unary(); }
    if (peek() === '+') { next(); return unary(); }
    return primary();
  }
  function primary(): number {
    const t = next();
    if (t === undefined) throw new Error('unexpected end');
    if (t === '(') { const v = ternary(); expect(')'); return v; }
    if (t === 'Infinity') return Infinity;
    if (/^\d|^\./.test(t)) return parseFloat(t);
    if (t.startsWith('Math.')) {
      const fn = MATH_FNS[t.slice(5)];
      if (!fn) throw new Error(`function not allowed: ${t}`);
      expect('(');
      const args: number[] = [];
      if (peek() !== ')') {
        args.push(ternary());
        while (peek() === ',') { next(); args.push(ternary()); }
      }
      expect(')');
      return fn(...args);
    }
    return Object.prototype.hasOwnProperty.call(scope, t) ? scope[t] : NaN;
  }

  const result = ternary();
  if (p !== toks.length) throw new Error('trailing tokens');
  return result;
}

export function evalExpr(expr: string, values: Record<string, number>): number {
  if (expr.length > 500) return NaN;
  try {
    const result = parseEval(lex(expr), values);
    return typeof result === 'number' && !isNaN(result) && result !== -Infinity ? result : NaN;
  } catch {
    return NaN;
  }
}

export function fmtValue(n: number, format: DialFormat, signed = false): string {
  if (n === Infinity) return 'never';
  if (!isFinite(n) || isNaN(n)) return '—';
  switch (format) {
    case 'dollar':  return signed ? fmtCompactSigned(n) : fmtCompact(n);
    case 'percent': return signed ? fmtPctSigned(n) : fmtPct(n);
    case 'months':  return fmtMonths(n);
    case 'years':   return `${Math.ceil(n)} yr`;
    case 'integer': return String(Math.round(n));
  }
}

export function fmtDialValue(n: number, format: DialFormat): string {
  if (!isFinite(n) || isNaN(n)) return '—';
  switch (format) {
    case 'dollar':  return fmt(n);
    case 'percent': return fmtPct(n);
    case 'months':  return fmtMonths(n);
    case 'years':   return `${n} yr`;
    case 'integer': return String(Math.round(n));
  }
}

// ─── System prompt ────────────────────────────────────────────────────────────

function buildSystemPrompt(financialContext: string): string {
  return `You are a financial canvas generator embedded in fungible, a personal finance TUI app.

Given a user's financial question and their live account data, you generate an interactive canvas — a set of adjustable inputs (dials) and computed outputs that render in the terminal.

## Live financial data

${financialContext}

## How the canvas renders

Each element is rendered top-to-bottom:
- \`section\`: a bold section header (e.g. "INPUTS", "RESULTS")
- \`text\`: a dim explanatory line — use for assumptions, caveats, data sources
- \`dial\`: an interactive row the user adjusts with ← → arrow keys. Shows: label [ value ] hint
- \`output\`: a computed result row. Shows: label   value   (hint)

Dial values update live — outputs re-evaluate instantly as dials change.

## Output expressions

Each output has an \`expr\` field: a single-line JavaScript arithmetic expression.
- Variables are the \`key\` fields of the dials
- Only use: + - * / Math.pow Math.log Math.abs Math.round Math.floor Math.ceil
- No variables other than dial keys, no function calls besides the Math methods above
- Expressions must be self-contained — they cannot reference other output values

Example: monthly mortgage payment with principal P, monthly rate r, n payments:
  expr: "P * r / (1 - Math.pow(1 + r, -n))"

## Design rules

1. Pre-fill dial defaults from the provided live data where relevant — show the user their real numbers
2. Add a \`text\` element when you use live data: "based on your $X monthly income"
3. Keep labels short (≤ 18 chars) — they're padded in a fixed-width column
4. Keep it focused — 3–6 dials and 1–4 outputs is ideal
5. Use \`section\` to group: "INPUTS" before dials, "RESULTS" before outputs
6. Choose \`color\` for outputs: "positive" for gains/savings, "negative" for costs/debt, "neutral" otherwise
7. Format dials and outputs consistently — if a dial is "dollar", its related output should be too
8. Set \`signed: true\` on outputs that represent deltas or values that can be negative — e.g. net savings, surplus/deficit, change in portfolio. This shows an explicit +/- prefix so the sign is always unambiguous
9. For any projection with a multi-year time horizon (retirement, investment growth, net worth, savings goals), show values in **real (inflation-adjusted) dollars** as the primary output — not nominal. Add an \`inflation\` dial (key: "inflation", default: 3, step: 0.5, min: 0, max: 8, format: "percent", hint: "annual inflation"). Convert nominal to real with: \`nominal / Math.pow(1 + inflation/100, years)\`. Label the output "Real value (today's $)" or similar. A nominal output may appear secondary.
10. If the live data includes household ages, use them to pre-fill age-related dials and personalize narrative (e.g. "years until retirement" = retirement_age - current_age). If age is not provided, add an \`age\` dial (default: 35, step: 1, min: 18, max: 80, format: "integer", hint: "your current age") so the user can set it accurately.

## Existing screen conventions (for consistency)

The app uses these formatters:
- dollar: fmt($1234.56) → "$1,234.56", fmtCompact($1.84M) → "$1.84M"
- percent: fmtPct(7.0) → "7.0%"
- months: fmtMonths(63.4) → "63.4 mo"
- years: Math.ceil(n) + " yr"

Colors: positive=green (gains/savings), negative=red (costs/debt/loss), accent=blue (neutral emphasis)

Example canvas for "how long to pay off my credit card":
{
  "title": "Credit Card Payoff",
  "elements": [
    { "type": "text", "content": "based on your $21,494 in credit card debt" },
    { "type": "section", "label": "INPUTS" },
    { "type": "dial", "dial": { "key": "balance", "label": "Balance", "default": 21494, "step": 500, "min": 0, "format": "dollar", "hint": "current balance" }},
    { "type": "dial", "dial": { "key": "rate", "label": "APR", "default": 22, "step": 0.5, "min": 0, "max": 40, "format": "percent", "hint": "annual rate" }},
    { "type": "dial", "dial": { "key": "monthly", "label": "Monthly payment", "default": 500, "step": 50, "min": 0, "format": "dollar", "hint": "what you pay each month" }},
    { "type": "section", "label": "RESULTS" },
    { "type": "output", "output": { "label": "Months to payoff", "expr": "monthly <= balance * rate/100/12 ? Infinity : -Math.log(1 - balance * rate/100/12 / monthly) / Math.log(1 + rate/100/12)", "format": "months", "color": "neutral" }},
    { "type": "output", "output": { "label": "Total interest", "expr": "monthly * (-Math.log(1 - balance * rate/100/12 / monthly) / Math.log(1 + rate/100/12)) - balance", "format": "dollar", "color": "negative" }}
  ]
}`;
}

// ─── Context loader ───────────────────────────────────────────────────────────

export type CanvasContext = {
  system: string;
  tool: typeof CANVAS_TOOL;
};

export async function loadCanvasContext(): Promise<CanvasContext> {
  const health = await loadHealthData();
  const taxableBrokerage = health.liquid - health.cash;
  const profile = await loadProfile();
  const currentYear = new Date().getFullYear();

  const householdLines: string[] = [];
  if (profile?.self.birthYear) {
    const age = currentYear - profile.self.birthYear;
    const name = profile.self.name || 'You';
    let line = `Household: ${name}, age ${age}`;
    if (profile.spouse?.birthYear) {
      const spouseAge = currentYear - profile.spouse.birthYear;
      line += `; ${profile.spouse.name || 'Spouse'}, age ${spouseAge}`;
    }
    householdLines.push(line);
    if (profile.children.length > 0) {
      const childDesc = profile.children
        .map((c) => c.birthYear > 0 ? `${c.name || 'Child'} (age ${currentYear - c.birthYear})` : (c.name || 'Child'))
        .join(', ');
      householdLines.push(`Children: ${childDesc}`);
    }
  }

  const financialContext = [
    ...householdLines,
    `Monthly income:    ${fmt(health.monthlyIncome)} (12-month avg)`,
    `Monthly expenses:  ${fmt(health.avgMonthlyExpenses)} (12-month avg)`,
    `Monthly surplus:   ${fmt(health.monthlySavings)} (savings rate: ${Math.round(health.savingsRate)}%)`,
    `Cash (checking/savings): ${fmtCompact(health.cash)}`,
    `Taxable investments (brokerage): ${fmtCompact(taxableBrokerage)}  ← no withdrawal restrictions`,
    `Total accessible (cash + brokerage): ${fmtCompact(health.liquid)}`,
    `Retirement accounts (401k/IRA/Roth): ${fmtCompact(health.retirement)}  ← restricted until ~59½`,
    `Credit card debt:  ${fmtCompact(health.totalDebt)}`,
    health.loanDebt > 0 ? `Loan debt (mortgage/auto/student): ${fmtCompact(health.loanDebt)}` : null,
    `Net worth:         ${fmtCompact(health.netWorth)}`,
  ].filter(Boolean).join('\n');
  return { system: buildSystemPrompt(financialContext), tool: CANVAS_TOOL };
}

// ─── Canvas generation ────────────────────────────────────────────────────────

const CANVAS_TOOL = {
  name: 'render_canvas',
  description: 'Render an interactive financial canvas with adjustable dials and computed outputs.',
  parameters: {
    type: 'object',
    required: ['title', 'elements'],
    properties: {
      title: { type: 'string' },
      elements: {
        type: 'array',
        items: {
          type: 'object',
          required: ['type'],
          properties: {
            type: { type: 'string', enum: ['section', 'text', 'dial', 'output'] },
            label:   { type: 'string' },
            content: { type: 'string' },
            dial: {
              type: 'object',
              required: ['key', 'label', 'default', 'step', 'format', 'hint'],
              properties: {
                key:     { type: 'string' },
                label:   { type: 'string' },
                default: { type: 'number' },
                step:    { type: 'number' },
                min:     { type: 'number' },
                max:     { type: 'number' },
                format:  { type: 'string', enum: ['dollar', 'percent', 'integer', 'months', 'years'] },
                hint:    { type: 'string' },
              },
            },
            output: {
              type: 'object',
              required: ['label', 'expr', 'format'],
              properties: {
                label:  { type: 'string' },
                expr:   { type: 'string' },
                format: { type: 'string', enum: ['dollar', 'percent', 'integer', 'months', 'years'] },
                color:  { type: 'string', enum: ['positive', 'negative', 'neutral', 'accent'] },
                signed: { type: 'boolean' },
              },
            },
          },
        },
      },
    },
  },
};

export async function generateCanvas(
  prompt: string,
  onStatus: (msg: string) => void,
): Promise<CanvasSpec> {
  const { system, tool } = await loadCanvasContext();

  onStatus('generating…');

  let spec: CanvasSpec | null = null;

  for await (const chunk of streamResponse(system, [{ role: 'user', content: prompt }], [tool])) {
    if (chunk.type === 'tool_use' && chunk.name === 'render_canvas') {
      spec = chunk.input as unknown as CanvasSpec;
    }
  }

  if (!spec) throw new Error('Canvas generation failed — no spec returned.');
  return spec;
}
