<?php

declare(strict_types=1);

namespace App\Framework\Discovery\Processing;

use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\Discovery\Events\ChunkEventType;
use App\Framework\Discovery\Events\ChunkProcessingEvent;
use App\Framework\Discovery\Memory\BatchParameters;
use App\Framework\Discovery\Memory\DiscoveryMemoryManager;
use App\Framework\Discovery\Memory\GuardResult;
use App\Framework\Discovery\Memory\MemoryGuard;
use App\Framework\Discovery\Memory\MemoryStatus;
use App\Framework\Discovery\ValueObjects\MemoryStrategy;
use App\Framework\Filesystem\File;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;

/**
 * Adaptive chunking system for Discovery operations
 *
 * Intelligently partitions files into processing chunks based on:
 * - Current memory pressure
 * - File sizes and complexity
 * - Memory strategy configuration
 * - Historical performance data
 */
final class AdaptiveChunker
{
    private array $performanceHistory = [];

    private int $processedChunks = 0;

    public function __construct(
        private readonly DiscoveryMemoryManager $memoryManager,
        private readonly MemoryGuard $memoryGuard,
        private readonly ?Logger $logger = null,
        private readonly ?EventDispatcher $eventDispatcher = null,
        private readonly ?Clock $clock = null
    ) {
    }

    /**
     * Create processing chunks from file list with adaptive sizing
     */
    public function createChunks(array $files): ChunkCollection
    {
        if (empty($files)) {
            return new ChunkCollection([]);
        }

        // Estimate file sizes and complexity
        $fileAnalysis = $this->analyzeFiles($files);

        // Calculate optimal chunk parameters
        $batchParams = $this->memoryManager->calculateBatchParameters(
            count($files),
            $fileAnalysis->averageFileSize
        );

        // Create chunks with adaptive sizing
        $chunks = $this->createAdaptiveChunks($files, $fileAnalysis, $batchParams);

        $this->logger?->debug('Created adaptive chunks', LogContext::withData([
            'total_files' => count($files),
            'chunk_count' => count($chunks),
            'average_chunk_size' => $this->calculateAverageChunkSize($chunks),
            'strategy' => $batchParams->strategy->value,
            'estimated_memory_per_chunk' => $batchParams->estimatedMemoryPerBatch->toHumanReadable(),
        ]));

        return new ChunkCollection($chunks);
    }

    /**
     * Process chunks with adaptive monitoring and adjustment
     */
    public function processChunks(
        ChunkCollection $chunks,
        callable $processor,
        ?callable $progressCallback = null
    ): ProcessingResult {
        $results = [];
        $processedFiles = 0;
        $totalFiles = $chunks->getTotalFileCount();
        $startTime = microtime(true);
        $startMemory = Byte::fromBytes(memory_get_usage(true));

        foreach ($chunks->getChunks() as $chunkIndex => $chunk) {
            // Emit chunk started event
            $this->emitChunkEvent(
                ChunkEventType::STARTED,
                $chunkIndex,
                count($chunks),
                $chunk,
                $processedFiles,
                $totalFiles
            );

            // Check memory status before processing chunk
            $guardResult = $this->memoryGuard->check();

            // Only stop processing if we're in CRITICAL memory state and have processed some chunks already
            // This prevents stopping too early and allows processing to continue under normal memory pressure
            if (! $this->memoryGuard->isSafeToProcess() &&
                $guardResult->memoryStatus->status === MemoryStatus::CRITICAL &&
                $chunkIndex > 0) {
                $this->logger?->warning('Stopping chunk processing due to CRITICAL memory constraints', LogContext::withData([
                    'chunk_index' => $chunkIndex,
                    'processed_files' => $processedFiles,
                    'memory_status' => $guardResult->memoryStatus->toArray(),
                ]));

                break;
            }

            // Adjust chunk size if needed based on current conditions
            $adjustedChunk = $this->adjustChunkIfNeeded($chunk, $guardResult);

            // Emit memory adjustment event if chunk was adjusted
            if ($adjustedChunk->getFileCount() !== $chunk->getFileCount()) {
                $this->emitChunkEvent(
                    ChunkEventType::MEMORY_ADJUSTED,
                    $chunkIndex,
                    count($chunks),
                    $adjustedChunk,
                    $processedFiles,
                    $totalFiles
                );
            }

            // Process the chunk
            $chunkStartTime = microtime(true);
            $chunkStartMemory = Byte::fromBytes(memory_get_usage(true));

            try {
                $chunkResult = $processor($adjustedChunk);
                $results[] = $chunkResult;

                $processedFiles += $adjustedChunk->getFileCount();
                $this->processedChunks++;

                $chunkProcessingTime = microtime(true) - $chunkStartTime;
                $chunkMemoryUsed = Byte::fromBytes(memory_get_usage(true))->subtract($chunkStartMemory);

                // Record performance data
                $this->recordChunkPerformance($adjustedChunk, $chunkProcessingTime, $chunkMemoryUsed);

                // Emit chunk completed event
                $this->emitChunkEvent(
                    ChunkEventType::COMPLETED,
                    $chunkIndex,
                    count($chunks),
                    $adjustedChunk,
                    $processedFiles,
                    $totalFiles,
                    Duration::fromSeconds($chunkProcessingTime),
                    $chunkMemoryUsed
                );

                // Progress callback
                if ($progressCallback) {
                    $progressCallback($processedFiles, $totalFiles, $chunkIndex + 1, count($chunks));
                }

            } catch (\Throwable $e) {
                $this->logger?->error('Chunk processing failed', LogContext::withData([
                    'chunk_index' => $chunkIndex,
                    'chunk_size' => $adjustedChunk->getFileCount(),
                    'error' => $e->getMessage(),
                ]));

                // Emit chunk failed event
                $this->emitChunkEvent(
                    ChunkEventType::FAILED,
                    $chunkIndex,
                    count($chunks),
                    $adjustedChunk,
                    $processedFiles,
                    $totalFiles
                );

                // Continue with next chunk on error
                continue;
            }

            // Cleanup if needed
            if ($this->memoryManager->shouldCleanup($this->processedChunks)) {
                $cleanupResult = $this->memoryManager->performCleanup(
                    isEmergency: false,
                    triggerReason: 'Scheduled chunk cleanup',
                    context: "chunk_{$chunkIndex}"
                );
                $this->logger?->debug('Performed chunk cleanup', LogContext::withData([
                    'memory_freed' => $cleanupResult->memoryFreed->toHumanReadable(),
                    'collected_cycles' => $cleanupResult->collectedCycles,
                ]));
            }
        }

        $endTime = microtime(true);
        $endMemory = Byte::fromBytes(memory_get_usage(true));

        return new ProcessingResult(
            processedFiles: $processedFiles,
            totalFiles: $totalFiles,
            processedChunks: $this->processedChunks,
            totalChunks: count($chunks),
            processingTime: $endTime - $startTime,
            memoryUsed: $endMemory->subtract($startMemory),
            results: $results
        );
    }

    /**
     * Analyze files to determine optimal chunking strategy
     */
    private function analyzeFiles(array $files): FileAnalysis
    {
        $fileSizes = [];
        $totalSize = Byte::zero();
        $complexityScores = [];

        foreach ($files as $file) {
            /** @var File $file */
            $size = $file->getSize();
            $fileSizes[] = $size;
            $totalSize = $totalSize->add($size);

            // Estimate complexity based on file size and extension
            $complexityScores[] = $this->estimateFileComplexity($file);
        }

        $fileCount = count($files);
        $averageSize = $fileCount > 0 ? $totalSize->divide($fileCount) : Byte::zero();
        $averageComplexity = $fileCount > 0 ? array_sum($complexityScores) / $fileCount : 0.0;

        // Calculate size distribution
        $sizeBytes = array_map(fn (Byte $size) => $size->toBytes(), $fileSizes);
        sort($sizeBytes);

        $medianSize = $fileCount > 0
            ? Byte::fromBytes($sizeBytes[intval($fileCount / 2)])
            : Byte::zero();

        $maxSize = $fileCount > 0
            ? Byte::fromBytes(max($sizeBytes))
            : Byte::zero();

        return new FileAnalysis(
            totalFiles: $fileCount,
            totalSize: $totalSize,
            averageFileSize: $averageSize,
            medianFileSize: $medianSize,
            maxFileSize: $maxSize,
            averageComplexity: $averageComplexity,
            fileSizes: $fileSizes
        );
    }

    /**
     * Create adaptive chunks based on analysis and memory constraints
     */
    private function createAdaptiveChunks(
        array $files,
        FileAnalysis $analysis,
        BatchParameters $batchParams
    ): array {
        $chunks = [];
        $currentChunk = [];
        $currentChunkSize = Byte::zero();
        $currentComplexity = 0.0;

        // Sort files by size (largest first for better distribution)
        $sortedFiles = $files;
        usort($sortedFiles, fn (File $a, File $b) => $b->getSize()->toBytes() <=> $a->getSize()->toBytes());

        foreach ($sortedFiles as $file) {
            $fileSize = $file->getSize();
            $fileComplexity = $this->estimateFileComplexity($file);

            // Check if adding this file would exceed chunk limits
            $wouldExceedSize = $currentChunkSize->add($fileSize)->greaterThan($batchParams->estimatedMemoryPerBatch);
            $wouldExceedCount = count($currentChunk) >= $batchParams->chunkSize;
            $wouldExceedComplexity = ($currentComplexity + $fileComplexity) > $this->getMaxComplexityPerChunk();

            // Start new chunk if limits would be exceeded
            if (($wouldExceedSize || $wouldExceedCount || $wouldExceedComplexity) && ! empty($currentChunk)) {
                $chunks[] = new ProcessingChunk($currentChunk, $currentChunkSize, $currentComplexity);
                $currentChunk = [];
                $currentChunkSize = Byte::zero();
                $currentComplexity = 0.0;
            }

            // Add file to current chunk
            $currentChunk[] = $file;
            $currentChunkSize = $currentChunkSize->add($fileSize);
            $currentComplexity += $fileComplexity;
        }

        // Add final chunk if not empty
        if (! empty($currentChunk)) {
            $chunks[] = new ProcessingChunk($currentChunk, $currentChunkSize, $currentComplexity);
        }

        return $chunks;
    }

    /**
     * Adjust chunk size based on current memory conditions
     */
    private function adjustChunkIfNeeded(ProcessingChunk $chunk, GuardResult $guardResult): ProcessingChunk
    {
        // No adjustment needed if memory is normal
        if ($guardResult->memoryStatus->status === MemoryStatus::NORMAL) {
            return $chunk;
        }

        // Reduce chunk size under memory pressure
        $files = $chunk->getFiles();
        $reductionFactor = match ($guardResult->memoryStatus->status) {
            MemoryStatus::WARNING => 0.7,  // Reduce to 70%
            MemoryStatus::CRITICAL => 0.5, // Reduce to 50%
            default => 1.0
        };

        $targetSize = (int) (count($files) * $reductionFactor);
        $targetSize = max(1, $targetSize); // At least 1 file

        if ($targetSize < count($files)) {
            $adjustedFiles = array_slice($files, 0, $targetSize);

            $this->logger?->info('Adjusted chunk size due to memory pressure', LogContext::withData([
                'original_size' => count($files),
                'adjusted_size' => $targetSize,
                'memory_status' => $guardResult->memoryStatus->status->value,
            ]));

            return ProcessingChunk::create($adjustedFiles);
        }

        return $chunk;
    }

    /**
     * Estimate file processing complexity
     */
    private function estimateFileComplexity(File $file): float
    {
        $size = $file->getSize()->toBytes();
        $extension = strtolower($file->getExtension());

        // Base complexity on file size (larger files are more complex)
        $baseComplexity = min($size / 1024 / 1024, 10.0); // Max 10 for very large files

        // Adjust based on file type
        $typeMultiplier = match ($extension) {
            'php' => 1.5,  // PHP files need parsing and reflection
            'js', 'ts' => 1.3,  // JavaScript/TypeScript complexity
            'html', 'twig' => 1.2, // Template files
            'json', 'xml' => 1.1,  // Structured data
            'txt', 'md' => 0.8,    // Simple text files
            default => 1.0
        };

        return $baseComplexity * $typeMultiplier;
    }

    /**
     * Get maximum complexity per chunk based on memory strategy
     */
    private function getMaxComplexityPerChunk(): float
    {
        $memoryStatus = $this->memoryManager->getMemoryStatus();

        return match ($memoryStatus->strategy) {
            MemoryStrategy::STREAMING => 5.0,
            MemoryStrategy::CONSERVATIVE => 15.0,
            MemoryStrategy::BATCH => 25.0,
            MemoryStrategy::ADAPTIVE => 20.0,
            MemoryStrategy::AGGRESSIVE => 40.0
        };
    }

    /**
     * Record performance data for future optimization
     */
    private function recordChunkPerformance(ProcessingChunk $chunk, float $processingTime, Byte $memoryUsed): void
    {
        $this->performanceHistory[] = [
            'chunk_size' => $chunk->getFileCount(),
            'chunk_complexity' => $chunk->getComplexity(),
            'processing_time' => $processingTime,
            'memory_used' => $memoryUsed->toBytes(),
            'timestamp' => time(),
        ];

        // Keep only recent history
        if (count($this->performanceHistory) > 50) {
            $this->performanceHistory = array_slice($this->performanceHistory, -25);
        }
    }

    /**
     * Calculate average chunk size from chunks array
     */
    private function calculateAverageChunkSize(array $chunks): float
    {
        if (empty($chunks)) {
            return 0.0;
        }

        $totalFiles = array_sum(array_map(fn (ProcessingChunk $chunk) => $chunk->getFileCount(), $chunks));

        return $totalFiles / count($chunks);
    }

    /**
     * Get performance statistics
     */
    public function getPerformanceStatistics(): array
    {
        if (empty($this->performanceHistory)) {
            return [];
        }

        $times = array_column($this->performanceHistory, 'processing_time');
        $memories = array_column($this->performanceHistory, 'memory_used');
        $sizes = array_column($this->performanceHistory, 'chunk_size');

        return [
            'total_chunks_processed' => $this->processedChunks,
            'average_processing_time' => array_sum($times) / count($times),
            'average_memory_usage' => Byte::fromBytes((int) (array_sum($memories) / count($memories)))->toHumanReadable(),
            'average_chunk_size' => array_sum($sizes) / count($sizes),
            'history_entries' => count($this->performanceHistory),
        ];
    }

    /**
     * Emit chunk processing event
     */
    private function emitChunkEvent(
        ChunkEventType $eventType,
        int $chunkIndex,
        int $totalChunks,
        ProcessingChunk $chunk,
        int $processedFiles,
        int $totalFiles,
        ?Duration $processingTime = null,
        ?Byte $chunkMemoryUsage = null
    ): void {
        if (! $this->eventDispatcher || ! $this->clock) {
            return;
        }

        $this->eventDispatcher->dispatch(new ChunkProcessingEvent(
            eventType: $eventType,
            chunkIndex: $chunkIndex,
            totalChunks: $totalChunks,
            chunkSize: $chunk->getFileCount(),
            processedFiles: $processedFiles,
            totalFiles: $totalFiles,
            chunkMemoryUsage: $chunkMemoryUsage ?? Byte::zero(),
            chunkComplexity: $chunk->getComplexity(),
            processingTime: $processingTime,
            strategy: $this->memoryManager->getCurrentStrategy(),
            context: "discovery_chunking",
            timestamp: $this->clock->time()
        ));
    }
}
