<?php
declare(strict_types=1);

namespace App\Framework\Discovery;

use App\Framework\Async1\AsyncFileScanner;
use App\Framework\Cache\Cache;
use App\Framework\Core\ClassParser;
use App\Framework\Core\PathProvider;
use App\Framework\Core\ProgressMeter;
use SplFileInfo;

/**
 * Zentraler Service für das Scannen von Dateien und die Verteilung an Visitors
 */
final class FileScannerService
{
    /** @var array<FileVisitor> */
    private array $visitors = [];

    /** @var array Gespeicherte Liste gescannter Dateien für Inkrementelle Scans */
    private array $scannedFiles = [];

    public function __construct(
        private readonly FileScannerInterface $fileScanner,
        private readonly PathProvider $pathProvider,
        private readonly Cache $cache,
        private readonly bool $useCache = true,
        private readonly bool $asyncProcessing = false,
        private readonly int $chunkSize = 100
    ) {}

    /**
     * Registriert einen Besucher, der Dateien verarbeiten kann
     */
    public function registerVisitor(FileVisitor $visitor): self
    {
        $this->visitors[] = $visitor;
        return $this;
    }

    /**
     * Gibt die Anzahl der verarbeiteten Dateien zurück
     */
    public function getProcessedFileCount(): int
    {
        return count($this->scannedFiles);
    }

    /**
     * Führt den Scan durch und ruft alle registrierten Besucher auf
     *
     * @param bool $showProgress Zeigt einen Fortschrittsindikator an (nur für CLI)
     */
    public function scan(bool $showProgress = false): void
    {
        // Prüfen, ob wir ein Rescan benötigen
        if ($this->useCache && !$this->shouldRescan()) {
            $this->loadVisitorsFromCache();
            return;
        }

        // Prüfen, ob ein inkrementeller Scan ausreicht
        if ($this->useCache && $this->canUseIncrementalScan()) {
            $this->incrementalScan($showProgress);
            return;
        }

        $basePath = $this->pathProvider->getBasePath();
        $phpFiles = $this->fileScanner->findFiles($basePath . '/src', '*.php');

        // Speichere die Liste der gescannten Dateien
        $this->scannedFiles = [];
        foreach ($phpFiles as $file) {
            $this->scannedFiles[$file->getPathname()] = $file->getMTime();
        }

        // Notifiziere alle Besucher über den Beginn des Scans
        foreach ($this->visitors as $visitor) {
            $visitor->onScanStart();
        }

        // Wenn zu viele Dateien, verarbeite in Chunks
        if (count($phpFiles) > $this->chunkSize) {
            $this->scanInChunks($phpFiles, $showProgress);
        } else {
            $progress = $showProgress ? new ProgressMeter(count($phpFiles)) : null;

            // Verarbeite synchron oder asynchron
            if ($this->asyncProcessing && $this->fileScanner instanceof AsyncFileScanner) {
                $this->processFilesAsync($phpFiles);
            } else {
                // Verarbeite jede Datei
                foreach ($phpFiles as $file) {
                    $this->processFile($file);
                    if ($progress) {
                        $progress->advance();
                    }
                }
            }

            $progress?->finish();
        }

        // Notifiziere alle Besucher über das Ende des Scans
        foreach ($this->visitors as $visitor) {
            $visitor->onScanComplete();
        }

        // Cache aktualisieren
        if ($this->useCache) {
            $this->updateVisitorCache();
        }
    }

    /**
     * Führt einen inkrementellen Scan durch (nur geänderte Dateien)
     *
     * @param bool $showProgress Zeigt einen Fortschrittsindikator an (nur für CLI)
     */
    public function incrementalScan(bool $showProgress = false): void
    {
        $basePath = $this->pathProvider->getBasePath();
        $lastScanTime = $this->getLastScanTime();

        $changedFiles = $this->fileScanner->findChangedFiles($basePath . '/src', $lastScanTime);

        if (empty($changedFiles)) {
            return; // Keine Änderungen seit dem letzten Scan
        }

        // Lade existierende Daten aus dem Cache
        $this->loadVisitorsFromCache();

        // Notifiziere Besucher über den Beginn des inkrementellen Scans
        foreach ($this->visitors as $visitor) {
            $visitor->onIncrementalScanStart();
        }

        $progress = $showProgress ? new ProgressMeter(count($changedFiles)) : null;

        // Verarbeite nur geänderte Dateien
        foreach ($changedFiles as $file) {
            $this->processFile($file);
            $this->scannedFiles[$file->getPathname()] = $file->getMTime();

            if ($progress) {
                $progress->advance();
            }
        }

        if ($progress) {
            $progress->finish();
        }

        // Notifiziere Besucher über das Ende des inkrementellen Scans
        foreach ($this->visitors as $visitor) {
            $visitor->onIncrementalScanComplete();
        }

        // Cache aktualisieren
        if ($this->useCache) {
            $this->updateVisitorCache();
        }
    }

    /**
     * Scannt das Projekt in Chunks, um Speicherverbrauch zu reduzieren
     *
     * @param array $phpFiles Die zu verarbeitenden Dateien
     * @param bool $showProgress Zeigt einen Fortschrittsindikator an
     */
    private function scanInChunks(array $phpFiles, bool $showProgress = false): void
    {
        $chunks = array_chunk($phpFiles, $this->chunkSize);
        $totalFiles = count($phpFiles);
        $progress = $showProgress ? new ProgressMeter($totalFiles) : null;
        $processedFiles = 0;

        foreach ($chunks as $chunk) {
            // Verarbeite den Chunk
            foreach ($chunk as $file) {
                $this->processFile($file);
                $processedFiles++;

                if ($progress) {
                    $progress->advance();
                }
            }

            // Speicher freigeben
            gc_collect_cycles();
        }

        if ($progress) {
            $progress->finish();
        }
    }

    /**
     * Verarbeitet eine einzelne Datei mit allen registrierten Besuchern
     */
    private function processFile(SplFileInfo $file): void
    {
        $filePath = $file->getPathname();

        // Versuche, die Datei zu parsen
        try {
            $classes = ClassParser::getClassesInFile($filePath);

            foreach ($classes as $class) {
                // Notifiziere alle Besucher über diese Klasse
                foreach ($this->visitors as $visitor) {
                    $visitor->visitClass($class, $filePath);
                }
            }
        } catch (\Throwable $e) {
            // Logging des Fehlers
            error_log("Fehler beim Parsen von {$filePath}: " . $e->getMessage());
        }
    }

    /**
     * Verarbeitet Dateien asynchron, wenn asyncProcessing aktiviert ist
     */
    private function processFilesAsync(array $files): void
    {
        if (!$this->asyncProcessing || empty($files)) {
            // Verarbeite synchron, wenn asyncProcessing deaktiviert ist
            foreach ($files as $file) {
                $this->processFile($file);
            }
            return;
        }

        // Stelle sicher, dass fileScanner AsyncFileScanner ist
        if (!$this->fileScanner instanceof AsyncFileScanner) {
            throw new \RuntimeException('Asynchrone Verarbeitung erfordert AsyncFileScanner');
        }

        // Verteile die Dateien auf Chunks für parallele Verarbeitung
        $chunks = array_chunk($files, (int)ceil(count($files) / 8));

        // Erstelle Tasks für jeden Chunk
        $tasks = [];
        foreach ($chunks as $index => $chunk) {
            $tasks[$index] = function () use ($chunk) {
                $results = [];
                foreach ($chunk as $file) {
                    try {
                        $classes = ClassParser::getClassesInFile($file->getPathname());
                        foreach ($classes as $class) {
                            $results[] = ['class' => $class, 'file' => $file->getPathname()];
                        }
                    } catch (\Throwable $e) {
                        error_log("Fehler beim Parsen von {$file->getPathname()}: " . $e->getMessage());
                    }
                }
                return $results;
            };
        }

        // Verarbeite die Tasks asynchron
        $results = $this->fileScanner->getTaskProcessor()->processTasks($tasks);

        // Ergebnisse verarbeiten
        foreach ($results as $chunkResults) {
            if (!is_array($chunkResults)) {
                continue;
            }

            foreach ($chunkResults as $result) {
                foreach ($this->visitors as $visitor) {
                    $visitor->visitClass($result['class'], $result['file']);
                }
            }
        }
    }

    /**
     * Prüft, ob ein erneuter Scan erforderlich ist
     */
    private function shouldRescan(): bool
    {
        $cacheKey = 'file_scanner_timestamp';
        $cachedItem = $this->cache->get($cacheKey);

        if (!$cachedItem->isHit) {
            return true;
        }

        $cachedTime = $cachedItem->value;
        $basePath = $this->pathProvider->getBasePath();
        $latestMTime = $this->fileScanner->getLatestMTime($basePath . '/src');

        return $latestMTime > $cachedTime;
    }

    /**
     * Prüft, ob ein inkrementeller Scan verwendet werden kann
     */
    private function canUseIncrementalScan(): bool
    {
        // Prüfe, ob wir bereits gescannte Dateien haben
        $cachedItem = $this->cache->get('file_scanner_files');
        if (!$cachedItem->isHit) {
            return false;
        }

        $this->scannedFiles = $cachedItem->value;
        return !empty($this->scannedFiles);
    }

    /**
     * Gibt den Zeitpunkt des letzten Scans zurück
     */
    private function getLastScanTime(): int
    {
        $cacheKey = 'file_scanner_timestamp';
        $cachedItem = $this->cache->get($cacheKey);

        return $cachedItem->isHit ? $cachedItem->value : 0;
    }

    /**
     * Aktualisiert den Cache-Zeitstempel
     */
    private function updateCacheTimestamp(): void
    {
        $this->cache->set('file_scanner_timestamp', time(), 3600);
    }

    /**
     * Aktualisiert den Cache für alle Visitors
     */
    private function updateVisitorCache(): void
    {
        foreach ($this->visitors as $visitor) {
            $cacheData = $visitor->getCacheableData();
            if ($cacheData !== null) {
                $this->cache->set(
                    $visitor->getCacheKey(),
                    $cacheData,
                    3600 // TTL in Sekunden
                );
            }
        }

        // Zusätzlich zum Zeitstempel auch die Liste der gescannten Dateien cachen
        $this->cache->set('file_scanner_files', $this->scannedFiles, 3600);
        $this->updateCacheTimestamp();
    }

    /**
     * Lädt Daten für alle Visitors aus dem Cache
     */
    private function loadVisitorsFromCache(): void
    {
        foreach ($this->visitors as $visitor) {
            $visitor->loadFromCache($this->cache);
        }

        // Lade auch die Liste der gescannten Dateien
        $cachedItem = $this->cache->get('file_scanner_files');
        if ($cachedItem->isHit) {

            $this->scannedFiles = $cachedItem->value;
        }
    }

    /**
     * Gibt eine Liste aller registrierten Besucher zurück
     *
     * @return array<FileVisitor>
     */
    public function getVisitors(): array
    {
        return $this->visitors;
    }
}
