<?php
/**
 * Shared per-source payload cache.
 *
 * One transient per source+deal combination, shared across all block types
 * (Exchange Rates, Ticker, Rate Card, Price Badge, Converter).
 * Eliminates duplicate HTTP requests when multiple blocks use the same source.
 *
 * @package CRTodayCurrencyBlocks
 */

declare(strict_types=1);

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

final class CRToday_Source_Cache {

	private const CACHE_PREFIX           = 'crtoday_src_';
	private const FETCH_LOCK_TTL_SECONDS = 20;
	private const API_TIMEOUT            = 8;

	/** @var array<string,array{state:string,payload:?array,fetch_ts:int,api_ts:int}> */
	private static array $memory = [];

	/**
	 * Resolve a raw API payload for a built-in source+deal.
	 *
	 * @return array{state:string,payload:?array,fetch_ts:int,api_ts:int}
	 *   state: 'fresh'|'stale'|'error'
	 *   payload: raw API payload {base, rates, ts, ...} or null on error
	 *   fetch_ts: Unix timestamp when data was fetched
	 *   api_ts:   Unix timestamp from the API itself (0 if unknown)
	 */
	public static function resolve(string $source_id, string $deal): array {
		$source_id = strtoupper($source_id);
		$deal      = strtoupper($deal);
		$mem_key   = $source_id . ':' . $deal;

		if (isset(self::$memory[$mem_key])) {
			return self::$memory[$mem_key];
		}

		$cache_key = self::cache_key($source_id, $deal);
		$ttl       = self::cache_ttl();
		$cached    = get_transient($cache_key);
		$fresh     = is_array($cached)
			&& isset($cached['savedAt'])
			&& (time() - (int) $cached['savedAt']) < $ttl;

		/* 1. Fresh transient → return immediately */
		if ($fresh && isset($cached['payload']) && is_array($cached['payload'])) {
			$result = [
				'state'    => 'fresh',
				'payload'  => $cached['payload'],
				'fetch_ts' => (int) ($cached['savedAt'] ?? time()),
				'api_ts'   => (int) ($cached['api_ts'] ?? 0),
			];
			self::$memory[$mem_key] = $result;
			self::record_metrics($source_id, $deal, $result);
			return $result;
		}

		/* 2. Stale → attempt HTTP fetch */
		if (self::try_acquire_fetch_lock($cache_key)) {
			try {
				$fetched = self::fetch_remote($source_id, $deal);
			} finally {
				self::release_fetch_lock($cache_key);
			}
			if ($fetched !== null) {
				$now = time();
				set_transient($cache_key, [
					'savedAt' => $now,
					'api_ts'  => $fetched['api_ts'],
					'payload' => $fetched['payload'],
				], $ttl);
				$result = [
					'state'    => 'fresh',
					'payload'  => $fetched['payload'],
					'fetch_ts' => $now,
					'api_ts'   => $fetched['api_ts'],
				];
				self::$memory[$mem_key] = $result;
				self::record_metrics($source_id, $deal, $result);
				return $result;
			}
		}

		/* 3. DB fallback */
		$db = self::fetch_db($source_id, $deal);
		if ($db !== null) {
			set_transient($cache_key, [
				'savedAt' => time(),
				'api_ts'  => $db['api_ts'],
				'payload' => $db['payload'],
			], $ttl);
			$result = [
				'state'    => 'fresh',
				'payload'  => $db['payload'],
				'fetch_ts' => $db['fetch_ts'],
				'api_ts'   => $db['api_ts'],
			];
			self::$memory[$mem_key] = $result;
			self::record_metrics($source_id, $deal, $result);
			return $result;
		}

		/* 4. Stale transient as last resort */
		if (is_array($cached) && isset($cached['payload']) && is_array($cached['payload'])) {
			$result = [
				'state'    => 'stale',
				'payload'  => $cached['payload'],
				'fetch_ts' => (int) ($cached['savedAt'] ?? time()),
				'api_ts'   => (int) ($cached['api_ts'] ?? 0),
			];
			self::$memory[$mem_key] = $result;
			self::record_metrics($source_id, $deal, $result);
			return $result;
		}

		return ['state' => 'error', 'payload' => null, 'fetch_ts' => 0, 'api_ts' => 0];
	}

	/* ================================================================
	 * Remote fetch
	 * ================================================================ */

	private static function fetch_remote(string $source_id, string $deal): ?array {
		$resolved = CRToday_Plugin_Settings::resolve_source($source_id, $deal);
		if ($resolved === null || ($resolved['type'] ?? '') !== 'builtin') {
			return null;
		}
		$endpoint = (string) ($resolved['endpoint'] ?? '');
		if ($endpoint === '') {
			return null;
		}

		$endpoint = apply_filters('crtoday_source_cache_endpoint', $endpoint, $source_id, $deal);

		$response = wp_remote_get($endpoint, ['timeout' => self::API_TIMEOUT]);
		if (is_wp_error($response)) {
			return null;
		}

		$status = (int) wp_remote_retrieve_response_code($response);
		if ($status < 200 || $status >= 300) {
			return null;
		}

		$body    = wp_remote_retrieve_body($response);
		$payload = json_decode($body, true);
		if (! is_array($payload) || ! isset($payload['rates']) || ! is_array($payload['rates'])) {
			return null;
		}

		$api_ts_unix   = (isset($payload['ts']) && is_numeric($payload['ts'])) ? (int) $payload['ts'] : 0;
		$fetch_ts_unix = time();

		/* Persist to DB for history + fallback */
		$config       = CRToday_Plugin_Settings::get_config();
		$interval_min = in_array((int) ($config['api_interval'] ?? 15), [15, 30, 60], true)
			? (int) $config['api_interval']
			: 15;
		CRToday_Raw_Snapshot_Repository::save_remote_payload(
			$source_id,
			$deal,
			strtoupper((string) ($payload['base'] ?? 'USD')),
			$payload,
			$fetch_ts_unix,
			($api_ts_unix > 0 ? $api_ts_unix : $fetch_ts_unix),
			$interval_min
		);

		/* Cache available currencies for source selector UI */
		$source_currencies   = array_map('strtoupper', array_keys($payload['rates']));
		$source_currencies[] = strtoupper((string) ($payload['base'] ?? 'USD'));
		$source_currencies   = array_values(array_unique($source_currencies));
		sort($source_currencies);
		set_transient(
			'crtoday_source_currencies_' . strtolower($source_id . '_' . $deal),
			$source_currencies,
			DAY_IN_SECONDS
		);

		return [
			'payload' => $payload,
			'api_ts'  => $api_ts_unix,
		];
	}

	/* ================================================================
	 * DB fallback
	 * ================================================================ */

	private static function fetch_db(string $source_id, string $deal): ?array {
		$config       = CRToday_Plugin_Settings::get_config();
		$interval_min = in_array((int) ($config['api_interval'] ?? 15), [15, 30, 60], true)
			? (int) $config['api_interval']
			: 15;

		$snapshot = CRToday_Raw_Snapshot_Repository::get_latest_payload($source_id, $deal, $interval_min);
		if (! is_array($snapshot)) {
			return null;
		}

		$payload = $snapshot['payload'] ?? null;
		if (! is_array($payload) || ! isset($payload['rates']) || ! is_array($payload['rates'])) {
			return null;
		}

		$fetch_ts_unix = max(1, (int) ($snapshot['fetch_ts'] ?? 0));
		$api_ts_unix   = (int) ($snapshot['source_ts'] ?? 0);
		if ($api_ts_unix <= 0 && isset($payload['ts']) && is_numeric($payload['ts'])) {
			$api_ts_unix = (int) $payload['ts'];
		}

		return [
			'payload'  => $payload,
			'fetch_ts' => $fetch_ts_unix,
			'api_ts'   => $api_ts_unix,
		];
	}

	/* ================================================================
	 * Metrics
	 * ================================================================ */

	private static function record_metrics(string $source_id, string $deal, array $result): void {
		CRToday_Plugin_Settings::record_builtin_source_fetch(
			$source_id,
			$deal,
			max(1, $result['fetch_ts']),
			$result['api_ts']
		);
	}

	/* ================================================================
	 * Helpers
	 * ================================================================ */

	private static function cache_key(string $source_id, string $deal): string {
		return self::CACHE_PREFIX . md5($source_id . ':' . $deal);
	}

	private static function cache_ttl(): int {
		$config = CRToday_Plugin_Settings::get_config();
		return max(1, (int) $config['api_interval']) * 60;
	}

	/* ── Fetch lock (same mechanism as trait-crtoday-fetch-lock.php) ── */

	private static function fetch_lock_option_key(string $cache_key): string {
		return 'crtoday_fetch_lock_' . md5($cache_key);
	}

	private static function try_acquire_fetch_lock(string $cache_key): bool {
		$lock_key = self::fetch_lock_option_key($cache_key);
		$now      = time();
		$current  = (int) get_option($lock_key, 0);

		if ($current > $now) {
			return false;
		}

		if ($current > 0) {
			delete_option($lock_key);
		}

		return add_option($lock_key, $now + self::FETCH_LOCK_TTL_SECONDS, '', false);
	}

	private static function release_fetch_lock(string $cache_key): void {
		delete_option(self::fetch_lock_option_key($cache_key));
	}
}
