<?php
/**
 * Lightweight Usage Tracker
 *
 * Aggregates per-tool generation events and AI API token consumption into a
 * single wp_option per calendar month (autoload=no). Designed to be cheap:
 *  - 0 INSERT per event (only UPDATE on a single option row)
 *  - 1 read per dashboard load (WordPress option cache absorbs repeats)
 *  - Automatic monthly rotation via `aipt_usage_YYYY_MM` key naming
 *
 * Concurrency: uses an in-request memory buffer that flushes once on
 * shutdown to coalesce many increments into a single update_option() call.
 * Concurrent requests can race; metrics are best-effort by design and a
 * lost update on a high-traffic dashboard is acceptable.
 *
 * @package AIPT\Core\Services
 * @since   2.6.0
 */

namespace AIPT\Core\Services;

use AIPT\Api\Constants\ApiPricing;

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

class UsageTracker {

	private const OPTION_PREFIX = 'aipt_usage_';
	private const OPTION_RECENT = 'aipt_usage_recent';
	private const RECENT_BUFFER_MAX = 30;

	/**
	 * In-request buffer of pending deltas keyed by current month.
	 *
	 * @var array<string, array>
	 */
	private static $buffer = array();

	/**
	 * In-request ring buffer of new events to push on shutdown.
	 *
	 * @var array<int, array>
	 */
	private static $recent_buffer = array();

	/**
	 * In-request API usage accumulator. Each `trackApiCall()` adds to this
	 * snapshot; `record_event()` reads + resets it so every tool-event entry
	 * carries the API cost it incurred. Zero DB calls — pure in-memory.
	 *
	 * Cost is stored in USD micros (USD × 1,000,000) so cheap models like
	 * gpt-5-nano ($0.00006/call) don't round up to 1¢.
	 *
	 * @var array{calls:int,input_tokens:int,output_tokens:int,cost_micros:int,providers:array<string,int>}
	 */
	private static $request_api_snapshot = array(
		'calls'         => 0,
		'input_tokens'  => 0,
		'output_tokens' => 0,
		'cost_micros'   => 0,
		'providers'     => array(),
	);

	/**
	 * Whether the shutdown flush hook has already been registered.
	 *
	 * @var bool
	 */
	private static $shutdown_registered = false;

	/**
	 * Increment a tool-level counter (generated / approved / rejected / applied / ...).
	 *
	 * Safe to call from any controller, service, or background job.
	 *
	 * @param string $tool   Tool identifier (advanced_desc, standard_desc, title, ...).
	 * @param string $event  Event name (generated, approved, rejected, applied, messages).
	 * @param int    $count  How many to add (default 1).
	 * @return void
	 */
	public static function increment( string $tool, string $event, int $count = 1 ): void {
		if ( $count <= 0 || '' === $tool || '' === $event ) {
			return;
		}
		$month = self::current_month();
		if ( ! isset( self::$buffer[ $month ] ) ) {
			self::$buffer[ $month ] = array( 'tools' => array(), 'api' => array() );
		}
		if ( ! isset( self::$buffer[ $month ]['tools'][ $tool ] ) ) {
			self::$buffer[ $month ]['tools'][ $tool ] = array();
		}
		if ( ! isset( self::$buffer[ $month ]['tools'][ $tool ][ $event ] ) ) {
			self::$buffer[ $month ]['tools'][ $tool ][ $event ] = 0;
		}
		self::$buffer[ $month ]['tools'][ $tool ][ $event ] += $count;
		self::register_shutdown_flush();
	}

	/**
	 * Record a discrete event and also push it onto the recent-events ring
	 * buffer so it appears in the Toolkit dashboard's Recent Activity feed.
	 *
	 * Combines `increment()` with a buffer push so each callsite stays a
	 * one-liner — the counter is incremented by $count (for the monthly
	 * totals card) and a single feed entry is appended (regardless of
	 * count, so a bulk run of 50 products is one feed row, not fifty).
	 *
	 * @param string $tool    Tool identifier (advanced_desc, standard_desc, title, faq, seo_meta, image_alt, taxonomy, automation).
	 * @param string $event   Event name (generated, approved, rejected, applied).
	 * @param int    $count   Items processed; used for the counter and the feed entry's `count` field.
	 * @param array  $context Optional: product_id (int), product_name (string), or label (string) override.
	 * @return void
	 */
	public static function record_event( string $tool, string $event, int $count = 1, array $context = array() ): void {
		if ( '' === $tool || '' === $event ) {
			return;
		}
		self::increment( $tool, $event, max( 1, $count ) );

		$entry = array(
			'tool'       => $tool,
			'event'      => $event,
			'count'      => max( 1, $count ),
			'product_id' => isset( $context['product_id'] ) ? (int) $context['product_id'] : 0,
			'label'      => isset( $context['label'] ) ? (string) $context['label'] : '',
			'created_at' => gmdate( 'Y-m-d H:i:s' ),
		);

		if ( self::$request_api_snapshot['calls'] > 0 ) {
			$entry['api'] = array(
				'calls'         => self::$request_api_snapshot['calls'],
				'input_tokens'  => self::$request_api_snapshot['input_tokens'],
				'output_tokens' => self::$request_api_snapshot['output_tokens'],
				'cost_micros'   => self::$request_api_snapshot['cost_micros'],
				'providers'     => self::$request_api_snapshot['providers'],
			);
			self::reset_request_api_snapshot();
		}

		self::$recent_buffer[] = $entry;
		self::register_shutdown_flush();
	}

	private static function reset_request_api_snapshot(): void {
		self::$request_api_snapshot = array(
			'calls'         => 0,
			'input_tokens'  => 0,
			'output_tokens' => 0,
			'cost_micros'   => 0,
			'providers'     => array(),
		);
	}

	/**
	 * Return the most recent N events in reverse-chronological order.
	 *
	 * Reads from the persisted ring buffer and prepends any in-request
	 * events that have not yet been flushed.
	 *
	 * @param int $limit Maximum number of entries to return (1..30).
	 * @return array<int, array>
	 */
	public static function get_recent_events( int $limit = 10 ): array {
		$limit = max( 1, min( self::RECENT_BUFFER_MAX, $limit ) );

		$persisted = get_option( self::OPTION_RECENT, array() );
		if ( ! is_array( $persisted ) ) {
			$persisted = array();
		}

		$merged = array_merge( self::$recent_buffer, $persisted );

		usort(
			$merged,
			static function ( $a, $b ) {
				$ta = isset( $a['created_at'] ) ? (string) $a['created_at'] : '';
				$tb = isset( $b['created_at'] ) ? (string) $b['created_at'] : '';
				return strcmp( $tb, $ta );
			}
		);

		return array_slice( $merged, 0, $limit );
	}

	/**
	 * Track a single AI provider API call.
	 *
	 * Pulls per-1M-token pricing from ApiPricing and accumulates token counts
	 * plus an estimated cost (in USD cents) per provider for the current month.
	 *
	 * @param string $provider      Provider id (openai, gemini, claude, openrouter).
	 * @param string $model         Model identifier as sent to the provider.
	 * @param int    $input_tokens  Prompt/input tokens.
	 * @param int    $output_tokens Completion/output tokens.
	 * @return void
	 */
	public static function trackApiCall( string $provider, string $model, int $input_tokens, int $output_tokens ): void {
		if ( '' === $provider ) {
			return;
		}
		$in_clamped  = max( 0, $input_tokens );
		$out_clamped = max( 0, $output_tokens );
		$cost_micros = ApiPricing::calculate_cost_micros( $provider, $model, $in_clamped, $out_clamped );

		$month = self::current_month();
		if ( ! isset( self::$buffer[ $month ] ) ) {
			self::$buffer[ $month ] = array( 'tools' => array(), 'api' => array() );
		}
		if ( ! isset( self::$buffer[ $month ]['api'][ $provider ] ) ) {
			self::$buffer[ $month ]['api'][ $provider ] = array(
				'calls'         => 0,
				'input_tokens'  => 0,
				'output_tokens' => 0,
				'cost_micros'   => 0,
			);
		}
		$bucket = &self::$buffer[ $month ]['api'][ $provider ];
		$bucket['calls']         += 1;
		$bucket['input_tokens']  += $in_clamped;
		$bucket['output_tokens'] += $out_clamped;
		$bucket['cost_micros']   += $cost_micros;
		unset( $bucket );

		self::$request_api_snapshot['calls']         += 1;
		self::$request_api_snapshot['input_tokens']  += $in_clamped;
		self::$request_api_snapshot['output_tokens'] += $out_clamped;
		self::$request_api_snapshot['cost_micros']   += $cost_micros;
		if ( ! isset( self::$request_api_snapshot['providers'][ $provider ] ) ) {
			self::$request_api_snapshot['providers'][ $provider ] = 0;
		}
		self::$request_api_snapshot['providers'][ $provider ] += 1;

		self::register_shutdown_flush();
	}

	/**
	 * Get usage payload for the current month (merged buffer + persisted).
	 *
	 * @return array
	 */
	public static function get_current_month(): array {
		return self::get_month( self::current_month() );
	}

	/**
	 * Get usage payload for a specific month key (YYYY_MM).
	 *
	 * @param string $month_key Month key, e.g. "2026_05".
	 * @return array
	 */
	public static function get_month( string $month_key ): array {
		$persisted = get_option( self::OPTION_PREFIX . $month_key, array() );
		if ( ! is_array( $persisted ) ) {
			$persisted = array();
		}
		$persisted = self::ensure_shape( $persisted );

		if ( ! empty( self::$buffer[ $month_key ] ) ) {
			$persisted = self::merge( $persisted, self::$buffer[ $month_key ] );
		}

		$persisted['totals'] = self::compute_totals( $persisted );

		return $persisted;
	}

	/**
	 * Flush the in-request buffer to wp_options. Registered as a shutdown
	 * callback the first time anything is tracked.
	 *
	 * @internal
	 * @return void
	 */
	public static function flush(): void {

		if ( ! empty( self::$buffer ) ) {
			foreach ( self::$buffer as $month_key => $delta ) {
				$option_key = self::OPTION_PREFIX . $month_key;
				$current    = get_option( $option_key, array() );
				if ( ! is_array( $current ) ) {
					$current = array();
				}
				$current = self::ensure_shape( $current );
				$merged  = self::merge( $current, $delta );
				update_option( $option_key, $merged, false );
			}
			self::$buffer = array();
		}

		if ( ! empty( self::$recent_buffer ) ) {
			$persisted = get_option( self::OPTION_RECENT, array() );
			if ( ! is_array( $persisted ) ) {
				$persisted = array();
			}
			$merged = array_merge( self::$recent_buffer, $persisted );
			if ( count( $merged ) > self::RECENT_BUFFER_MAX ) {
				$merged = array_slice( $merged, 0, self::RECENT_BUFFER_MAX );
			}
			update_option( self::OPTION_RECENT, $merged, false );
			self::$recent_buffer = array();
		}
	}

	/**
	 * Return current YYYY_MM key in site timezone-agnostic UTC.
	 *
	 * @return string
	 */
	private static function current_month(): string {
		return gmdate( 'Y_m' );
	}

	private static function register_shutdown_flush(): void {
		if ( self::$shutdown_registered ) {
			return;
		}
		self::$shutdown_registered = true;
		if ( function_exists( 'add_action' ) ) {
			add_action( 'shutdown', array( __CLASS__, 'flush' ), 99 );
		}
	}

	private static function ensure_shape( array $data ): array {
		if ( ! isset( $data['tools'] ) || ! is_array( $data['tools'] ) ) {
			$data['tools'] = array();
		}
		if ( ! isset( $data['api'] ) || ! is_array( $data['api'] ) ) {
			$data['api'] = array();
		}
		return $data;
	}

	/**
	 * Deep-merge two usage payloads by summing numeric leaves.
	 *
	 * @param array $base  Existing persisted snapshot.
	 * @param array $delta Pending increments.
	 * @return array
	 */
	private static function merge( array $base, array $delta ): array {
		$base = self::ensure_shape( $base );
		$delta = self::ensure_shape( $delta );

		foreach ( $delta['tools'] as $tool => $events ) {
			if ( ! is_array( $events ) ) {
				continue;
			}
			if ( ! isset( $base['tools'][ $tool ] ) || ! is_array( $base['tools'][ $tool ] ) ) {
				$base['tools'][ $tool ] = array();
			}
			foreach ( $events as $event => $count ) {
				$prev = isset( $base['tools'][ $tool ][ $event ] ) ? (int) $base['tools'][ $tool ][ $event ] : 0;
				$base['tools'][ $tool ][ $event ] = $prev + (int) $count;
			}
		}

		foreach ( $delta['api'] as $provider => $bucket ) {
			if ( ! is_array( $bucket ) ) {
				continue;
			}
			if ( ! isset( $base['api'][ $provider ] ) || ! is_array( $base['api'][ $provider ] ) ) {
				$base['api'][ $provider ] = array(
					'calls'         => 0,
					'input_tokens'  => 0,
					'output_tokens' => 0,
					'cost_micros'   => 0,
				);
			}
			foreach ( array( 'calls', 'input_tokens', 'output_tokens', 'cost_micros' ) as $field ) {
				if ( isset( $bucket[ $field ] ) ) {
					$base['api'][ $provider ][ $field ] = (int) ( $base['api'][ $provider ][ $field ] ?? 0 ) + (int) $bucket[ $field ];
				}
			}
		}

		return $base;
	}

	/**
	 * Compute aggregate totals across all tools/providers.
	 *
	 * @param array $data Usage payload.
	 * @return array<string,int>
	 */
	private static function compute_totals( array $data ): array {
		$totals = array(
			'generated'     => 0,
			'approved'      => 0,
			'rejected'      => 0,
			'applied'       => 0,
			'api_calls'     => 0,
			'input_tokens'  => 0,
			'output_tokens' => 0,
			'cost_micros'   => 0,
		);
		foreach ( $data['tools'] as $events ) {
			if ( ! is_array( $events ) ) {
				continue;
			}
			foreach ( array( 'generated', 'approved', 'rejected', 'applied' ) as $k ) {
				if ( isset( $events[ $k ] ) ) {
					$totals[ $k ] += (int) $events[ $k ];
				}
			}
		}
		foreach ( $data['api'] as $bucket ) {
			if ( ! is_array( $bucket ) ) {
				continue;
			}
			$totals['api_calls']     += (int) ( $bucket['calls'] ?? 0 );
			$totals['input_tokens']  += (int) ( $bucket['input_tokens'] ?? 0 );
			$totals['output_tokens'] += (int) ( $bucket['output_tokens'] ?? 0 );
			$totals['cost_micros']   += (int) ( $bucket['cost_micros'] ?? 0 );
		}
		return $totals;
	}
}
