<?php

declare(strict_types=1);

namespace App\Framework\MachineLearning\ModelManagement;

use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelMetadata;
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ABTestConfig;
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ABTestResult;
use App\Framework\Core\ValueObjects\Version;
use App\Framework\Random\SecureRandomGenerator;

/**
 * A/B Testing Service - Model Comparison and Traffic Splitting
 *
 * Enables A/B testing of different ML model versions by:
 * - Traffic splitting (percentage-based or random)
 * - Performance metric comparison
 * - Statistical significance testing
 * - Winner selection based on metrics
 *
 * Usage:
 * ```php
 * // Create A/B test config
 * $config = ABTestConfig::create(
 *     modelName: 'n1-detector',
 *     versionA: Version::fromString('1.0.0'),
 *     versionB: Version::fromString('1.1.0'),
 *     trafficSplit: 0.5  // 50/50 split
 * );
 *
 * // Route traffic
 * $selectedVersion = $abTesting->selectVersion($config);
 *
 * // Compare results
 * $result = $abTesting->compareModels($config, $metadataA, $metadataB);
 * ```
 */
final readonly class ABTestingService
{
    public function __construct(
        private SecureRandomGenerator $random,
        private ModelRegistry $registry
    ) {}

    /**
     * Select model version based on A/B test configuration
     *
     * Uses traffic split percentage to randomly route between versions.
     *
     * @return Version Selected model version (A or B)
     */
    public function selectVersion(ABTestConfig $config): Version
    {
        // Generate random number 0.0-1.0
        $randomValue = $this->random->float(0, 1);

        // If random < trafficSplit, select version A, otherwise B
        return $randomValue < $config->trafficSplitA
            ? $config->versionA
            : $config->versionB;
    }

    /**
     * Compare two model versions based on performance metrics
     *
     * Analyzes performance differences and determines statistical significance.
     *
     * @return ABTestResult Comparison result with winner and metrics diff
     */
    public function compareModels(
        ABTestConfig $config,
        ModelMetadata $metadataA,
        ModelMetadata $metadataB
    ): ABTestResult {
        // Calculate metric differences
        $metricsDiff = $this->calculateMetricsDifference($metadataA, $metadataB);

        // Determine winner based on primary metric
        $winner = $this->determineWinner(
            $metadataA,
            $metadataB,
            $config->primaryMetric,
            $config->minimumImprovement
        );

        // Calculate statistical significance (if sample size available)
        $statisticallySignificant = $this->isStatisticallySignificant(
            $metricsDiff,
            $config->significanceLevel
        );

        return new ABTestResult(
            config: $config,
            metadataA: $metadataA,
            metadataB: $metadataB,
            metricsDifference: $metricsDiff,
            winner: $winner,
            isStatisticallySignificant: $statisticallySignificant,
            recommendation: $this->generateRecommendation(
                $winner,
                $statisticallySignificant,
                $metricsDiff
            )
        );
    }

    /**
     * Run A/B test automatically from registry
     */
    public function runTest(ABTestConfig $config): ABTestResult
    {
        // Retrieve models from registry
        $metadataA = $this->registry->get($config->modelName, $config->versionA);
        $metadataB = $this->registry->get($config->modelName, $config->versionB);

        if ($metadataA === null) {
            throw new \InvalidArgumentException(
                "Model '{$config->modelName}@{$config->versionA}' not found in registry"
            );
        }

        if ($metadataB === null) {
            throw new \InvalidArgumentException(
                "Model '{$config->modelName}@{$config->versionB}' not found in registry"
            );
        }

        return $this->compareModels($config, $metadataA, $metadataB);
    }

    /**
     * Calculate difference between model metrics
     *
     * @return array<string, float> Metric name => difference (B - A)
     */
    private function calculateMetricsDifference(
        ModelMetadata $metadataA,
        ModelMetadata $metadataB
    ): array {
        $diff = [];

        // Get all unique metric names
        $metricsA = $metadataA->performanceMetrics;
        $metricsB = $metadataB->performanceMetrics;

        $allMetrics = array_unique(array_merge(
            array_keys($metricsA),
            array_keys($metricsB)
        ));

        foreach ($allMetrics as $metric) {
            $valueA = $metadataA->getMetric($metric, 0.0);
            $valueB = $metadataB->getMetric($metric, 0.0);

            $diff[$metric] = $valueB - $valueA;
        }

        return $diff;
    }

    /**
     * Determine winner based on primary metric
     *
     * @return string 'A', 'B', or 'tie'
     */
    private function determineWinner(
        ModelMetadata $metadataA,
        ModelMetadata $metadataB,
        string $primaryMetric,
        float $minimumImprovement
    ): string {
        $valueA = $metadataA->getMetric($primaryMetric, 0.0);
        $valueB = $metadataB->getMetric($primaryMetric, 0.0);

        // Calculate improvement percentage
        if ($valueA === 0.0) {
            return $valueB > 0.0 ? 'B' : 'tie';
        }

        $improvement = ($valueB - $valueA) / $valueA;

        // Check if improvement exceeds minimum threshold
        if ($improvement >= $minimumImprovement) {
            return 'B';
        }

        if ($improvement <= -$minimumImprovement) {
            return 'A';
        }

        return 'tie';
    }

    /**
     * Check statistical significance using simplified test
     *
     * For more accurate testing, implement:
     * - T-test (small samples)
     * - Z-test (large samples)
     * - Chi-square test (categorical data)
     * - Mann-Whitney U test (non-parametric)
     *
     * Current implementation: Simple threshold check
     * TODO: Implement proper statistical tests when sample sizes available
     */
    private function isStatisticallySignificant(
        array $metricsDiff,
        float $significanceLevel
    ): bool {
        // Simplified: Check if primary metric difference exceeds threshold
        // In production, use actual statistical tests with sample sizes

        foreach ($metricsDiff as $metric => $diff) {
            // Absolute difference > significance level
            if (abs($diff) > $significanceLevel) {
                return true;
            }
        }

        return false;
    }

    /**
     * Generate recommendation based on test results
     */
    private function generateRecommendation(
        string $winner,
        bool $statisticallySignificant,
        array $metricsDiff
    ): string {
        if (!$statisticallySignificant) {
            return 'Results not statistically significant - continue testing or collect more data';
        }

        if ($winner === 'tie') {
            return 'Models perform equally - choose based on other factors (cost, latency, maintainability)';
        }

        $primaryDiff = reset($metricsDiff);
        $improvementPct = abs($primaryDiff) * 100;

        return match ($winner) {
            'A' => sprintf(
                'Version A wins with %.1f%% improvement - keep current version',
                $improvementPct
            ),
            'B' => sprintf(
                'Version B wins with %.1f%% improvement - deploy new version',
                $improvementPct
            ),
            default => 'Unable to determine winner'
        };
    }

    /**
     * Generate traffic routing weights for gradual rollout
     *
     * Creates a rollout plan increasing traffic to version B gradually.
     *
     * @param int $steps Number of rollout steps
     * @return array<int, float> Step number => traffic split for B
     */
    public function generateRolloutPlan(int $steps = 5): array
    {
        $plan = [];

        for ($i = 1; $i <= $steps; $i++) {
            $trafficSplitB = $i / $steps;
            $plan[$i] = $trafficSplitB;
        }

        return $plan;
    }

    /**
     * Calculate sample size needed for desired confidence
     *
     * Uses simplified formula. For production, use:
     * n = (Z^2 * p * (1-p)) / E^2
     *
     * Where:
     * - Z = Z-score for confidence level (1.96 for 95%)
     * - p = estimated proportion (0.5 for maximum sample size)
     * - E = margin of error
     */
    public function calculateRequiredSampleSize(
        float $confidenceLevel = 0.95,
        float $marginOfError = 0.05
    ): int {
        // Z-score for common confidence levels
        $zScore = match (true) {
            $confidenceLevel >= 0.99 => 2.576,
            $confidenceLevel >= 0.95 => 1.96,
            $confidenceLevel >= 0.90 => 1.645,
            default => 1.96
        };

        // Use p = 0.5 for maximum sample size (most conservative)
        $p = 0.5;

        // Calculate sample size
        $n = ($zScore ** 2 * $p * (1 - $p)) / ($marginOfError ** 2);

        return (int) ceil($n);
    }
}
