<?php
declare(strict_types=1);

namespace Ekesto\CacheWarmup;

use Ekesto\CacheWarmup\Debug;

/**
 * Rolling per-URL millisecond telemetry for adaptive pacing.
 *
 * Stores a rolling window of request times (last 200 samples) and computes
 * safe batch parameters automatically based on p90 latency.
 */
final class TuningStats
{
    /** Option key for storing tuning data. */
    private const OPTION_KEY = 'ekesto_ecw_tuning_stats';

    /** Legacy option key retained for reset and uninstall cleanup. */
    private const LEGACY_OPTION_KEY = 'ecw_tuning_stats';

    /** Maximum samples to retain (keep rolling window bounded). */
    private const MAX_SAMPLES = 200;

    /** Minimum samples before auto-computation is considered sufficient. */
    private const MIN_SUFFICIENT_SAMPLES = 30;

    /** Max age of telemetry before considering it stale (days). */
    private const STALE_DAYS = 30;

    /**
     * Get or initialize tuning stats from the database.
     *
     * @return array<string,mixed>
     */
    public static function get(): array
    {
        $stored = get_option(self::OPTION_KEY, []);
        $stored = is_array($stored) ? $stored : [];

        // Ensure structure
        if (!isset($stored['samples_ms']) || !is_array($stored['samples_ms'])) {
            $stored['samples_ms'] = [];
        }
        if (!isset($stored['samples_brotli_ms']) || !is_array($stored['samples_brotli_ms'])) {
            $stored['samples_brotli_ms'] = [];
        }
        if (!isset($stored['samples_wp_ms']) || !is_array($stored['samples_wp_ms'])) {
            $stored['samples_wp_ms'] = [];
        }
        if (!isset($stored['transport_samples']) || !is_array($stored['transport_samples'])) {
            $stored['transport_samples'] = [];
        }
        if (!isset($stored['sample_count_total'])) {
            $stored['sample_count_total'] = 0;
        }
        if (!isset($stored['sample_count_brotli_total'])) {
            $stored['sample_count_brotli_total'] = 0;
        }
        if (!isset($stored['sample_count_wp_total'])) {
            $stored['sample_count_wp_total'] = 0;
        }
        if (!isset($stored['updated_at'])) {
            $stored['updated_at'] = 0;
        }
        if (!isset($stored['last_fallback_reason'])) {
            $stored['last_fallback_reason'] = '';
        }
        if (!isset($stored['last_fallback_at'])) {
            $stored['last_fallback_at'] = 0;
        }
        if (!isset($stored['last_computed'])) {
            $stored['last_computed'] = null;
        }

        return $stored;
    }

    /**
     * Append millisecond samples to the rolling window.
     *
     * @param array<int,float|int> $msValues Array of millisecond values to append.
     * @return void
     */
    public static function push(array $msValues): void
    {
        if (empty($msValues)) {
            return;
        }

        $rows = [];
        foreach ($msValues as $v) {
            if (!is_numeric($v)) {
                continue;
            }
            $rows[] = ['effective_ms' => (float) $v];
        }

        self::pushDetailed($rows);
    }

    /**
     * Append effective and transport-specific latency samples.
     *
     * @param array<int,array<string,mixed>> $rows Sample rows.
     * @return void
     */
    public static function pushDetailed(array $rows): void
    {
        if (empty($rows)) {
            return;
        }

        $effective = [];
        $brotli = [];
        $wp = [];
        $transportSamples = [];
        $lastFallbackReason = '';

        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }

            if (isset($row['effective_ms']) && is_numeric($row['effective_ms'])) {
                $effective[] = (float) $row['effective_ms'];

                $transportSample = [
                    'effective_ms' => self::normalizeMsValue($row['effective_ms']),
                ];

                if (isset($row['brotli_ms']) && is_numeric($row['brotli_ms'])) {
                    $transportSample['brotli_ms'] = self::normalizeMsValue($row['brotli_ms']);
                }

                if (isset($row['wp_ms']) && is_numeric($row['wp_ms'])) {
                    $transportSample['fallback_ms'] = self::normalizeMsValue($row['wp_ms']);
                }

                $transportSamples[] = $transportSample;
            }

            if (isset($row['brotli_ms']) && is_numeric($row['brotli_ms'])) {
                $brotli[] = (float) $row['brotli_ms'];
            }

            if (isset($row['wp_ms']) && is_numeric($row['wp_ms'])) {
                $wp[] = (float) $row['wp_ms'];
            }

            if (!empty($row['fallback_reason']) && is_string($row['fallback_reason'])) {
                $fallbackReason = trim($row['fallback_reason']);
                if ($fallbackReason !== '') {
                    $lastFallbackReason = $fallbackReason;
                }
            }
        }

        $effective = self::normalizeMsValues($effective);
        $brotli = self::normalizeMsValues($brotli);
        $wp = self::normalizeMsValues($wp);

        if (empty($effective) && empty($brotli) && empty($wp)) {
            return;
        }

        $data = self::get();

        if (!empty($transportSamples)) {
            $samples = is_array($data['transport_samples'] ?? null) ? $data['transport_samples'] : [];
            $data['transport_samples'] = self::appendTransportSamples($samples, $transportSamples);
        }

        if (!empty($effective)) {
            $samples = is_array($data['samples_ms'] ?? null) ? $data['samples_ms'] : [];
            $data['samples_ms'] = self::appendSamples($samples, $effective);
            $data['sample_count_total'] = (int) ($data['sample_count_total'] ?? 0) + count($effective);
        }

        if (!empty($brotli)) {
            $samples = is_array($data['samples_brotli_ms'] ?? null) ? $data['samples_brotli_ms'] : [];
            $data['samples_brotli_ms'] = self::appendSamples($samples, $brotli);
            $data['sample_count_brotli_total'] = (int) ($data['sample_count_brotli_total'] ?? 0) + count($brotli);
        }

        if (!empty($wp)) {
            $samples = is_array($data['samples_wp_ms'] ?? null) ? $data['samples_wp_ms'] : [];
            $data['samples_wp_ms'] = self::appendSamples($samples, $wp);
            $data['sample_count_wp_total'] = (int) ($data['sample_count_wp_total'] ?? 0) + count($wp);
        }

        if ($lastFallbackReason !== '') {
            $data['last_fallback_reason'] = $lastFallbackReason;
            $data['last_fallback_at'] = time();
        }

        $data['updated_at'] = time();
        update_option(self::OPTION_KEY, $data, false);

        Debug::verbose('TuningStats::pushDetailed', [
            'effective_new_samples' => count($effective),
            'effective_total' => (int) ($data['sample_count_total'] ?? 0),
            'brotli_new_samples' => count($brotli),
            'brotli_total' => (int) ($data['sample_count_brotli_total'] ?? 0),
            'wp_new_samples' => count($wp),
            'wp_total' => (int) ($data['sample_count_wp_total'] ?? 0),
        ]);
    }

    /**
     * Get p90 (90th percentile) millisecond value from current samples.
     *
     * @return int|null P90 value in ms, or null if insufficient samples.
     */
    public static function getP90Ms(): ?int
    {
        $data = self::get();
        $samples = is_array($data['samples_ms'] ?? null) ? $data['samples_ms'] : [];

        if (count($samples) < 10) {
            return null; // Need at least a few samples for meaningful percentile
        }

        sort($samples, SORT_NUMERIC);
        $index = (int) ceil(0.90 * count($samples)) - 1;
        $index = max(0, min($index, count($samples) - 1));

        return (int) $samples[$index];
    }

    /**
     * Check if telemetry is sufficient for auto-computation.
     *
     * Requirements:
     * - At least MIN_SUFFICIENT_SAMPLES samples in rolling window
     * - Data not older than STALE_DAYS
     *
     * @return bool True if safe to use for auto-computation.
     */
    public static function isSufficient(): bool
    {
        $data = self::get();

        $samples = isset($data['samples_ms']) && is_array($data['samples_ms'])
            ? $data['samples_ms']
            : [];

        if (count($samples) < self::MIN_SUFFICIENT_SAMPLES) {
            return false;
        }

        $updatedAt = (int) ($data['updated_at'] ?? 0);
        if ($updatedAt <= 0) {
            return false;
        }

        $ageDays = (time() - $updatedAt) / DAY_IN_SECONDS;
        if ($ageDays > self::STALE_DAYS) {
            return false;
        }

        return true;
    }

    /**
     * Save the last computed safe parameters.
     *
     * Called by Runner after computing auto-pacing values.
     *
     * @param int    $pagesPerBatch   Computed safe pages per batch.
     * @param int    $maxBatchSeconds Computed max batch seconds.
     * @param int    $p90Ms           P90 latency used (for UI display).
     * @param string $basis           'telemetry' or 'bootstrap' indicator.
     * @return void
     */
    public static function saveLastComputed(int $pagesPerBatch, int $maxBatchSeconds, string $basis): void
    {
        $data = self::get();

        $data['last_computed'] = [
            'pages_per_batch'   => max(1, $pagesPerBatch),
            'max_batch_seconds' => max(5, $maxBatchSeconds),
            'basis'             => in_array($basis, ['telemetry', 'bootstrap'], true) ? $basis : 'unknown',
            'computed_at'       => time(),
        ];

        update_option(self::OPTION_KEY, $data, false);

        Debug::verbose('TuningStats::saveLastComputed', [
            'pages' => $pagesPerBatch,
            'max_batch' => $maxBatchSeconds,
            'basis' => $basis,
        ]);
    }

    /**
     * Clear all tuning stats.
     *
     * Called from LoggingTab "Reset Tuning Data" button.
     *
     * @return void
     */
    public static function clear(): void
    {
        delete_option(self::OPTION_KEY);
        delete_option(self::LEGACY_OPTION_KEY);

        Debug::log('TuningStats::cleared');
    }

    /**
     * Normalize raw millisecond values into bounded integers.
     *
     * @param array<int,float|int> $msValues Raw values.
     * @return array<int,int> Normalized values.
     */
    private static function normalizeMsValues(array $msValues): array
    {
        $out = [];

        foreach ($msValues as $v) {
            if (!is_numeric($v)) {
                continue;
            }

            $out[] = self::normalizeMsValue($v);
        }

        return $out;
    }

    /**
     * Normalize one millisecond value into the telemetry bounds.
     *
     * @param mixed $value Raw millisecond value.
     * @return int Normalized millisecond value.
     */
    private static function normalizeMsValue($value): int
    {
        $ms = (int) round((float) $value);

        return max(1, min(60000, $ms));
    }

    /**
     * Append and trim rolling samples.
     *
     * @param array<int,int> $existing Existing bounded samples.
     * @param array<int,int> $newSamples New bounded samples.
     * @return array<int,int> Bounded rolling samples.
     */
    private static function appendSamples(array $existing, array $newSamples): array
    {
        $samples = array_values(array_merge($existing, $newSamples));

        if (count($samples) > self::MAX_SAMPLES) {
            $samples = array_slice($samples, -self::MAX_SAMPLES);
        }

        return $samples;
    }

    /**
     * Append aligned transport samples and trim the diagnostics window.
     *
     * @param array<int,array<string,int>> $existing Existing transport samples.
     * @param array<int,array<string,int>> $newSamples New transport samples.
     * @return array<int,array<string,int>> Bounded transport samples.
     */
    private static function appendTransportSamples(array $existing, array $newSamples): array
    {
        $samples = array_values(array_merge($existing, $newSamples));

        if (count($samples) > self::MAX_SAMPLES) {
            $samples = array_slice($samples, -self::MAX_SAMPLES);
        }

        return $samples;
    }
}
