<?php

namespace App\Services;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\File;

class LogViewerService
{
    /**
     * Map of supported log levels for filtering and display.
     *
     * @var array<string, string>
     */
    public const SUPPORTED_LEVELS = [
        'ALL' => 'ALL',
        'EMERGENCY' => 'EMERGENCY',
        'ALERT' => 'ALERT',
        'CRITICAL' => 'CRITICAL',
        'ERROR' => 'ERROR',
        'WARNING' => 'WARNING',
        'NOTICE' => 'NOTICE',
        'INFO' => 'INFO',
        'DEBUG' => 'DEBUG',
    ];

    /**
     * Available tail limits.
     *
     * @var array<string, string>
     */
    public const TAIL_OPTIONS = [
        '50' => 'Last 50',
        '100' => 'Last 100',
        '200' => 'Last 200',
        '500' => 'Last 500',
        'all' => 'All',
    ];

    /**
     * Get the absolute path to the logs directory.
     */
    public function getLogsDirectory(): string
    {
        return storage_path('logs');
    }

    /**
     * Retrieve all available log files sorted newest first.
     *
     * @return array<int, array{
     *     name: string,
     *     size: int,
     *     size_formatted: string,
     *     modified_at: int,
     *     date_formatted: string
     * }>
     */
    public function getLogFiles(): array
    {
        $dir = $this->getLogsDirectory();
        if (! File::isDirectory($dir)) {
            return [];
        }

        $files = File::files($dir);
        $logFiles = [];

        foreach ($files as $file) {
            $filename = $file->getFilename();

            // Only include .log files and ignore hidden files
            if (! str_ends_with($filename, '.log') || str_starts_with($filename, '.')) {
                continue;
            }

            $size = $file->getSize();
            $mtime = $file->getMTime();

            $logFiles[] = [
                'name' => $filename,
                'size' => $size,
                'size_formatted' => $this->formatBytes($size),
                'modified_at' => $mtime,
                'date_formatted' => Carbon::createFromTimestamp($mtime)->format('Y-m-d H:i:s'),
            ];
        }

        // Sort by last modified descending, but place laravel.log at top if equally recent
        usort($logFiles, function (array $a, array $b): int {
            if ($a['name'] === 'laravel.log' && abs($a['modified_at'] - $b['modified_at']) < 3600) {
                return -1;
            }
            if ($b['name'] === 'laravel.log' && abs($a['modified_at'] - $b['modified_at']) < 3600) {
                return 1;
            }

            return $b['modified_at'] <=> $a['modified_at'];
        });

        return $logFiles;
    }

    /**
     * Resolve default log file to open.
     *
     * @param  array<int, array{name: string}>  $files
     */
    public function getDefaultLogFile(array $files): ?string
    {
        if (empty($files)) {
            return null;
        }

        foreach ($files as $file) {
            if ($file['name'] === 'laravel.log') {
                return 'laravel.log';
            }
        }

        return $files[0]['name'] ?? null;
    }

    /**
     * Validate and retrieve secure full path for a given filename.
     */
    public function getSecureFilePath(string $filename): ?string
    {
        $filename = basename(trim($filename));

        if ($filename === '' || ! str_ends_with($filename, '.log') || str_contains($filename, '..')) {
            return null;
        }

        $baseDir = realpath($this->getLogsDirectory());
        if ($baseDir === false) {
            return null;
        }

        $targetPath = $baseDir.DIRECTORY_SEPARATOR.$filename;
        $realTarget = realpath($targetPath);

        // If file doesn't exist yet, verify directory match
        if ($realTarget === false) {
            if (file_exists($targetPath)) {
                return null;
            }
            $targetDir = realpath(dirname($targetPath));

            return ($targetDir === $baseDir) ? $targetPath : null;
        }

        if (! str_starts_with($realTarget, $baseDir.DIRECTORY_SEPARATOR)) {
            return null;
        }

        return $realTarget;
    }

    /**
     * Parse log entries from a file with level, search, and tail filtering.
     *
     * @param  array{
     *     level?: string,
     *     search?: string,
     *     tail?: int|string
     * }  $filters
     * @return array{
     *     entries: array<int, array<string, mixed>>,
     *     total_parsed: int,
     *     shown_count: int,
     *     file_size_formatted: string,
     *     file_size_bytes: int
     * }
     */
    public function parseFile(string $filePath, array $filters = []): array
    {
        if (! file_exists($filePath) || ! is_readable($filePath)) {
            return [
                'entries' => [],
                'total_parsed' => 0,
                'shown_count' => 0,
                'file_size_formatted' => '0 B',
                'file_size_bytes' => 0,
            ];
        }

        $fileSizeBytes = (int) filesize($filePath);
        $fileSizeFormatted = $this->formatBytes($fileSizeBytes);

        $requestedLevel = strtoupper(trim((string) ($filters['level'] ?? 'ALL')));
        $searchTerm = trim((string) ($filters['search'] ?? ''));
        $tailParam = strtolower(trim((string) ($filters['tail'] ?? '200')));

        $tailLimit = match ($tailParam) {
            '50' => 50,
            '100' => 100,
            '200' => 200,
            '500' => 500,
            'all' => PHP_INT_MAX,
            default => 200,
        };

        $rawEntries = $this->readLogFileEntries($filePath, $tailLimit, $searchTerm !== '' || $requestedLevel !== 'ALL');

        $filteredEntries = [];
        $idCounter = 1;

        // Process raw entries (newest first)
        foreach ($rawEntries as $rawEntry) {
            $parsed = $this->parseLogEntry($rawEntry, 'entry-'.$idCounter++);

            // Level filtering
            if ($requestedLevel !== 'ALL' && ! $this->matchesLevel($parsed['level'], $requestedLevel)) {
                continue;
            }

            // Search filtering
            if ($searchTerm !== '') {
                $searchHaystack = $parsed['date'].' '.
                    $parsed['level'].' '.
                    $parsed['message'].' '.
                    $parsed['context'].' '.
                    $parsed['stack_trace'];

                if (stripos($searchHaystack, $searchTerm) === false) {
                    continue;
                }
            }

            $filteredEntries[] = $parsed;

            if (count($filteredEntries) >= $tailLimit) {
                break;
            }
        }

        return [
            'entries' => $filteredEntries,
            'total_parsed' => count($rawEntries),
            'shown_count' => count($filteredEntries),
            'file_size_formatted' => $fileSizeFormatted,
            'file_size_bytes' => $fileSizeBytes,
        ];
    }

    /**
     * Read log file entries.
     * When tailing large files without search, reads from end of file for instant loading.
     *
     * @return array<int, string> List of raw entry strings, newest first.
     */
    protected function readLogFileEntries(string $filePath, int $tailLimit, bool $exhaustiveScan): array
    {
        $fileSize = (int) filesize($filePath);
        if ($fileSize === 0) {
            return [];
        }

        // For small files (< 3MB) or exhaustive scans (search or tail=all), read and parse directly
        if ($fileSize < 3 * 1024 * 1024 || ($exhaustiveScan && $tailLimit > 1000)) {
            return $this->readEntriesForward($filePath);
        }

        // For large files with tail limit, seek backwards in chunks
        return $this->readEntriesBackward($filePath, $tailLimit);
    }

    /**
     * Forward scan reading line by line.
     *
     * @return array<int, string> List of raw entry strings (newest first).
     */
    protected function readEntriesForward(string $filePath): array
    {
        $handle = fopen($filePath, 'r');
        if (! $handle) {
            return [];
        }

        $entries = [];
        $currentEntry = '';

        while (($line = fgets($handle)) !== false) {
            if ($this->isHeaderLine($line)) {
                if ($currentEntry !== '') {
                    $entries[] = trim($currentEntry);
                }
                $currentEntry = $line;
            } else {
                $currentEntry .= $line;
            }
        }

        if ($currentEntry !== '') {
            $entries[] = trim($currentEntry);
        }

        fclose($handle);

        // Reverse so newest entries appear first
        return array_reverse($entries);
    }

    /**
     * Backward scan from end of file in blocks to avoid reading huge files into memory.
     *
     * @return array<int, string>
     */
    protected function readEntriesBackward(string $filePath, int $targetCount): array
    {
        $handle = fopen($filePath, 'r');
        if (! $handle) {
            return [];
        }

        $fileSize = (int) filesize($filePath);
        $chunkSize = 256 * 1024; // 256KB chunks
        $position = $fileSize;
        $buffer = '';
        $entries = [];

        // Fetch buffer backwards until we have enough entries or reach beginning
        while ($position > 0 && count($entries) < ($targetCount * 2 + 50)) {
            $readSize = min($chunkSize, $position);
            $position -= $readSize;

            fseek($handle, $position);
            $chunk = fread($handle, $readSize);
            $buffer = $chunk.$buffer;

            // Extract completed entries from buffer
            $lines = explode("\n", $buffer);

            // The first line might be incomplete unless position is 0
            $keepForNext = ($position > 0) ? array_shift($lines) : '';

            $chunkEntries = [];
            $current = '';

            foreach ($lines as $line) {
                if ($this->isHeaderLine($line)) {
                    if ($current !== '') {
                        $chunkEntries[] = trim($current);
                    }
                    $current = $line."\n";
                } else {
                    $current .= $line."\n";
                }
            }

            if ($current !== '') {
                $chunkEntries[] = trim($current);
            }

            if (! empty($chunkEntries)) {
                // Prepend to accumulated entries
                $entries = array_merge(array_reverse($chunkEntries), $entries);
                $buffer = $keepForNext;
            } else {
                $buffer = $keepForNext."\n".implode("\n", $lines);
            }
        }

        fclose($handle);

        return $entries;
    }

    /**
     * Check if a line is a log header line starting a new entry.
     */
    public function isHeaderLine(string $line): bool
    {
        // 1. Standard Monolog / Laravel: [2026-09-30 05:31:14] env.LEVEL: ...
        if (preg_match('/^\[\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}/', $line)) {
            return true;
        }

        // 2. CodeIgniter style: LEVEL - 2026-09-29 22:34:20 --> ...
        if (preg_match('/^(?:EMERGENCY|ALERT|CRITICAL|ERROR|WARNING|NOTICE|INFO|DEBUG)\s+-\s+\d{4}-\d{2}-\d{2}/i', $line)) {
            return true;
        }

        // 3. Simple timestamp prefix: 2026-09-29 22:34:20 App\Jobs\...
        if (preg_match('/^\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/', $line)) {
            return true;
        }

        return false;
    }

    /**
     * Parse a raw multi-line log entry into structured data.
     *
     * @return array{
     *     id: string,
     *     date: string,
     *     env: string,
     *     level: string,
     *     badge_class: string,
     *     border_class: string,
     *     message: string,
     *     context: string,
     *     stack_trace: string,
     *     has_trace: bool,
     *     raw: string
     * }
     */
    public function parseLogEntry(string $rawEntry, string $id): array
    {
        $lines = explode("\n", $rawEntry);
        $headerLine = $lines[0] ?? '';
        $bodyLines = array_slice($lines, 1);

        $date = '';
        $env = '';
        $level = 'INFO';
        $message = $headerLine;

        // Try Pattern 1: Laravel / Monolog
        // [2026-09-30 05:31:14] testing.ERROR: Message here {"context":...}
        if (preg_match('/^\[(?P<date>\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:?\d{2}|Z)?)\]\s+(?:(?P<env>[a-zA-Z0-9_\-]+)\.)?(?P<level>[A-Z]+):\s*(?P<message>.*)$/i', $headerLine, $matches)) {
            $date = $matches['date'];
            $env = $matches['env'] ?? '';
            $level = strtoupper($matches['level']);
            $message = $matches['message'];
        }
        // Try Pattern 2: CodeIgniter / AdCenter
        // ERROR - 2026-09-29 22:34:20 --> 404 - Method: GET
        elseif (preg_match('/^(?P<level>[A-Z]+)\s+-\s+(?P<date>\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2})\s+-->\s*(?P<message>.*)$/i', $headerLine, $matches)) {
            $date = $matches['date'];
            $level = strtoupper($matches['level']);
            $message = $matches['message'];
        }
        // Try Pattern 3: Simple timestamp
        // 2026-09-15 09:08:32 App\Jobs\GenerateImageCreativeJob ...
        elseif (preg_match('/^\s*(?P<date>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+(?P<message>.*)$/', $headerLine, $matches)) {
            $date = $matches['date'];
            $message = $matches['message'];
            if (str_contains($message, 'FAIL') || str_contains($message, 'ERROR')) {
                $level = 'ERROR';
            } elseif (str_contains($message, 'WARN')) {
                $level = 'WARNING';
            } else {
                $level = 'INFO';
            }
        }

        // Normalize level
        $level = $this->normalizeLevel($level);

        // Separate context and stack trace from body lines
        $contextLines = [];
        $stackTraceLines = [];
        $inStackTrace = false;

        // Check if message itself contains exception or JSON that should be formatted
        $extraContext = '';
        if (preg_match('/^(.*?)\s*(\{"(?:userId|exception|creative_id|campaign|error|query|url|brand_name|attachments)":.*)$/s', $message, $jsonMatch)) {
            $message = $jsonMatch[1];
            $extraContext = $jsonMatch[2];
        }

        if ($extraContext !== '') {
            $contextLines[] = $extraContext;
        }

        foreach ($bodyLines as $bodyLine) {
            $trimmed = trim($bodyLine);

            if ($trimmed === '[stacktrace]' || preg_match('/^#\d+\s+/', $trimmed) || preg_match('/^\d+\s+\[internal function\]:/', $trimmed)) {
                $inStackTrace = true;
            }

            if ($inStackTrace) {
                if ($trimmed !== '[stacktrace]') {
                    $stackTraceLines[] = $bodyLine;
                }
            } else {
                $contextLines[] = $bodyLine;
            }
        }

        $context = trim(implode("\n", $contextLines));
        $stackTrace = trim(implode("\n", $stackTraceLines));

        return [
            'id' => $id,
            'date' => $date,
            'env' => $env,
            'level' => $level,
            'badge_class' => $this->getLevelBadgeClass($level),
            'border_class' => $this->getLevelBorderClass($level),
            'message' => $message,
            'context' => $context,
            'stack_trace' => $stackTrace,
            'has_trace' => ! empty($stackTrace),
            'raw' => $rawEntry,
        ];
    }

    /**
     * Normalize log level.
     */
    public function normalizeLevel(string $level): string
    {
        $level = strtoupper($level);

        return match ($level) {
            'EMERGENCY', 'ALERT', 'CRITICAL' => 'CRITICAL',
            'ERROR' => 'ERROR',
            'WARNING', 'WARN' => 'WARNING',
            'NOTICE', 'INFO' => 'INFO',
            'DEBUG' => 'DEBUG',
            default => 'INFO',
        };
    }

    /**
     * Check if an entry level matches the filter level.
     */
    public function matchesLevel(string $entryLevel, string $filterLevel): bool
    {
        if ($filterLevel === 'ALL') {
            return true;
        }

        if ($filterLevel === 'CRITICAL') {
            return in_array($entryLevel, ['CRITICAL', 'EMERGENCY', 'ALERT'], true);
        }

        return $entryLevel === $filterLevel;
    }

    /**
     * Get badge color styling class.
     */
    public function getLevelBadgeClass(string $level): string
    {
        return match ($level) {
            'CRITICAL', 'EMERGENCY', 'ALERT' => 'bg-danger text-white',
            'ERROR' => 'bg-warning text-dark text-nowrap',
            'WARNING' => 'bg-warning-subtle text-warning-emphasis border border-warning',
            'NOTICE', 'INFO' => 'bg-info-subtle text-info-emphasis border border-info',
            'DEBUG' => 'bg-secondary text-white',
            default => 'bg-secondary text-white',
        };
    }

    /**
     * Get left border styling class.
     */
    public function getLevelBorderClass(string $level): string
    {
        return match ($level) {
            'CRITICAL', 'EMERGENCY', 'ALERT' => 'border-danger',
            'ERROR' => 'border-warning',
            'WARNING' => 'border-warning',
            'NOTICE', 'INFO' => 'border-info',
            'DEBUG' => 'border-secondary',
            default => 'border-secondary',
        };
    }

    /**
     * Clear / delete log files based on action.
     *
     * @return array{success: bool, message: string}
     */
    public function clearLogs(string $action, ?string $filename = null): array
    {
        $dir = $this->getLogsDirectory();

        switch ($action) {
            case 'clear_current':
                if (empty($filename)) {
                    return ['success' => false, 'message' => 'No log file specified.'];
                }
                $path = $this->getSecureFilePath($filename);
                if (! $path || ! file_exists($path)) {
                    return ['success' => false, 'message' => 'File not found.'];
                }
                file_put_contents($path, '');

                return ['success' => true, 'message' => "Cleared {$filename} successfully."];

            case 'delete_current':
                if (empty($filename)) {
                    return ['success' => false, 'message' => 'No log file specified.'];
                }
                $path = $this->getSecureFilePath($filename);
                if (! $path || ! file_exists($path)) {
                    return ['success' => false, 'message' => 'File not found.'];
                }
                if ($filename === 'laravel.log') {
                    file_put_contents($path, '');

                    return ['success' => true, 'message' => 'Cleared laravel.log successfully.'];
                }
                unlink($path);

                return ['success' => true, 'message' => "Deleted {$filename} successfully."];

            case 'delete_old':
                $files = File::files($dir);
                $deletedCount = 0;
                $cutoff = Carbon::now()->subDays(7)->getTimestamp();

                foreach ($files as $file) {
                    $name = $file->getFilename();
                    if ($name === 'laravel.log' || ! str_ends_with($name, '.log')) {
                        continue;
                    }
                    if ($file->getMTime() < $cutoff) {
                        unlink($file->getRealPath());
                        $deletedCount++;
                    }
                }

                return ['success' => true, 'message' => "Deleted {$deletedCount} old log files (older than 7 days)."];

            case 'delete_all':
                $files = File::files($dir);
                $deletedCount = 0;

                foreach ($files as $file) {
                    $name = $file->getFilename();
                    if (! str_ends_with($name, '.log')) {
                        continue;
                    }
                    if ($name === 'laravel.log') {
                        file_put_contents($file->getRealPath(), '');
                    } else {
                        unlink($file->getRealPath());
                        $deletedCount++;
                    }
                }

                return ['success' => true, 'message' => "Deleted {$deletedCount} log files and cleared laravel.log."];

            default:
                return ['success' => false, 'message' => 'Invalid clear action specified.'];
        }
    }

    /**
     * Format byte count to human-readable string.
     */
    public function formatBytes(int $bytes, int $precision = 1): string
    {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];

        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);

        $bytes /= (1 << (10 * $pow));

        return round($bytes, $precision).' '.$units[$pow];
    }
}
