/**
 * Limits Plugin - Rate limit check, cost calculation, and utility helpers.
 *
 * Extracted from limits-plugin.ts to keep each file under 300 lines.
 * @internal
 */

import {
  PluginError,
  estimateBlendedCostPer1000,
  CONTEXT_ESTIMATE_CHARS_PER_TOKEN,
} from '@robota-sdk/agent-core';

import type { ILimitWindow, ITokenBucket } from './types';
import type { TUniversalMessage } from '@robota-sdk/agent-core';

const COST_DECIMAL_PLACES = 4;
const TOKEN_ESTIMATE_BUFFER = 100;
const TOKENS_PER_COST_UNIT = 1000;
const MS_PER_SECOND = 1000;

/**
 * Default cost calculator: uses the agent-core pricing SSOT for a known model's blended per-1000
 * rate, falling back to the caller-supplied `tokenCostPer1000` when the model is unknown. @internal
 */
export function defaultCostCalculator(
  tokens: number,
  model: string,
  tokenCostPer1000: number,
): number {
  const rate = estimateBlendedCostPer1000(model) ?? tokenCostPer1000;
  return (tokens / TOKENS_PER_COST_UNIT) * rate;
}

/** Estimate token count from message content lengths. @internal */
export function estimateTokensFromMessages(messages: TUniversalMessage[]): number {
  return (
    Math.ceil(
      messages.reduce((t, m) => t + (m.content?.length || 0), 0) / CONTEXT_ESTIMATE_CHARS_PER_TOKEN,
    ) + TOKEN_ESTIMATE_BUFFER
  );
}

/** Check and update token-bucket state. Throws PluginError if any limit is exceeded. @internal */
export function checkTokenBucket(
  bucket: ITokenBucket,
  now: number,
  estimatedTokens: number,
  estimatedCost: number,
  options: {
    bucketSize: number;
    refillRate: number;
    timeWindow: number;
    maxRequests: number;
    maxCost: number;
  },
  pluginName: string,
): void {
  const timePassed = (now - bucket.lastRefill) / MS_PER_SECOND;
  bucket.tokens = Math.min(options.bucketSize, bucket.tokens + timePassed * options.refillRate);
  bucket.lastRefill = now;

  if (bucket.tokens < estimatedTokens) {
    throw new PluginError(
      `Token bucket depleted. Available: ${Math.floor(bucket.tokens)}, Required: ${estimatedTokens}`,
      pluginName,
      { availableTokens: bucket.tokens, requiredTokens: estimatedTokens },
    );
  }

  if (now - bucket.windowStart >= options.timeWindow) {
    bucket.requests = 0;
    bucket.cost = 0;
    bucket.windowStart = now;
  }

  if (bucket.requests >= options.maxRequests) {
    throw new PluginError(`Request limit exceeded. Max: ${options.maxRequests}`, pluginName, {
      currentRequests: bucket.requests,
      maxRequests: options.maxRequests,
    });
  }

  if (bucket.cost + estimatedCost > options.maxCost) {
    throw new PluginError(
      `Cost limit exceeded. Current: $${bucket.cost.toFixed(COST_DECIMAL_PLACES)}, Estimated: $${estimatedCost.toFixed(COST_DECIMAL_PLACES)}, Max: $${options.maxCost}`,
      pluginName,
      { currentCost: bucket.cost, estimatedCost, maxCost: options.maxCost },
    );
  }

  bucket.tokens -= estimatedTokens;
  bucket.requests++;
}

/** Check and update sliding-window state. Throws PluginError if any limit is exceeded. @internal */
export function checkSlidingWindow(
  window: ILimitWindow,
  now: number,
  estimatedTokens: number,
  estimatedCost: number,
  options: {
    timeWindow: number;
    maxTokens: number;
    maxRequests: number;
    maxCost: number;
  },
  pluginName: string,
): void {
  if (now - window.windowStart < options.timeWindow) {
    if (window.tokens + estimatedTokens > options.maxTokens) {
      throw new PluginError(
        `Token limit exceeded in sliding window. Current: ${window.tokens}, Estimated: ${estimatedTokens}, Max: ${options.maxTokens}`,
        pluginName,
        { currentTokens: window.tokens, estimatedTokens, maxTokens: options.maxTokens },
      );
    }
    if (window.count >= options.maxRequests) {
      throw new PluginError(
        `Request limit exceeded in sliding window. Current: ${window.count}, Max: ${options.maxRequests}`,
        pluginName,
        { currentRequests: window.count, maxRequests: options.maxRequests },
      );
    }
    if (window.cost + estimatedCost > options.maxCost) {
      throw new PluginError(
        `Cost limit exceeded in sliding window. Current: $${window.cost.toFixed(COST_DECIMAL_PLACES)}, Estimated: $${estimatedCost.toFixed(COST_DECIMAL_PLACES)}, Max: $${options.maxCost}`,
        pluginName,
        { currentCost: window.cost, estimatedCost, maxCost: options.maxCost },
      );
    }
  } else {
    window.count = 0;
    window.tokens = 0;
    window.cost = 0;
    window.windowStart = now;
  }
  window.count++;
}

/** Check and update fixed-window state. Throws PluginError if any limit is exceeded. @internal */
export function checkFixedWindow(
  window: ILimitWindow,
  now: number,
  estimatedTokens: number,
  estimatedCost: number,
  options: {
    timeWindow: number;
    maxTokens: number;
    maxRequests: number;
    maxCost: number;
  },
  pluginName: string,
): void {
  if (now - window.windowStart >= options.timeWindow) {
    window.count = 0;
    window.tokens = 0;
    window.cost = 0;
    window.windowStart = now;
  }

  if (window.tokens + estimatedTokens > options.maxTokens) {
    throw new PluginError(
      `Token limit exceeded in fixed window. Current: ${window.tokens}, Estimated: ${estimatedTokens}, Max: ${options.maxTokens}`,
      pluginName,
      { currentTokens: window.tokens, estimatedTokens, maxTokens: options.maxTokens },
    );
  }
  if (window.count >= options.maxRequests) {
    throw new PluginError(
      `Request limit exceeded in fixed window. Current: ${window.count}, Max: ${options.maxRequests}`,
      pluginName,
      { currentRequests: window.count, maxRequests: options.maxRequests },
    );
  }
  if (window.cost + estimatedCost > options.maxCost) {
    throw new PluginError(
      `Cost limit exceeded in fixed window. Current: $${window.cost.toFixed(COST_DECIMAL_PLACES)}, Estimated: $${estimatedCost.toFixed(COST_DECIMAL_PLACES)}, Max: $${options.maxCost}`,
      pluginName,
      { currentCost: window.cost, estimatedCost, maxCost: options.maxCost },
    );
  }
  window.count++;
}
