<?php

declare(strict_types=1);

namespace App\Framework\Waf\Feedback;

use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\Logging\Logger;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\MachineLearning\MachineLearningEngine;
use App\Framework\Waf\MachineLearning\ValueObjects\ModelAdjustment;

/**
 * Service for analyzing feedback data and learning from it to improve WAF detection
 */
final readonly class FeedbackLearningService
{
    /**
     * @param FeedbackRepositoryInterface $repository Repository for accessing feedback data
     * @param MachineLearningEngine $mlEngine Machine learning engine to adjust
     * @param Clock $clock Clock for getting current time
     * @param Logger $logger Logger for logging learning events
     * @param int $minimumFeedbackThreshold Minimum number of feedback items required for learning
     * @param float $learningRate Rate at which to apply adjustments (0.0-1.0)
     */
    public function __construct(
        private FeedbackRepositoryInterface $repository,
        private MachineLearningEngine $mlEngine,
        private Clock $clock,
        private Logger $logger,
        private int $minimumFeedbackThreshold = 5,
        private float $learningRate = 0.3
    ) {
    }

    /**
     * Learn from feedback and adjust detection models
     *
     * @return array<string, mixed> Results of the learning process
     */
    public function learnFromFeedback(): array
    {
        $startTime = $this->clock->now();
        $this->logger->info('Starting WAF feedback learning process');

        // Get feedback statistics
        $stats = $this->repository->getFeedbackStats();
        $totalFeedbackCount = $stats['total_count'] ?? 0;

        if ($totalFeedbackCount < $this->minimumFeedbackThreshold) {
            $this->logger->info('Not enough feedback for learning', [
                'feedback_count' => $totalFeedbackCount,
                'minimum_threshold' => $this->minimumFeedbackThreshold,
            ]);

            return [
                'success' => false,
                'message' => 'Not enough feedback for learning',
                'feedback_count' => $totalFeedbackCount,
                'minimum_threshold' => $this->minimumFeedbackThreshold,
            ];
        }

        // Process false positives
        $falsePositives = $this->repository->getFeedbackByFeedbackType(
            FeedbackType::FALSE_POSITIVE,
            Timestamp::fromString('-30 days')
        );

        $falsePositiveAdjustments = $this->processfalsePositives($falsePositives);

        // Process false negatives
        $falseNegatives = $this->repository->getFeedbackByFeedbackType(
            FeedbackType::FALSE_NEGATIVE,
            Timestamp::fromString('-30 days')
        );

        $falseNegativeAdjustments = $this->processFalseNegatives($falseNegatives);

        // Process severity adjustments
        $severityAdjustments = $this->repository->getFeedbackByFeedbackType(
            FeedbackType::SEVERITY_ADJUSTMENT,
            Timestamp::fromString('-30 days')
        );

        $severityAdjustmentResults = $this->processSeverityAdjustments($severityAdjustments);

        // Combine all adjustments
        $allAdjustments = array_merge(
            $falsePositiveAdjustments,
            $falseNegativeAdjustments,
            $severityAdjustmentResults['adjustments'] ?? []
        );

        // Apply adjustments to the ML engine
        if (! empty($allAdjustments)) {
            $this->applyModelAdjustments($allAdjustments);
        }

        $endTime = $this->clock->now();
        $duration = $endTime->getTimestamp() - $startTime->getTimestamp();

        $this->logger->info('Completed WAF feedback learning process', [
            'duration_seconds' => $duration,
            'false_positives_processed' => count($falsePositives),
            'false_negatives_processed' => count($falseNegatives),
            'severity_adjustments_processed' => count($severityAdjustments),
            'total_adjustments_applied' => count($allAdjustments),
        ]);

        return [
            'success' => true,
            'duration_seconds' => $duration,
            'false_positives_processed' => count($falsePositives),
            'false_negatives_processed' => count($falseNegatives),
            'severity_adjustments_processed' => count($severityAdjustments),
            'total_adjustments_applied' => count($allAdjustments),
            'adjustment_details' => [
                'false_positives' => $falsePositiveAdjustments,
                'false_negatives' => $falseNegativeAdjustments,
                'severity_adjustments' => $severityAdjustmentResults,
            ],
        ];
    }

    /**
     * Process false positive feedback to generate model adjustments
     *
     * @param DetectionFeedback[] $falsePositives Array of false positive feedback
     * @return array<string, ModelAdjustment> Model adjustments keyed by adjustment ID
     */
    private function processfalsePositives(array $falsePositives): array
    {
        if (empty($falsePositives)) {
            return [];
        }

        $this->logger->info('Processing false positive feedback', [
            'count' => count($falsePositives),
        ]);

        $adjustments = [];
        $categoryFrequency = [];

        // Count frequency by category
        foreach ($falsePositives as $feedback) {
            $categoryKey = $feedback->category->value;
            $categoryFrequency[$categoryKey] = ($categoryFrequency[$categoryKey] ?? 0) + 1;
        }

        // Generate adjustments for categories with multiple false positives
        foreach ($categoryFrequency as $category => $count) {
            if ($count >= 3) { // Require at least 3 false positives in a category
                $detectionCategory = DetectionCategory::from($category);
                $adjustmentFactor = min(0.8, $count / 20); // Cap at 0.8

                // Apply learning rate
                $adjustmentFactor *= $this->learningRate;

                $adjustmentId = 'fp_' . $category . '_' . $this->clock->now()->getTimestamp();

                $adjustments[$adjustmentId] = new ModelAdjustment(
                    id: $adjustmentId,
                    category: $detectionCategory,
                    thresholdAdjustment: Percentage::from($adjustmentFactor * 100),
                    confidenceAdjustment: Percentage::from(-$adjustmentFactor * 100),
                    featureWeightAdjustments: $this->generateFeatureWeightAdjustments($detectionCategory, true),
                    description: "Adjustment based on {$count} false positive reports for {$category}",
                    timestamp: Timestamp::fromClock($this->clock)
                );

                $this->logger->info('Created false positive adjustment', [
                    'category' => $category,
                    'count' => $count,
                    'adjustment_factor' => $adjustmentFactor,
                    'adjustment_id' => $adjustmentId,
                ]);
            }
        }

        return $adjustments;
    }

    /**
     * Process false negative feedback to generate model adjustments
     *
     * @param DetectionFeedback[] $falseNegatives Array of false negative feedback
     * @return array<string, ModelAdjustment> Model adjustments keyed by adjustment ID
     */
    private function processFalseNegatives(array $falseNegatives): array
    {
        if (empty($falseNegatives)) {
            return [];
        }

        $this->logger->info('Processing false negative feedback', [
            'count' => count($falseNegatives),
        ]);

        $adjustments = [];
        $categoryFrequency = [];

        // Count frequency by category
        foreach ($falseNegatives as $feedback) {
            $categoryKey = $feedback->category->value;
            $categoryFrequency[$categoryKey] = ($categoryFrequency[$categoryKey] ?? 0) + 1;
        }

        // Generate adjustments for categories with multiple false negatives
        foreach ($categoryFrequency as $category => $count) {
            if ($count >= 2) { // Require at least 2 false negatives in a category
                $detectionCategory = DetectionCategory::from($category);
                $adjustmentFactor = min(0.7, $count / 15); // Cap at 0.7

                // Apply learning rate
                $adjustmentFactor *= $this->learningRate;

                $adjustmentId = 'fn_' . $category . '_' . $this->clock->now()->getTimestamp();

                $adjustments[$adjustmentId] = new ModelAdjustment(
                    id: $adjustmentId,
                    category: $detectionCategory,
                    thresholdAdjustment: Percentage::from(-$adjustmentFactor * 100),
                    confidenceAdjustment: Percentage::from($adjustmentFactor * 100),
                    featureWeightAdjustments: $this->generateFeatureWeightAdjustments($detectionCategory, false),
                    description: "Adjustment based on {$count} false negative reports for {$category}",
                    timestamp: Timestamp::fromClock($this->clock)
                );

                $this->logger->info('Created false negative adjustment', [
                    'category' => $category,
                    'count' => $count,
                    'adjustment_factor' => $adjustmentFactor,
                    'adjustment_id' => $adjustmentId,
                ]);
            }
        }

        return $adjustments;
    }

    /**
     * Process severity adjustment feedback
     *
     * @param DetectionFeedback[] $severityAdjustments Array of severity adjustment feedback
     * @return array<string, mixed> Results of processing severity adjustments
     */
    private function processSeverityAdjustments(array $severityAdjustments): array
    {
        if (empty($severityAdjustments)) {
            return ['adjustments' => [], 'severity_changes' => []];
        }

        $this->logger->info('Processing severity adjustment feedback', [
            'count' => count($severityAdjustments),
        ]);

        $adjustments = [];
        $severityChanges = [];
        $categoryAdjustments = [];

        // Group by category and analyze suggested severities
        foreach ($severityAdjustments as $feedback) {
            $categoryKey = $feedback->category->value;
            $suggestedSeverity = $feedback->getSuggestedSeverity();

            if ($suggestedSeverity === null) {
                continue;
            }

            if (! isset($categoryAdjustments[$categoryKey])) {
                $categoryAdjustments[$categoryKey] = [
                    'count' => 0,
                    'severities' => [],
                ];
            }

            $categoryAdjustments[$categoryKey]['count']++;
            $categoryAdjustments[$categoryKey]['severities'][$suggestedSeverity->value] =
                ($categoryAdjustments[$categoryKey]['severities'][$suggestedSeverity->value] ?? 0) + 1;
        }

        // Find consensus for each category
        foreach ($categoryAdjustments as $category => $data) {
            if ($data['count'] < 3) {
                continue; // Not enough data
            }

            // Find most suggested severity
            $mostSuggestedSeverity = null;
            $mostSuggestedCount = 0;

            foreach ($data['severities'] as $severity => $count) {
                if ($count > $mostSuggestedCount) {
                    $mostSuggestedSeverity = $severity;
                    $mostSuggestedCount = $count;
                }
            }

            // Check if there's a clear consensus (more than 50% of suggestions)
            if ($mostSuggestedCount > ($data['count'] / 2)) {
                $detectionCategory = DetectionCategory::from($category);
                $suggestedSeverity = DetectionSeverity::from($mostSuggestedSeverity);
                $defaultSeverity = $detectionCategory->getDefaultSeverity();

                // Only adjust if different from default
                if ($suggestedSeverity !== $defaultSeverity) {
                    $severityChanges[$category] = [
                        'from' => $defaultSeverity->value,
                        'to' => $suggestedSeverity->value,
                        'consensus_percentage' => round(($mostSuggestedCount / $data['count']) * 100, 1),
                    ];

                    // Create adjustment based on severity change
                    $adjustmentId = 'sev_' . $category . '_' . $this->clock->now()->getTimestamp();

                    // Determine confidence adjustment based on severity change
                    $confidenceAdjustment = match(true) {
                        $suggestedSeverity->value > $defaultSeverity->value => Percentage::from(15.0 * $this->learningRate),
                        $suggestedSeverity->value < $defaultSeverity->value => Percentage::from(-15.0 * $this->learningRate),
                        default => Percentage::from(0.0)
                    };

                    $adjustments[$adjustmentId] = new ModelAdjustment(
                        id: $adjustmentId,
                        category: $detectionCategory,
                        thresholdAdjustment: Percentage::from(0.0),
                        confidenceAdjustment: $confidenceAdjustment,
                        featureWeightAdjustments: [],
                        description: "Severity adjustment from {$defaultSeverity->value} to {$suggestedSeverity->value} for {$category}",
                        timestamp: Timestamp::fromClock($this->clock)
                    );

                    $this->logger->info('Created severity adjustment', [
                        'category' => $category,
                        'from_severity' => $defaultSeverity->value,
                        'to_severity' => $suggestedSeverity->value,
                        'consensus_percentage' => round(($mostSuggestedCount / $data['count']) * 100, 1),
                        'adjustment_id' => $adjustmentId,
                    ]);
                }
            }
        }

        return [
            'adjustments' => $adjustments,
            'severity_changes' => $severityChanges,
        ];
    }

    /**
     * Generate feature weight adjustments for a specific category
     *
     * @param DetectionCategory $category The detection category
     * @param bool $isfalsePositive Whether this is for false positive (true) or false negative (false)
     * @return array<string, float> Feature weight adjustments
     */
    private function generateFeatureWeightAdjustments(DetectionCategory $category, bool $isfalsePositive): array
    {
        // This is a simplified implementation - in a real system, this would analyze
        // the specific features that contributed to false positives/negatives

        $adjustments = [];

        // Get category-specific features based on detection type
        $categoryFeatures = match($category) {
            DetectionCategory::SQL_INJECTION => [
                'sql_keywords_count' => $isfalsePositive ? -0.2 : 0.15,
                'special_chars_ratio' => $isfalsePositive ? -0.25 : 0.2,
                'parameter_entropy' => $isfalsePositive ? -0.15 : 0.1,
            ],
            DetectionCategory::XSS => [
                'script_tag_count' => $isfalsePositive ? -0.3 : 0.25,
                'event_handler_count' => $isfalsePositive ? -0.25 : 0.2,
                'url_encoding_depth' => $isfalsePositive ? -0.2 : 0.15,
            ],
            DetectionCategory::PATH_TRAVERSAL => [
                'directory_traversal_sequences' => $isfalsePositive ? -0.35 : 0.3,
                'path_separator_count' => $isfalsePositive ? -0.2 : 0.15,
            ],
            DetectionCategory::COMMAND_INJECTION => [
                'command_delimiter_count' => $isfalsePositive ? -0.3 : 0.25,
                'shell_command_count' => $isfalsePositive ? -0.35 : 0.3,
            ],
            default => [
                'anomaly_score' => $isfalsePositive ? -0.15 : 0.1,
                'request_complexity' => $isfalsePositive ? -0.1 : 0.05,
            ]
        };

        // Apply learning rate to all adjustments
        foreach ($categoryFeatures as $feature => $adjustment) {
            $adjustments[$feature] = $adjustment * $this->learningRate;
        }

        return $adjustments;
    }

    /**
     * Apply model adjustments to the machine learning engine
     *
     * @param array<string, ModelAdjustment> $adjustments The adjustments to apply
     * @return array<string, mixed> Results of applying the adjustments
     */
    private function applyModelAdjustments(array $adjustments): array
    {
        if (empty($adjustments)) {
            return [
                'success' => true,
                'applied_count' => 0,
                'message' => 'No adjustments to apply',
            ];
        }

        $this->logger->info('Applying model adjustments', [
            'count' => count($adjustments),
        ]);

        // Apply adjustments to the machine learning engine
        $result = $this->mlEngine->applyFeedbackAdjustments($adjustments);

        // Log the results
        if ($result['success']) {
            $this->logger->info('Successfully applied model adjustments', [
                'applied_count' => $result['applied_count'],
                'failed_count' => $result['failed_count'] ?? 0,
            ]);
        } else {
            $this->logger->warning('Failed to apply some model adjustments', [
                'applied_count' => $result['applied_count'],
                'failed_count' => $result['failed_count'] ?? 0,
            ]);

            // Log details of failed adjustments
            if (isset($result['results'])) {
                foreach ($result['results'] as $id => $adjustmentResult) {
                    if (! $adjustmentResult['success']) {
                        $this->logger->warning('Failed to apply adjustment', [
                            'id' => $id,
                            'message' => $adjustmentResult['message'] ?? 'Unknown error',
                        ]);
                    }
                }
            }
        }

        return $result;
    }

    /**
     * Get the minimum feedback threshold
     *
     * @return int The minimum feedback threshold
     */
    public function getMinimumFeedbackThreshold(): int
    {
        return $this->minimumFeedbackThreshold;
    }

    /**
     * Get the learning rate
     *
     * @return float The learning rate
     */
    public function getLearningRate(): float
    {
        return $this->learningRate;
    }

    /**
     * Create a new instance with a different minimum feedback threshold
     *
     * @param int $threshold The new threshold
     * @return self
     */
    public function withMinimumFeedbackThreshold(int $threshold): self
    {
        return new self(
            $this->repository,
            $this->mlEngine,
            $this->clock,
            $this->logger,
            $threshold,
            $this->learningRate
        );
    }

    /**
     * Create a new instance with a different learning rate
     *
     * @param float $rate The new learning rate (0.0-1.0)
     * @return self
     */
    public function withLearningRate(float $rate): self
    {
        $clampedRate = max(0.0, min(1.0, $rate));

        return new self(
            $this->repository,
            $this->mlEngine,
            $this->clock,
            $this->logger,
            $this->minimumFeedbackThreshold,
            $clampedRate
        );
    }
}
