<?php

declare(strict_types=1);

namespace App\Framework\Logging;

use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Logging\ValueObjects\LogEntry;
use App\Framework\Logging\ValueObjects\LogName;
use App\Framework\Logging\ValueObjects\LogReadResult;
use App\Framework\Logging\ValueObjects\LogSearchResult;
use App\Framework\Logging\ValueObjects\LogViewerConfig;

/**
 * Log Viewer Service
 * Framework-compliant readonly implementation with Value Objects
 */
final readonly class LogViewer
{
    public function __construct(
        private LogViewerConfig $config
    ) {}

    /**
     * Get all available logs
     *
     * @return array<string, array{name: LogName, path: FilePath, size: string, modified: string, readable: bool}>
     */
    public function getAvailableLogs(): array
    {
        $logs = [];

        foreach ($this->config->logDirectories as $subdir) {
            $subdirPath = $this->config->getSubdirectoryPath($subdir);

            if (!is_dir($subdirPath)) {
                continue;
            }

            $files = glob($subdirPath . '/*.log');

            foreach ($files as $filePath) {
                try {
                    $path = FilePath::create($filePath);

                    if (!$path->isFile() || !$path->isReadable()) {
                        continue;
                    }

                    $filename = str_replace('.log', '', $path->getFilename());
                    $logName = LogName::fromString($subdir . '_' . $filename);

                    $logs[$logName->value] = [
                        'name' => $logName,
                        'path' => $path,
                        'size' => $path->getSize()->toHumanReadable(),
                        'modified' => date('Y-m-d H:i:s', $path->getModifiedTime()),
                        'readable' => $path->isReadable(),
                    ];
                } catch (\InvalidArgumentException $e) {
                    // Skip invalid paths
                    continue;
                }
            }
        }

        return $logs;
    }

    /**
     * Read log entries with filtering
     */
    public function readLog(
        LogName|string $logName,
        ?int $limit = null,
        LogLevel|string|null $level = null,
        ?string $search = null
    ): LogReadResult {
        // Normalize inputs
        $logName = $logName instanceof LogName ? $logName : LogName::fromString($logName);
        $limit = $limit ?? $this->config->defaultLimit;
        $levelFilter = $this->normalizeLevel($level);

        // Find log path
        $availableLogs = $this->getAvailableLogs();

        if (!isset($availableLogs[$logName->value])) {
            throw new \InvalidArgumentException("Log '{$logName->value}' not found");
        }

        $logPath = $availableLogs[$logName->value]['path'];

        // Read entries using generator
        $entries = [];
        $count = 0;

        foreach ($this->readLogEntriesGenerator($logPath, $levelFilter, $search) as $entry) {
            $entries[] = $entry;
            $count++;

            if ($count >= $limit) {
                break;
            }
        }

        return LogReadResult::fromEntries(
            logName: $logName,
            logPath: $logPath,
            entries: $entries,
            limit: $limit,
            search: $search,
            levelFilter: $levelFilter
        );
    }

    /**
     * Read last N lines from log (tail)
     */
    public function tailLog(LogName|string $logName, int $lines = 50): LogReadResult
    {
        $logName = $logName instanceof LogName ? $logName : LogName::fromString($logName);

        $availableLogs = $this->getAvailableLogs();

        if (!isset($availableLogs[$logName->value])) {
            throw new \InvalidArgumentException("Log '{$logName->value}' not found");
        }

        $logPath = $availableLogs[$logName->value]['path'];

        // Read entries using generator
        $entries = [];

        foreach ($this->readLogLinesGenerator($logPath, $lines) as $line) {
            $entry = $this->parseLogLine($line, $logPath);
            if ($entry) {
                $entries[] = $entry;
            }
        }

        // Reverse to show most recent first
        $entries = array_reverse($entries);

        return LogReadResult::fromEntries(
            logName: $logName,
            logPath: $logPath,
            entries: $entries,
            limit: $lines
        );
    }

    /**
     * Search across multiple logs
     */
    public function searchLogs(
        string $query,
        ?array $logNames = null,
        LogLevel|string|null $level = null
    ): LogSearchResult {
        $availableLogs = $this->getAvailableLogs();
        $logNamesToSearch = $logNames ?? array_keys($availableLogs);
        $levelFilter = $this->normalizeLevel($level);

        $results = [];

        foreach ($logNamesToSearch as $logNameString) {
            if (!isset($availableLogs[$logNameString])) {
                continue;
            }

            try {
                $result = $this->readLog(
                    logName: $logNameString,
                    limit: 500,
                    level: $levelFilter,
                    search: $query
                );

                if (!$result->isEmpty()) {
                    $results[] = $result;
                }
            } catch (\Exception $e) {
                // Skip logs that fail to read
                continue;
            }
        }

        return LogSearchResult::fromResults($query, $results);
    }

    /**
     * Stream log entries in batches (for SSE/Ajax streaming)
     *
     * @return \Generator<int, array{batch: array<LogEntry>, batch_number: int, entries_in_batch: int, total_processed: int, is_final?: bool}>
     */
    public function streamLog(
        LogName|string $logName,
        ?LogLevel $level = null,
        ?string $search = null,
        int $batchSize = 10
    ): \Generator {
        $logName = $logName instanceof LogName ? $logName : LogName::fromString($logName);

        $availableLogs = $this->getAvailableLogs();

        if (!isset($availableLogs[$logName->value])) {
            throw new \InvalidArgumentException("Log '{$logName->value}' not found");
        }

        $logPath = $availableLogs[$logName->value]['path'];

        $batch = [];
        $count = 0;

        foreach ($this->readLogEntriesGenerator($logPath, $level, $search) as $entry) {
            $batch[] = $entry;
            $count++;

            // Send batch when full
            if (count($batch) >= $batchSize) {
                yield [
                    'batch' => $batch,
                    'batch_number' => intval($count / $batchSize),
                    'entries_in_batch' => count($batch),
                    'total_processed' => $count,
                ];
                $batch = [];
            }
        }

        // Send final batch if not empty
        if (!empty($batch)) {
            yield [
                'batch' => $batch,
                'batch_number' => intval($count / $batchSize) + 1,
                'entries_in_batch' => count($batch),
                'total_processed' => $count,
                'is_final' => true,
            ];
        }
    }

    /**
     * Generator for reading last N lines from file
     * Memory-efficient for large files
     *
     * @return \Generator<int, string>
     */
    private function readLogLinesGenerator(FilePath $path, int $limit): \Generator
    {
        $file = fopen($path->toString(), 'r');

        if (!$file) {
            return;
        }

        $fileSize = $path->getSize()->toBytes();
        if ($fileSize === 0) {
            fclose($file);
            return;
        }

        // For small files: simple read
        if ($fileSize < 1024 * 1024) { // < 1MB
            yield from $this->readSmallFileLines($file, $limit);
            fclose($file);
            return;
        }

        // For large files: chunk-wise reading from end
        yield from $this->readLargeFileLines($file, $fileSize, $limit);
        fclose($file);
    }

    /**
     * Read lines from small files (< 1MB)
     *
     * @return \Generator<int, string>
     */
    private function readSmallFileLines($file, int $limit): \Generator
    {
        $content = stream_get_contents($file);
        $allLines = array_filter(explode("\n", $content));

        // Take last $limit lines
        $lastLines = array_slice($allLines, -$limit);

        foreach ($lastLines as $line) {
            yield trim($line);
        }
    }

    /**
     * Read lines from large files chunk-wise from end
     *
     * @return \Generator<int, string>
     */
    private function readLargeFileLines($file, int $fileSize, int $limit): \Generator
    {
        $chunkSize = 8192; // 8KB chunks
        $lines = [];
        $buffer = '';
        $position = $fileSize;

        while ($position > 0 && count($lines) < $limit) {
            // Calculate chunk size
            $readSize = min($chunkSize, $position);
            $position -= $readSize;

            // Read chunk
            fseek($file, $position);
            $chunk = fread($file, $readSize);

            // Add to buffer
            $buffer = $chunk . $buffer;

            // Extract complete lines
            $newLines = explode("\n", $buffer);

            // First "line" is incomplete (except at file start)
            if ($position > 0) {
                $buffer = array_shift($newLines);
            } else {
                $buffer = '';
            }

            // Add new lines (reversed)
            foreach (array_reverse($newLines) as $line) {
                if (trim($line) !== '') {
                    array_unshift($lines, trim($line));
                    if (count($lines) >= $limit) {
                        break 2;
                    }
                }
            }
        }

        // Yield lines
        foreach ($lines as $line) {
            yield $line;
        }
    }

    /**
     * Generator for filtered log entries
     *
     * @return \Generator<int, LogEntry>
     */
    private function readLogEntriesGenerator(
        FilePath $path,
        ?LogLevel $level = null,
        ?string $search = null
    ): \Generator {
        foreach ($this->readLogLinesGenerator($path, PHP_INT_MAX) as $line) {
            $entry = $this->parseLogLine($line, $path);

            if (!$entry) {
                continue;
            }

            // Apply level filter
            if ($level && !$entry->matchesLevel($level)) {
                continue;
            }

            // Apply search filter
            if ($search && !$entry->matchesSearch($search)) {
                continue;
            }

            yield $entry;
        }
    }

    /**
     * Parse log line into LogEntry VO
     */
    private function parseLogLine(string $line, FilePath $sourcePath): ?LogEntry
    {
        // Standard PHP/Laravel format: [2023-12-25 10:30:45] local.ERROR: Message {"context":"data"}
        if (preg_match('/\[([^\]]+)\]\s+\w+\.(\w+):\s+(.+)/', $line, $matches)) {
            return LogEntry::fromParsedLine(
                timestamp: $matches[1],
                level: $matches[2],
                messageWithContext: $matches[3],
                raw: $line,
                sourcePath: $sourcePath
            );
        }

        // Simple format: 2023-12-25 10:30:45 ERROR: Message
        if (preg_match('/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+(\w+):\s+(.+)/', $line, $matches)) {
            return LogEntry::fromParsedLine(
                timestamp: $matches[1],
                level: $matches[2],
                messageWithContext: $matches[3],
                raw: $line,
                sourcePath: $sourcePath
            );
        }

        // Fallback - treat as unstructured log
        return LogEntry::fromRawLine($line, $sourcePath);
    }

    /**
     * Normalize level input to LogLevel enum
     */
    private function normalizeLevel(LogLevel|string|null $level): ?LogLevel
    {
        if ($level === null) {
            return null;
        }

        if ($level instanceof LogLevel) {
            return $level;
        }

        // Try to find case by name
        foreach (LogLevel::cases() as $case) {
            if ($case->name === strtoupper($level)) {
                return $case;
            }
        }

        return null;
    }
}
