// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * Cost Tracker — Per-session spending limits for auto agents.
 *
 * Tracks token usage, estimates cost per interaction using model-specific
 * pricing, and enforces a configurable budget limit. Integrates with
 * SessionLogger for persistent cost records.
 *
 * @module session/cost-tracker
 */

// ---------------------------------------------------------------------------
// Model pricing (per 1M tokens)
// ---------------------------------------------------------------------------

/**
 * Token pricing by model family.
 * @type {Record<string, { input: number, output: number }>}
 */
export const MODEL_PRICING = {
  // Claude models
  'claude-sonnet': { input: 3.0, output: 15.0 },
  'claude-haiku': { input: 0.25, output: 1.25 },
  'claude-opus': { input: 15.0, output: 75.0 },
  // OpenAI models
  'gpt-4o': { input: 2.5, output: 10.0 },
  'gpt-4': { input: 5.0, output: 15.0 },
  'gpt-3.5': { input: 0.5, output: 1.5 },
  // Local models (free)
  'ollama': { input: 0, output: 0 },
  'local': { input: 0, output: 0 },
  // Default fallback
  'default': { input: 3.0, output: 15.0 },
};

// ---------------------------------------------------------------------------
// PriceLimitExceeded
// ---------------------------------------------------------------------------

/**
 * Thrown when session cost exceeds the configured budget.
 */
export class PriceLimitExceeded extends Error {
  /**
   * @param {number} currentCost
   * @param {number} limit
   * @param {string} sessionId
   */
  constructor(currentCost, limit, sessionId = '') {
    super(
      `Session cost $${currentCost.toFixed(4)} exceeds budget limit $${limit.toFixed(2)}` +
      (sessionId ? ` (session: ${sessionId})` : '')
    );
    this.name = 'PriceLimitExceeded';
    this.currentCost = currentCost;
    this.limit = limit;
    this.sessionId = sessionId;
  }
}

// ---------------------------------------------------------------------------
// CostTracker
// ---------------------------------------------------------------------------

/**
 * Per-session cost tracker with budget enforcement.
 */
export class CostTracker {
  /**
   * @param {object} [opts]
   * @param {number} [opts.budgetUSD] - Maximum spend (default from env or $5)
   * @param {string} [opts.model] - Model identifier for pricing lookup
   * @param {string} [opts.sessionId] - Associated session ID
   * @param {import('./logger.js').SessionLogger} [opts.logger] - Optional session logger
   */
  constructor(opts = {}) {
    this._budgetUSD = opts.budgetUSD ?? parseFloat(process.env.NERF_PRICE_LIMIT || '5');
    this._model = opts.model || 'default';
    this._sessionId = opts.sessionId || '';
    this._logger = opts.logger || null;
    this._totalCostUSD = 0;
    this._totalTokensIn = 0;
    this._totalTokensOut = 0;
    this._interactionCount = 0;
  }

  /** Current total cost. */
  get totalCost() { return this._totalCostUSD; }

  /** Configured budget. */
  get budget() { return this._budgetUSD; }

  /** Remaining budget. */
  get remaining() { return Math.max(0, this._budgetUSD - this._totalCostUSD); }

  /** Total input tokens tracked. */
  get totalTokensIn() { return this._totalTokensIn; }

  /** Total output tokens tracked. */
  get totalTokensOut() { return this._totalTokensOut; }

  /** Number of interactions tracked. */
  get interactionCount() { return this._interactionCount; }

  /**
   * Look up pricing for the configured model.
   * @returns {{ input: number, output: number }}
   */
  getPricing() {
    const modelLower = this._model.toLowerCase();
    for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
      if (modelLower.includes(key)) return pricing;
    }
    return MODEL_PRICING.default;
  }

  /**
   * Calculate cost for a given number of tokens.
   *
   * @param {number} tokensIn
   * @param {number} tokensOut
   * @returns {number} Cost in USD
   */
  calculateCost(tokensIn, tokensOut) {
    const pricing = this.getPricing();
    return (tokensIn / 1_000_000) * pricing.input + (tokensOut / 1_000_000) * pricing.output;
  }

  /**
   * Track an interaction's token usage and check budget.
   *
   * @param {number} tokensIn
   * @param {number} tokensOut
   * @returns {{ costUSD: number, cumulativeCostUSD: number, remaining: number, withinBudget: boolean }}
   * @throws {PriceLimitExceeded} If cumulative cost exceeds budget
   */
  track(tokensIn, tokensOut) {
    const costUSD = this.calculateCost(tokensIn, tokensOut);
    this._totalCostUSD += costUSD;
    this._totalTokensIn += tokensIn;
    this._totalTokensOut += tokensOut;
    this._interactionCount += 1;

    const withinBudget = this._totalCostUSD <= this._budgetUSD;

    // Log to session if available
    if (this._logger) {
      const status = withinBudget ? 'ok' : 'exceeded';
      this._logger.logCost(costUSD, this._budgetUSD, status);
    }

    if (!withinBudget) {
      throw new PriceLimitExceeded(this._totalCostUSD, this._budgetUSD, this._sessionId);
    }

    return {
      costUSD,
      cumulativeCostUSD: this._totalCostUSD,
      remaining: this.remaining,
      withinBudget,
    };
  }

  /**
   * Check if the budget would be exceeded by additional tokens.
   * Does NOT track — just estimates.
   *
   * @param {number} tokensIn
   * @param {number} tokensOut
   * @returns {{ wouldExceed: boolean, estimatedCost: number, projectedTotal: number }}
   */
  wouldExceed(tokensIn, tokensOut) {
    const estimatedCost = this.calculateCost(tokensIn, tokensOut);
    const projectedTotal = this._totalCostUSD + estimatedCost;
    return {
      wouldExceed: projectedTotal > this._budgetUSD,
      estimatedCost,
      projectedTotal,
    };
  }

  /**
   * Return a status summary.
   * @returns {object}
   */
  getStatus() {
    return {
      totalCostUSD: Math.round(this._totalCostUSD * 10000) / 10000,
      budgetUSD: this._budgetUSD,
      remainingUSD: Math.round(this.remaining * 10000) / 10000,
      totalTokensIn: this._totalTokensIn,
      totalTokensOut: this._totalTokensOut,
      interactionCount: this._interactionCount,
      model: this._model,
      utilizationPct: this._budgetUSD > 0
        ? Math.round((this._totalCostUSD / this._budgetUSD) * 10000) / 100
        : 0,
    };
  }
}
