<?php
/**
 * Model Pricing Class
 *
 * Calculates estimated cost of API calls based on hardcoded model prices (USD per 1M tokens)
 * and dynamic OpenRouter pricing from the cached model catalog.
 *
 * @package SmartyPress_AI_Engine
 * @since 1.0.20
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Model pricing calculator.
 */
class SmartyPress_AI_Model_Pricing {

	/**
	 * Hardcoded prices per 1 million tokens in USD.
	 *
	 * Structure: model_id => [ 'in' => float, 'cached_in' => float, 'out' => float ]
	 *   or for image models: model_id => [ 'per_image' => float ]
	 *
	 * Prices sourced from official provider pricing pages (as of May 2026).
	 */
	const PRICING = array(
		// OpenAI — text models
		'gpt-5.5'           => array( 'in' => 5.00,  'cached_in' => 0.50,  'out' => 30.00 ),
		'gpt-5.4'           => array( 'in' => 2.50,  'cached_in' => 0.25,  'out' => 15.00 ),
		'gpt-5.4-mini'      => array( 'in' => 0.75,  'cached_in' => 0.075, 'out' => 4.50 ),
		'gpt-5.4-nano'      => array( 'in' => 0.20,  'cached_in' => 0.02,  'out' => 1.25 ),
		'gpt-5.2'           => array( 'in' => 1.75,  'cached_in' => 0.175, 'out' => 14.00 ),
		'gpt-5.1'           => array( 'in' => 1.25,  'cached_in' => 0.125, 'out' => 10.00 ),
		'gpt-5'             => array( 'in' => 1.25,  'cached_in' => 0.125, 'out' => 10.00 ),
		'gpt-5-mini'        => array( 'in' => 0.25,  'cached_in' => 0.025, 'out' => 2.00 ),
		'gpt-5-nano'        => array( 'in' => 0.05,  'cached_in' => 0.005, 'out' => 0.40 ),
		'gpt-4.1'           => array( 'in' => 2.00,  'cached_in' => 0.50,  'out' => 8.00 ),
		'gpt-4.1-mini'      => array( 'in' => 0.40,  'cached_in' => 0.10,  'out' => 1.60 ),
		'gpt-4.1-nano'      => array( 'in' => 0.10,  'cached_in' => 0.025, 'out' => 0.40 ),
		'gpt-4o'            => array( 'in' => 2.50,  'cached_in' => 1.25,  'out' => 10.00 ),
		'gpt-4o-mini'       => array( 'in' => 0.15,  'cached_in' => 0.075, 'out' => 0.60 ),
		'o4-mini'           => array( 'in' => 1.10,  'cached_in' => 0.275, 'out' => 4.40 ),
		'o3'                => array( 'in' => 2.00,  'cached_in' => 0.50,  'out' => 8.00 ),
		'o3-mini'           => array( 'in' => 1.10,  'cached_in' => 0.55,  'out' => 4.40 ),
		'o1'                => array( 'in' => 15.00, 'cached_in' => 7.50,  'out' => 60.00 ),
		'o1-mini'           => array( 'in' => 1.10,  'cached_in' => 0.55,  'out' => 4.40 ),

		// OpenAI — image models (token-based)
		'gpt-image-1'       => array( 'in' => 5.00,  'out' => 40.00 ),
		'gpt-image-1.5'     => array( 'in' => 5.00,  'out' => 40.00 ),
		'gpt-image-2'       => array( 'in' => 5.00,  'cached_in' => 1.25,  'out' => 30.00 ),

		// OpenAI — DALL-E (per-image pricing)
		'dall-e-3'          => array( 'per_image' => 0.040 ),
		'dall-e-3-hd'       => array( 'per_image' => 0.080 ),

		// DeepSeek — regular public pricing without temporary promotional discounts.
		'deepseek-v4-flash' => array( 'in' => 0.14,  'cached_in' => 0.0028,   'out' => 0.28 ),
		'deepseek-v4-pro'   => array( 'in' => 1.74,  'cached_in' => 0.0145,   'out' => 3.48 ),

		// Anthropic Claude — text and vision models.
		'claude-opus-4-7'   => array( 'in' => 5.00,  'cached_in' => 0.50,     'out' => 25.00 ),
		'claude-opus-4-6'   => array( 'in' => 5.00,  'cached_in' => 0.50,     'out' => 25.00 ),
		'claude-opus-4-5'   => array( 'in' => 5.00,  'cached_in' => 0.50,     'out' => 25.00 ),
		'claude-opus-4-1'   => array( 'in' => 15.00, 'cached_in' => 1.50,     'out' => 75.00 ),
		'claude-opus-4-0'   => array( 'in' => 15.00, 'cached_in' => 1.50,     'out' => 75.00 ),
		'claude-sonnet-4-6' => array( 'in' => 3.00,  'cached_in' => 0.30,     'out' => 15.00 ),
		'claude-sonnet-4-5' => array( 'in' => 3.00,  'cached_in' => 0.30,     'out' => 15.00 ),
		'claude-sonnet-4-0' => array( 'in' => 3.00,  'cached_in' => 0.30,     'out' => 15.00 ),
		'claude-haiku-4-5'  => array( 'in' => 1.00,  'cached_in' => 0.10,     'out' => 5.00 ),
		'claude-3-haiku-20240307' => array( 'in' => 0.25, 'cached_in' => 0.03, 'out' => 1.25 ),

		// GigaChat — approximate package prices converted from RUB to USD at 90 RUB/USD.
		'GigaChat-2'        => array( 'in' => 0.72,  'out' => 0.72 ),
		'GigaChat-2-Pro'    => array( 'in' => 5.56,  'out' => 5.56 ),
		'GigaChat-2-Max'    => array( 'in' => 7.22,  'out' => 7.22 ),
		'GigaChat-Pro'      => array( 'in' => 5.56,  'out' => 5.56 ),
		'GigaChat-Max'      => array( 'in' => 7.22,  'out' => 7.22 ),

		// YandexGPT (approximate RUB-based pricing converted to USD)
		'yandexgpt'         => array( 'in' => 0.40,  'out' => 1.20 ),
		'yandexgpt-lite'    => array( 'in' => 0.20,  'out' => 0.60 ),
		'yandexgpt-32k'     => array( 'in' => 0.80,  'out' => 2.40 ),
	);

	/**
	 * Calculate the estimated cost of an API call.
	 *
	 * Priority:
	 *   1. API-returned cost (usage.cost / usage.total_cost)
	 *   2. OpenRouter dynamic pricing from cached model catalog
	 *   3. Hardcoded PRICING table (prefix matching)
	 *   4. Unknown (0.0)
	 *
	 * @param string $provider     Provider identifier.
	 * @param string $model        Model identifier.
	 * @param int    $input_tokens Input token count.
	 * @param int    $output_tokens Output token count.
	 * @param array  $usage        Raw usage array (may contain 'cost' or 'total_cost').
	 * @return array{cost: float, source: string}
	 */
	public static function calculate_cost(
		string $provider,
		string $model,
		int $input_tokens,
		int $output_tokens,
		array $usage = array()
	): array {
		// 1. API-returned cost takes top priority.
		$api_cost = $usage['total_cost'] ?? $usage['cost'] ?? null;
		if ( null !== $api_cost && is_numeric( $api_cost ) && (float) $api_cost > 0 ) {
			return array(
				'cost'   => (float) $api_cost,
				'source' => 'api',
			);
		}

		// 2. OpenRouter dynamic pricing from cached model catalog.
		if ( 'openrouter' === $provider ) {
			$or_cost = self::get_openrouter_cost( $model, $input_tokens, $output_tokens );
			if ( null !== $or_cost ) {
				return array(
					'cost'   => $or_cost,
					'source' => 'openrouter_catalog',
				);
			}
		}

		// 3. Hardcoded pricing table with prefix matching.
		$price_entry = self::find_hardcoded_price( $model );
		if ( null !== $price_entry ) {
			if ( isset( $price_entry['per_image'] ) ) {
				return array(
					'cost'   => (float) $price_entry['per_image'],
					'source' => 'hardcoded',
				);
			}

			$cached_input_tokens   = self::get_cached_input_tokens( $usage );
			$uncached_input_tokens = max( 0, $input_tokens - $cached_input_tokens );
			$input_cost            = ( $input_tokens / 1_000_000 ) * (float) ( $price_entry['in'] ?? 0 );

			if ( $cached_input_tokens > 0 && isset( $price_entry['cached_in'] ) ) {
				$input_cost = ( $uncached_input_tokens / 1_000_000 ) * (float) ( $price_entry['in'] ?? 0 )
					+ ( $cached_input_tokens / 1_000_000 ) * (float) $price_entry['cached_in'];
			}

			$cost = $input_cost + ( $output_tokens / 1_000_000 ) * (float) ( $price_entry['out'] ?? 0 );

			return array(
				'cost'   => $cost,
				'source' => 'hardcoded',
			);
		}

		// 4. Unknown model — record tokens but no cost estimate.
		return array(
			'cost'   => 0.0,
			'source' => 'unknown',
		);
	}

	/**
	 * Try to calculate cost from the OpenRouter cached model catalog.
	 *
	 * OpenRouter pricing uses per-token rates (not per 1M):
	 *   pricing.prompt     = USD per input token
	 *   pricing.completion  = USD per output token
	 *
	 * @param string $model        Full OpenRouter model ID (e.g. 'openai/gpt-4o-mini').
	 * @param int    $input_tokens Input token count.
	 * @param int    $output_tokens Output token count.
	 * @return float|null Cost in USD, or null if model not found.
	 */
	private static function get_openrouter_cost( string $model, int $input_tokens, int $output_tokens ): ?float {
		if ( ! class_exists( 'SmartyPress_AI_Model_Map' ) ) {
			return null;
		}

		$model_data = SmartyPress_AI_Model_Map::get_model( 'openrouter', $model );

		if ( empty( $model_data['pricing'] ) || ! is_array( $model_data['pricing'] ) ) {
			return null;
		}

		$prompt_rate     = isset( $model_data['pricing']['prompt'] ) ? (float) $model_data['pricing']['prompt'] : 0;
		$completion_rate = isset( $model_data['pricing']['completion'] ) ? (float) $model_data['pricing']['completion'] : 0;

		if ( $prompt_rate <= 0 && $completion_rate <= 0 ) {
			return null;
		}

		return $prompt_rate * $input_tokens + $completion_rate * $output_tokens;
	}

	/**
	 * Extract cached input tokens from common provider usage formats.
	 *
	 * OpenAI returns cached prompt tokens in prompt_tokens_details.cached_tokens.
	 * DeepSeek-compatible providers may expose prompt_cache_hit_tokens directly.
	 *
	 * @param array $usage Raw usage array.
	 * @return int
	 */
	private static function get_cached_input_tokens( array $usage ): int {
		if ( isset( $usage['prompt_tokens_details']['cached_tokens'] ) ) {
			return max( 0, (int) $usage['prompt_tokens_details']['cached_tokens'] );
		}

		if ( isset( $usage['prompt_cache_hit_tokens'] ) ) {
			return max( 0, (int) $usage['prompt_cache_hit_tokens'] );
		}

		if ( isset( $usage['cache_hit_tokens'] ) ) {
			return max( 0, (int) $usage['cache_hit_tokens'] );
		}

		return 0;
	}

	/**
	 * Find a hardcoded price entry by exact or prefix match.
	 *
	 * Tries exact match first, then strips provider prefixes (e.g. 'openai/gpt-4o' -> 'gpt-4o'),
	 * then tries prefix matching for versioned models (e.g. 'gpt-4.1-mini-2025-04-14' -> 'gpt-4.1-mini').
	 *
	 * @param string $model Model identifier.
	 * @return array|null Price entry or null.
	 */
	private static function find_hardcoded_price( string $model ): ?array {
		if ( isset( self::PRICING[ $model ] ) ) {
			return self::PRICING[ $model ];
		}

		// Strip provider prefix (openai/gpt-4o -> gpt-4o).
		if ( strpos( $model, '/' ) !== false ) {
			$short = substr( $model, strrpos( $model, '/' ) + 1 );
			if ( isset( self::PRICING[ $short ] ) ) {
				return self::PRICING[ $short ];
			}
			$model = $short;
		}

		// Prefix match: longest match wins (e.g. 'gpt-4.1-mini-2025' matches 'gpt-4.1-mini').
		$best_key    = null;
		$best_length = 0;
		foreach ( self::PRICING as $key => $entry ) {
			if ( strpos( $model, $key ) === 0 && strlen( $key ) > $best_length ) {
				$best_key    = $key;
				$best_length = strlen( $key );
			}
		}

		return $best_key !== null ? self::PRICING[ $best_key ] : null;
	}
}
