<?php
/**
 * Costudio_Usage — Token usage and cost tracking.
 *
 * Records input/output tokens and estimated cost for each AI API call.
 * Data is stored in the wp_costudio_usage table.
 *
 * @package Contentosapp_Studio
 * @since   3.1.0
 */

defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Queries on plugin-owned wp_costudio_usage table; $table = $wpdb->prefix . constant.

class Costudio_Usage {

	const TABLE = 'costudio_usage';

	/**
	 * Approximate cost per 1M tokens (USD) by provider and direction.
	 * Updated 2026-04. Override via 'costudio_token_costs' filter.
	 */
	const DEFAULT_COSTS = [
		'gemini' => [ 'input' => 0.075, 'output' => 0.30 ],   // Gemini 2.5 Flash
		'claude' => [ 'input' => 3.00,  'output' => 15.00 ],   // Claude Sonnet 4
		'openai' => [ 'input' => 2.50,  'output' => 10.00 ],   // GPT-4o
	];

	/**
	 * Record a single API call's token usage.
	 *
	 * @param array $data {
	 *   @type int    $production_id  Production record ID.
	 *   @type int    $agent          Agent number (1-7, 0 for edits).
	 *   @type string $provider       Provider name (gemini, claude, openai).
	 *   @type string $model          Model identifier used.
	 *   @type int    $input_tokens   Input tokens consumed.
	 *   @type int    $output_tokens  Output tokens consumed.
	 *   @type float  $latency_ms     Request latency in milliseconds.
	 *   @type string $started_at     Stable UTC start of this agent attempt.
	 *   @type string $byok_job_id    Optional durable BYOK job UUID.
	 * }
	 */
	public static function record( array $data ): bool {
		global $wpdb;

		$provider      = $data['provider'] ?? '';
		$input_tokens  = (int) ( $data['input_tokens'] ?? 0 );
		$output_tokens = (int) ( $data['output_tokens'] ?? 0 );
		$cost_usd      = self::estimate_cost( $provider, $input_tokens, $output_tokens );

		$row = [
				'production_id' => (int) ( $data['production_id'] ?? 0 ),
				'agent'         => (int) ( $data['agent'] ?? 0 ),
				'provider'      => sanitize_text_field( $provider ),
				'model'         => sanitize_text_field( $data['model'] ?? '' ),
				'input_tokens'  => $input_tokens,
				'output_tokens' => $output_tokens,
				'cost_usd'      => $cost_usd,
				'grounding'     => ! empty( $data['grounding'] ) ? 1 : 0,
				'latency_ms'    => (int) ( $data['latency_ms'] ?? 0 ),
				'started_at'    => ! empty( $data['started_at'] ) ? (string) $data['started_at'] : null,
				'created_at'    => current_time( 'mysql', true ),
			];
		$formats = [ '%d', '%d', '%s', '%s', '%d', '%d', '%f', '%d', '%d', '%s', '%s' ];

		if ( array_key_exists( 'byok_job_id', $data ) ) {
			$byok_job_id = strtolower( trim( (string) $data['byok_job_id'] ) );
			if ( 1 !== preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $byok_job_id ) ) {
				return false;
			}
			$row['byok_job_id'] = $byok_job_id;
			$formats[]          = '%s';
		}

		$inserted = $wpdb->insert(
			$wpdb->prefix . self::TABLE,
			$row,
			$formats
		);

		return false !== $inserted;
	}

	/**
	 * Check whether a durable BYOK job already has its exactly-once usage row.
	 */
	public static function has_byok_job( string $job_id ): bool {
		global $wpdb;

		$job_id = strtolower( trim( $job_id ) );
		if ( 1 !== preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $job_id ) ) {
			return false;
		}

		return null !== $wpdb->get_var(
			$wpdb->prepare(
				'SELECT id FROM ' . $wpdb->prefix . self::TABLE . ' WHERE byok_job_id=%s LIMIT 1',
				$job_id
			)
		);
	}

	/**
	 * Get total usage for a specific production.
	 *
	 * @param  int $production_id
	 * @return object { total_input_tokens, total_output_tokens, total_cost_usd, total_latency_ms, call_count, agents_count }
	 */
	public static function get_production_totals( int $production_id ): object {
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;

		$row = $wpdb->get_row( $wpdb->prepare(
			"SELECT
				COALESCE(SUM(input_tokens), 0)  AS total_input_tokens,
				COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
				COALESCE(SUM(cost_usd), 0)      AS total_cost_usd,
				COALESCE(SUM(latency_ms), 0)    AS total_latency_ms,
				COUNT(*)                         AS call_count,
				COUNT(DISTINCT agent)            AS agents_count,
				MAX(model)                       AS model,
				COALESCE(SUM(grounding), 0)    AS grounding_count
			FROM {$table}
			WHERE production_id = %d",
			$production_id
		) );

		return $row ?: (object) [
			'total_input_tokens'  => 0,
			'total_output_tokens' => 0,
			'total_cost_usd'      => 0,
			'total_latency_ms'    => 0,
			'call_count'          => 0,
			'agents_count'        => 0,
		];
	}

	/**
	 * Get the most recent usage record for a specific agent of a production.
	 *
	 * @param  int $production_id Production record ID.
	 * @param  int $agent         Agent number (1-7, 0 for edits).
	 * @return array|null         Associative array with usage data, or null if not found.
	 */
	public static function get_agent_usage( int $production_id, int $agent ): ?array {
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;

		$row = $wpdb->get_row( $wpdb->prepare(
			"SELECT provider, model, input_tokens, output_tokens, cost_usd, latency_ms
			FROM {$table}
			WHERE production_id = %d AND agent = %d
			ORDER BY id DESC
			LIMIT 1",
			$production_id,
			$agent
		), ARRAY_A );

		return $row ?: null;
	}

	/**
	 * Get per-agent latency rows for UI timing displays.
	 *
	 * @param  int $production_id Production record ID.
	 * @return object[] Latest row per agent with latency and timing boundaries.
	 */
	public static function get_production_latency_rows( int $production_id ): array {
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;

		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT usage_row.agent, usage_row.latency_ms, usage_row.started_at, usage_row.created_at
				FROM {$table} usage_row
				INNER JOIN (
					SELECT agent, MAX(id) AS latest_id
					FROM {$table}
					WHERE production_id = %d
					GROUP BY agent
				) latest ON latest.latest_id = usage_row.id
				ORDER BY usage_row.agent ASC",
				$production_id
			)
		) ?: array();
	}

	/**
	 * Finalize the latest usage row when the complete agent output is persisted.
	 *
	 * Provider return is not always the end of an agent: Writer validation and
	 * other deterministic post-processing still run before update_output().
	 * Updating only the latest row keeps call history intact while making the UI
	 * duration match the actual runner transition boundary.
	 */
	public static function finalize_agent_timing( int $production_id, int $agent, string $started_at, int $elapsed_ms ): void {
		global $wpdb;

		if ( $production_id <= 0 || $agent <= 0 || '' === $started_at || $elapsed_ms <= 0 ) {
			return;
		}

		$table = $wpdb->prefix . self::TABLE;
		$id    = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id
				 FROM {$table}
				 WHERE production_id = %d
				   AND agent = %d
				 ORDER BY id DESC
				 LIMIT 1",
				$production_id,
				$agent
			)
		);

		if ( ! $id ) {
			return;
		}

		$wpdb->update(
			$table,
			array(
				'started_at' => $started_at,
				'latency_ms' => $elapsed_ms,
			),
			array( 'id' => (int) $id ),
			array( '%s', '%d' ),
			array( '%d' )
		);
	}

	/**
	 * Estimate cost in USD based on token counts.
	 *
	 * @param string $provider
	 * @param int    $input_tokens
	 * @param int    $output_tokens
	 * @return float
	 */
	public static function estimate_cost( string $provider, int $input_tokens, int $output_tokens ): float {
		$costs    = apply_filters( 'costudio_token_costs', self::DEFAULT_COSTS );
		$rates    = $costs[ $provider ] ?? [ 'input' => 1.0, 'output' => 3.0 ];
		$in_cost  = ( $input_tokens / 1_000_000 ) * $rates['input'];
		$out_cost = ( $output_tokens / 1_000_000 ) * $rates['output'];

		return round( $in_cost + $out_cost, 6 );
	}
}
