<?php

declare(strict_types=1);

namespace App\Framework\MachineLearning\ModelManagement;

use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelMetadata;
use App\Framework\Core\ValueObjects\Version;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Core\ValueObjects\Duration;

/**
 * Model Performance Monitor - Real-Time Accuracy Tracking
 *
 * Monitors ML model performance in production with:
 * - Real-time accuracy tracking
 * - Performance degradation detection
 * - Drift detection
 * - Automated alerting
 *
 * Usage:
 * ```php
 * // Track prediction
 * $monitor->trackPrediction(
 *     modelName: 'n1-detector',
 *     version: Version::fromString('1.0.0'),
 *     prediction: true,
 *     actual: true,
 *     confidence: 0.95
 * );
 *
 * // Get current metrics
 * $metrics = $monitor->getCurrentMetrics('n1-detector', $version);
 *
 * // Check for degradation
 * if ($monitor->hasPerformanceDegraded('n1-detector', $version)) {
 *     $monitor->triggerAlert($degradationInfo);
 * }
 * ```
 */
final readonly class ModelPerformanceMonitor
{
    /**
     * @param ModelRegistry $registry Model registry for baseline comparison
     * @param PerformanceStorage $storage Performance data storage
     * @param AlertingService $alerting Alert service for notifications
     * @param MLConfig $config ML configuration settings
     */
    public function __construct(
        private ModelRegistry $registry,
        private PerformanceStorage $storage,
        private AlertingService $alerting,
        private MLConfig $config
    ) {}

    /**
     * Track a prediction and its outcome
     *
     * @param string $modelName Model identifier
     * @param Version $version Model version
     * @param mixed $prediction Model prediction
     * @param mixed $actual Actual outcome (if known)
     * @param float $confidence Prediction confidence score
     * @param array $features Feature values used for prediction
     */
    public function trackPrediction(
        string $modelName,
        Version $version,
        mixed $prediction,
        mixed $actual = null,
        float $confidence = 0.0,
        array $features = []
    ): void {
        $predictionRecord = [
            'model_name' => $modelName,
            'version' => $version->toString(),
            'prediction' => $prediction,
            'actual' => $actual,
            'confidence' => $confidence,
            'features' => $features,
            'timestamp' => Timestamp::now()->toDateTime(),
            'is_correct' => $actual !== null ? $prediction === $actual : null,
        ];

        $this->storage->storePrediction($predictionRecord);

        // Check for degradation if we have ground truth
        if ($actual !== null) {
            $this->checkPerformanceDegradation($modelName, $version);
        }
    }

    /**
     * Get current performance metrics for a model
     *
     * @return array{
     *     accuracy: float,
     *     precision: float,
     *     recall: float,
     *     f1_score: float,
     *     total_predictions: int,
     *     correct_predictions: int,
     *     average_confidence: float,
     *     last_updated: Timestamp
     * }
     */
    public function getCurrentMetrics(
        string $modelName,
        Version $version,
        ?Duration $timeWindow = null
    ): array {
        $timeWindow = $timeWindow ?? Duration::fromHours(24);

        $predictions = $this->storage->getPredictions(
            $modelName,
            $version,
            $timeWindow
        );

        if (empty($predictions)) {
            return $this->getEmptyMetrics();
        }

        // Filter predictions with ground truth
        $validPredictions = array_filter(
            $predictions,
            fn($p) => $p['actual'] !== null
        );

        if (empty($validPredictions)) {
            return $this->getEmptyMetrics();
        }

        $total = count($validPredictions);
        $correct = count(array_filter($validPredictions, fn($p) => $p['is_correct']));

        // Calculate confusion matrix
        $confusionMatrix = $this->calculateConfusionMatrix($validPredictions);

        $accuracy = $total > 0 ? $correct / $total : 0.0;
        $precision = $this->calculatePrecision($confusionMatrix);
        $recall = $this->calculateRecall($confusionMatrix);
        $f1Score = $this->calculateF1Score($precision, $recall);

        $avgConfidence = array_sum(array_column($validPredictions, 'confidence')) / $total;

        return [
            'accuracy' => $accuracy,
            'precision' => $precision,
            'recall' => $recall,
            'f1_score' => $f1Score,
            'total_predictions' => $total,
            'correct_predictions' => $correct,
            'average_confidence' => $avgConfidence,
            'confusion_matrix' => $confusionMatrix,
            'last_updated' => Timestamp::now(),
        ];
    }

    /**
     * Check if model performance has degraded below baseline
     */
    public function hasPerformanceDegraded(
        string $modelName,
        Version $version,
        float $thresholdPercent = 0.05
    ): bool {
        // Get baseline metrics from registry
        $metadata = $this->registry->get($modelName, $version);

        if ($metadata === null) {
            return false;
        }

        $baselineAccuracy = $metadata->getAccuracy() ?? 0.0;

        if ($baselineAccuracy === 0.0) {
            return false;
        }

        // Get current real-time metrics
        $currentMetrics = $this->getCurrentMetrics($modelName, $version);
        $currentAccuracy = $currentMetrics['accuracy'];

        // Calculate degradation
        $degradation = ($baselineAccuracy - $currentAccuracy) / $baselineAccuracy;

        return $degradation > $thresholdPercent;
    }

    /**
     * Get performance degradation info
     *
     * @return array{
     *     has_degraded: bool,
     *     baseline_accuracy: float,
     *     current_accuracy: float,
     *     degradation_percent: float,
     *     threshold_percent: float,
     *     recommendation: string
     * }
     */
    public function getPerformanceDegradationInfo(
        string $modelName,
        Version $version,
        float $thresholdPercent = 0.05
    ): array {
        $metadata = $this->registry->get($modelName, $version);
        $baselineAccuracy = $metadata?->getAccuracy() ?? 0.0;

        $currentMetrics = $this->getCurrentMetrics($modelName, $version);
        $currentAccuracy = $currentMetrics['accuracy'];

        $degradation = $baselineAccuracy > 0
            ? ($baselineAccuracy - $currentAccuracy) / $baselineAccuracy
            : 0.0;

        $hasDegraded = $degradation > $thresholdPercent;

        $recommendation = $this->generateDegradationRecommendation(
            $degradation,
            $hasDegraded
        );

        return [
            'has_degraded' => $hasDegraded,
            'baseline_accuracy' => $baselineAccuracy,
            'current_accuracy' => $currentAccuracy,
            'degradation_percent' => $degradation * 100,
            'threshold_percent' => $thresholdPercent * 100,
            'recommendation' => $recommendation,
            'current_metrics' => $currentMetrics,
        ];
    }

    /**
     * Detect concept drift based on prediction confidence distribution
     */
    public function detectConceptDrift(
        string $modelName,
        Version $version,
        ?Duration $timeWindow = null
    ): bool {
        $timeWindow = $timeWindow ?? Duration::fromHours(24);

        $predictions = $this->storage->getPredictions(
            $modelName,
            $version,
            $timeWindow
        );

        if (count($predictions) < 100) {
            return false; // Not enough data
        }

        // Calculate confidence distribution
        $confidences = array_column($predictions, 'confidence');
        $avgConfidence = array_sum($confidences) / count($confidences);
        $stdDevConfidence = $this->calculateStdDev($confidences);

        // Get historical baseline
        $historicalAvg = $this->storage->getHistoricalAverageConfidence(
            $modelName,
            $version
        );

        if ($historicalAvg === null) {
            // Store current as baseline
            $this->storage->storeConfidenceBaseline(
                $modelName,
                $version,
                $avgConfidence,
                $stdDevConfidence
            );
            return false;
        }

        // Detect drift: significant deviation from baseline
        $drift = abs($avgConfidence - $historicalAvg) / $historicalAvg;

        return $drift > 0.1; // 10% drift threshold
    }

    /**
     * Get performance trend over time
     *
     * @return array<array{timestamp: Timestamp, accuracy: float, predictions: int}>
     */
    public function getPerformanceTrend(
        string $modelName,
        Version $version,
        Duration $timeWindow,
        Duration $interval
    ): array {
        $predictions = $this->storage->getPredictions(
            $modelName,
            $version,
            $timeWindow
        );

        // Group by time intervals
        $buckets = [];
        $intervalSeconds = $interval->toSeconds();

        foreach ($predictions as $prediction) {
            $timestamp = $prediction['timestamp']->getTimestamp();
            $bucketKey = (int) floor($timestamp / $intervalSeconds) * $intervalSeconds;

            if (!isset($buckets[$bucketKey])) {
                $buckets[$bucketKey] = [
                    'total' => 0,
                    'correct' => 0,
                    'timestamp' => Timestamp::fromTimestamp($bucketKey),
                ];
            }

            if ($prediction['actual'] !== null) {
                $buckets[$bucketKey]['total']++;
                if ($prediction['is_correct']) {
                    $buckets[$bucketKey]['correct']++;
                }
            }
        }

        // Calculate accuracy for each bucket
        $trend = [];
        foreach ($buckets as $bucket) {
            $accuracy = $bucket['total'] > 0
                ? $bucket['correct'] / $bucket['total']
                : 0.0;

            $trend[] = [
                'timestamp' => $bucket['timestamp'],
                'accuracy' => $accuracy,
                'predictions' => $bucket['total'],
            ];
        }

        // Sort by timestamp
        usort($trend, fn($a, $b) => $a['timestamp']->isBefore($b['timestamp']) ? -1 : 1);

        return $trend;
    }

    /**
     * Compare performance across multiple model versions
     */
    public function compareVersions(
        string $modelName,
        array $versions,
        ?Duration $timeWindow = null
    ): array {
        $comparison = [];

        foreach ($versions as $version) {
            $metrics = $this->getCurrentMetrics($modelName, $version, $timeWindow);
            $metadata = $this->registry->get($modelName, $version);

            $comparison[$version->toString()] = [
                'version' => $version->toString(),
                'current_metrics' => $metrics,
                'baseline_metrics' => $metadata?->performanceMetrics ?? [],
                'deployed_at' => $metadata?->deployedAt?->format('Y-m-d H:i:s'),
                'environment' => $metadata?->environment,
            ];
        }

        return $comparison;
    }

    /**
     * Check performance degradation and send alert if needed
     */
    private function checkPerformanceDegradation(
        string $modelName,
        Version $version
    ): void {
        $degradationInfo = $this->getPerformanceDegradationInfo($modelName, $version);

        if ($degradationInfo['has_degraded']) {
            $this->alerting->sendAlert(
                level: 'warning',
                title: "Model Performance Degradation: {$modelName}@{$version->toString()}",
                message: sprintf(
                    'Accuracy dropped from %.2f%% to %.2f%% (%.2f%% degradation)',
                    $degradationInfo['baseline_accuracy'] * 100,
                    $degradationInfo['current_accuracy'] * 100,
                    $degradationInfo['degradation_percent']
                ),
                data: $degradationInfo
            );
        }
    }

    /**
     * Calculate confusion matrix
     */
    private function calculateConfusionMatrix(array $predictions): array
    {
        $matrix = [
            'true_positive' => 0,
            'true_negative' => 0,
            'false_positive' => 0,
            'false_negative' => 0,
        ];

        foreach ($predictions as $p) {
            if ($p['prediction'] === true && $p['actual'] === true) {
                $matrix['true_positive']++;
            } elseif ($p['prediction'] === false && $p['actual'] === false) {
                $matrix['true_negative']++;
            } elseif ($p['prediction'] === true && $p['actual'] === false) {
                $matrix['false_positive']++;
            } elseif ($p['prediction'] === false && $p['actual'] === true) {
                $matrix['false_negative']++;
            }
        }

        return $matrix;
    }

    private function calculatePrecision(array $confusionMatrix): float
    {
        $tp = $confusionMatrix['true_positive'];
        $fp = $confusionMatrix['false_positive'];

        return ($tp + $fp) > 0 ? $tp / ($tp + $fp) : 0.0;
    }

    private function calculateRecall(array $confusionMatrix): float
    {
        $tp = $confusionMatrix['true_positive'];
        $fn = $confusionMatrix['false_negative'];

        return ($tp + $fn) > 0 ? $tp / ($tp + $fn) : 0.0;
    }

    private function calculateF1Score(float $precision, float $recall): float
    {
        return ($precision + $recall) > 0
            ? 2 * ($precision * $recall) / ($precision + $recall)
            : 0.0;
    }

    private function calculateStdDev(array $values): float
    {
        $count = count($values);

        if ($count === 0) {
            return 0.0;
        }

        $mean = array_sum($values) / $count;
        $variance = array_sum(array_map(fn($x) => ($x - $mean) ** 2, $values)) / $count;

        return sqrt($variance);
    }

    private function generateDegradationRecommendation(
        float $degradation,
        bool $hasDegraded
    ): string {
        if (!$hasDegraded) {
            return 'Model performance is within acceptable range';
        }

        if ($degradation > 0.2) {
            return 'CRITICAL: Roll back to previous version immediately';
        }

        if ($degradation > 0.1) {
            return 'WARNING: Schedule model retraining or rollback';
        }

        return 'NOTICE: Monitor closely, consider retraining if trend continues';
    }

    private function getEmptyMetrics(): array
    {
        return [
            'accuracy' => 0.0,
            'precision' => 0.0,
            'recall' => 0.0,
            'f1_score' => 0.0,
            'total_predictions' => 0,
            'correct_predictions' => 0,
            'average_confidence' => 0.0,
            'confusion_matrix' => [
                'true_positive' => 0,
                'true_negative' => 0,
                'false_positive' => 0,
                'false_negative' => 0,
            ],
            'last_updated' => Timestamp::now(),
        ];
    }
}
