<?php
/**
 * ===========================================
 * FLOWBOT DCI - PROGRESS TRACKER SERVICE
 * ===========================================
 * Tracks processing progress using JSON files
 * v2.2: Added batch write optimization (PERF-002)
 */

declare(strict_types=1);

namespace FlowbotDCI\Services;

class ProgressTracker
{
    private array $config;
    private string $processId;
    private string $tempFolder;
    private string $linksFile;
    private string $progressFile;
    private string $timeFile;
    private string $logFile;

    // OTIMIZADO: Cache para evitar leituras repetidas de JSON
    private ?array $cachedProgress = null;
    private ?array $cachedTime = null;

    // PERF-002: Batch write buffers - accumulate changes and write once
    private array $pendingDomainStats = [];
    private array $pendingRequestStats = [];
    private array $pendingLogEntries = [];
    private int $pendingRetries = 0;
    private bool $hasPendingChanges = false;

    // v3.0: Redis service for distributed caching
    private ?RedisService $redis = null;
    private const CACHE_TTL = 300; // 5 minutes

    public function __construct(array $config, string $processId, ?RedisService $redis = null)
    {
        $this->config = $config;
        $this->processId = $processId;
        $this->tempFolder = $config['paths']['temp'] . '/' . $processId . '/';

        $this->linksFile    = $this->tempFolder . $processId . '_all_links.json';
        $this->progressFile = $this->tempFolder . $processId . '_progress.json';
        $this->timeFile     = $this->tempFolder . $processId . '_time.json';
        $this->logFile      = $this->tempFolder . $processId . '_log.json';

        // v3.0: Initialize Redis (can be null for backward compatibility)
        $this->redis = $redis;
    }

    /**
     * Initialize new processing session
     */
    public function initialize(array $links): void
    {
        // Create temp folder
        if (!is_dir($this->tempFolder)) {
            mkdir($this->tempFolder, 0755, true);
        }

        // Save all links
        $this->saveJson($this->linksFile, $links);

        // Initialize phase queues (all links start in phase 0)
        $phaseQueues = [[], [], [], []];
        $phaseQueues[0] = $links;

        $data = [
            'total_links'           => count($links),
            'processed_links'       => 0,
            'ignored_links'         => 0,
            'error_links'           => 0,
            'imported_links'        => 0,
            'total_without_errors'  => 0,
            'total_with_errors'     => 0,
            'total_links_processed' => 0,
            'batches_processed'     => 0,
            'max_batch_time'        => 0,
            'last_batch_time'       => 0,
            'elapsed_time'          => 0,
            'remaining_time'        => 0,
            'processing_rate'       => 0,
            'phase_index'           => 0,
            'phase_queues'          => $phaseQueues,

            // NOVO: Detalhes de URLs ignoradas
            'ignored_details' => [
                'duplicate'      => 0,  // Já existe no BD
                'invalid_url'    => 0,  // URL malformada
                'blocked_domain' => 0,  // Facebook, Instagram, etc.
                'non_html'       => 0,  // PDF, imagem, etc.
            ],

            // NOVO: Detalhes de erros por tipo
            'error_details' => [
                'timeout'    => 0,  // Timeout de conexão
                'http_429'   => 0,  // Too Many Requests
                'http_404'   => 0,  // Not Found
                'http_403'   => 0,  // Forbidden
                'http_5xx'   => 0,  // Server errors
                'connection' => 0,  // Connection refused/reset
                'metadata'   => 0,  // Erro ao extrair metadata
                'other'      => 0,  // Outros erros
            ],

            // NOVO: Estatísticas por fase
            'phase_stats' => [
                0 => ['success' => 0, 'retry' => 0, 'error' => 0],
                1 => ['success' => 0, 'retry' => 0, 'error' => 0],
                2 => ['success' => 0, 'retry' => 0, 'error' => 0],
                3 => ['success' => 0, 'retry' => 0, 'error' => 0],
            ],

            // NEW: Domain-level statistics
            'domain_stats' => [],

            // v2.1: Request statistics (response times)
            'request_stats' => [
                'total_requests' => 0,
                'total_response_time' => 0,
                'min_response_time' => PHP_FLOAT_MAX,
                'max_response_time' => 0,
                'response_times' => [],  // Last 100 for percentiles
            ],

            // v2.1: HTTP response code tracking
            'http_codes' => [],  // ['200' => 50, '403' => 10, ...]

            // v2.1: Retry statistics
            'retry_stats' => [
                'total_retries' => 0,
                'retries_by_phase' => [0 => 0, 1 => 0, 2 => 0, 3 => 0],
                'urls_with_retries' => 0,
            ],
        ];

        $this->saveJson($this->progressFile, $data);
        $this->saveJson($this->timeFile, ['start_time' => microtime(true)]);
        $this->saveJson($this->logFile, []);
    }

    /**
     * Check if progress file exists
     */
    public function hasProgress(): bool
    {
        return file_exists($this->progressFile);
    }

    /**
     * Load progress data
     * OTIMIZADO: Usa Redis cache primeiro, depois local cache, depois arquivo
     */
    public function loadProgress(): array
    {
        if (!$this->hasProgress()) {
            return [];
        }

        // v3.0: Try Redis cache first (fastest)
        if ($this->redis !== null) {
            $cached = $this->redis->getProgress($this->processId);
            if ($cached !== null && !empty($cached)) {
                // Redis has valid cached data - just recalculate timing
                return $this->addTimingToProgress($cached);
            }
        }

        // OTIMIZADO: Retorna local cache se disponível
        if ($this->cachedProgress !== null && $this->cachedTime !== null) {
            $data = $this->cachedProgress;
            $timeData = $this->cachedTime;
        } else {
            $data = $this->loadJson($this->progressFile);
            $timeData = $this->loadJson($this->timeFile);
            $this->cachedProgress = $data;
            $this->cachedTime = $timeData;

            // v3.0: Store in Redis for other processes/requests
            if ($this->redis !== null && !empty($data)) {
                $data['_time_data'] = $timeData;
                $this->redis->cacheProgress($this->processId, $data);
            }
        }

        return $this->addTimingToProgress($data, $timeData);
    }

    /**
     * Add timing calculations to progress data
     */
    private function addTimingToProgress(array $data, ?array $timeData = null): array
    {
        // Extract time data if embedded
        if ($timeData === null && isset($data['_time_data'])) {
            $timeData = $data['_time_data'];
            unset($data['_time_data']);
        }

        $timeData = $timeData ?? $this->cachedTime ?? $this->loadJson($this->timeFile);

        // Calculate timing
        $startTime = $timeData['start_time'] ?? microtime(true);
        $currentTime = microtime(true);
        $elapsedTime = $currentTime - $startTime;

        $processedCount = $data['processed_links'] ?? 0;
        $avgTimePerLink = ($processedCount > 0) ? ($elapsedTime / $processedCount) : 0;

        $remainingInQueues = 0;
        foreach ($data['phase_queues'] ?? [] as $queue) {
            $remainingInQueues += count($queue);
        }

        $data['elapsed_time'] = $elapsedTime;
        $data['remaining_time'] = $avgTimePerLink * $remainingInQueues;
        $data['processing_rate'] = ($processedCount > 0) ? round($processedCount / $elapsedTime, 2) : 0;

        // v3.0: Add current phase for quick access
        $data['current_phase'] = $data['phase_index'] ?? 0;

        return $data;
    }

    /**
     * Save progress data
     * OTIMIZADO: Invalida cache após salvar e atualiza Redis
     */
    public function saveProgress(array $data): void
    {
        $this->cachedProgress = null; // Invalidar local cache

        // Save to file (source of truth)
        $this->saveJson($this->progressFile, $data);

        // v3.0: Update Redis cache for fast reads
        if ($this->redis !== null) {
            $timeData = $this->cachedTime ?? $this->loadJson($this->timeFile);
            $data['_time_data'] = $timeData;
            $this->redis->cacheProgress($this->processId, $data);
        }
    }

    /**
     * Get current batch of URLs to process
     */
    public function getCurrentBatch(): array
    {
        $data = $this->loadProgress();
        $phaseIndex = $data['phase_index'] ?? 0;
        $phaseQueues = $data['phase_queues'] ?? [[], [], [], []];
        $maxLinks = $this->config['processing']['max_links_per_batch'];

        return array_slice($phaseQueues[$phaseIndex] ?? [], 0, $maxLinks);
    }

    /**
     * Get current phase configuration
     */
    public function getCurrentPhaseConfig(): array
    {
        $data = $this->loadProgress();
        $phaseIndex = min($data['phase_index'] ?? 0, 3);
        return $this->config['phases'][$phaseIndex];
    }

    /**
     * Get current phase index
     */
    public function getCurrentPhaseIndex(): int
    {
        $data = $this->loadProgress();
        return $data['phase_index'] ?? 0;
    }

    /**
     * Update progress after batch processing
     */
    public function updateAfterBatch(array $results, float $batchTime): void
    {
        $data = $this->loadProgress();

        // Ensure phase_index exists with default value
        $phaseIndex = $data['phase_index'] ?? 0;
        $maxLinks = $this->config['processing']['max_links_per_batch'] ?? 50;

        // Ensure phase_queues structure exists
        if (!isset($data['phase_queues']) || !is_array($data['phase_queues'])) {
            $data['phase_queues'] = [[], [], [], []];
        }

        // Ensure the specific phase queue exists and is an array
        if (!isset($data['phase_queues'][$phaseIndex]) || !is_array($data['phase_queues'][$phaseIndex])) {
            $data['phase_queues'][$phaseIndex] = [];
        }

        // Remove processed links from current phase queue
        $data['phase_queues'][$phaseIndex] = array_slice(
            $data['phase_queues'][$phaseIndex],
            $maxLinks
        );

        // Update counters
        $data['processed_links'] += $results['processed'];
        $data['ignored_links'] += $results['ignored'];
        $data['error_links'] += $results['errors'];
        $data['imported_links'] += $results['imported'];
        $data['total_links_processed'] += $results['total'];
        $data['batches_processed']++;
        $data['last_batch_time'] = $batchTime;
        $data['max_batch_time'] = max($data['max_batch_time'], $batchTime);

        // NOVO: Atualizar detalhes de ignored
        if (!isset($data['ignored_details'])) {
            $data['ignored_details'] = ['duplicate' => 0, 'invalid_url' => 0, 'blocked_domain' => 0, 'non_html' => 0];
        }
        foreach ($results['ignored_details'] ?? [] as $reason => $count) {
            $data['ignored_details'][$reason] = ($data['ignored_details'][$reason] ?? 0) + $count;
        }

        // NOVO: Atualizar detalhes de erros
        if (!isset($data['error_details'])) {
            $data['error_details'] = ['timeout' => 0, 'http_429' => 0, 'http_404' => 0, 'http_403' => 0, 'http_5xx' => 0, 'connection' => 0, 'metadata' => 0, 'other' => 0];
        }
        foreach ($results['error_details'] ?? [] as $errorType => $count) {
            $data['error_details'][$errorType] = ($data['error_details'][$errorType] ?? 0) + $count;
        }

        // NOVO: Atualizar estatísticas por fase
        if (!isset($data['phase_stats'])) {
            $data['phase_stats'] = [
                0 => ['success' => 0, 'retry' => 0, 'error' => 0],
                1 => ['success' => 0, 'retry' => 0, 'error' => 0],
                2 => ['success' => 0, 'retry' => 0, 'error' => 0],
                3 => ['success' => 0, 'retry' => 0, 'error' => 0],
            ];
        }
        $data['phase_stats'][$phaseIndex]['success'] += $results['imported'];
        $data['phase_stats'][$phaseIndex]['retry'] += count($results['move_to_next_phase'] ?? []);
        $data['phase_stats'][$phaseIndex]['error'] += $results['errors'];

        // Move failed URLs to next phase
        foreach ($results['move_to_next_phase'] ?? [] as $url) {
            if ($phaseIndex < 3) {
                $data['phase_queues'][$phaseIndex + 1][] = $url;
            }
        }

        // Check if current phase is complete
        if (empty($data['phase_queues'][$phaseIndex]) && $phaseIndex < 3) {
            $data['phase_index'] = $phaseIndex + 1;
        }

        $this->saveProgress($data);

        // Update log
        $this->addLogEntry([
            'timestamp'  => gmdate('Y-m-d H:i:s'),
            'phase'      => $phaseIndex,
            'processed'  => $results['total'],
            'imported'   => $results['imported'],
            'ignored'    => $results['ignored'],
            'errors'     => $results['errors'],
            'batch_time' => round($batchTime, 3),
        ]);

        // PERF-002: Flush all pending changes (domain stats, request stats, log entries)
        // This writes everything in a single batch instead of per-URL
        $this->flushPendingChanges();
    }

    /**
     * Check if processing is complete
     */
    public function isComplete(): bool
    {
        $data = $this->loadProgress();
        $totalRemaining = 0;

        foreach ($data['phase_queues'] ?? [] as $queue) {
            $totalRemaining += count($queue);
        }

        return $totalRemaining === 0;
    }

    /**
     * v4.5: Find actual process directory (same logic as ProcessManager.getProcessDir)
     * This unifies path resolution to fix pause/cancel/cleanup issues
     */
    private function findProcessDir(): string
    {
        $baseDir = $this->config['paths']['temp'];

        // Try direct path first (temp/processId/)
        $directDir = $baseDir . '/' . $this->processId;
        if (is_dir($directDir)) {
            return $directDir . '/';
        }

        // Try with process_ prefix (temp/process_processId/)
        $processDir = $baseDir . '/process_' . $this->processId;
        if (is_dir($processDir)) {
            return $processDir . '/';
        }

        // Try with batch_ prefix if not already in processId (temp/batch_processId/)
        if (strpos($this->processId, 'batch_') !== 0) {
            $batchDir = $baseDir . '/batch_' . $this->processId;
            if (is_dir($batchDir)) {
                return $batchDir . '/';
            }
        }

        // Default to original tempFolder (may be for new processes)
        return $this->tempFolder;
    }

    /**
     * v4.4/v4.5: Check if process was cancelled
     * Enables mid-batch cancel detection by reading control.json and progress.json
     * v4.5: Uses unified path resolution to find control.json
     */
    public function isCancelled(): bool
    {
        // v4.5: Use unified path resolution
        $processDir = $this->findProcessDir();

        // Check control.json first (faster, written by ProcessManager)
        $controlFile = $processDir . 'control.json';
        if (file_exists($controlFile)) {
            $control = @json_decode(file_get_contents($controlFile), true);
            if ($control && ($control['action'] ?? '') === 'cancel') {
                return true;
            }
        }

        // Check progress.json for isCancelled flag (try multiple formats)
        $progressFiles = [
            $processDir . $this->processId . '_progress.json',
            $processDir . 'progress.json',
            $this->progressFile, // Original path as fallback
        ];

        foreach ($progressFiles as $pFile) {
            if (file_exists($pFile)) {
                $progress = @json_decode(file_get_contents($pFile), true);
                if ($progress && ($progress['isCancelled'] ?? false)) {
                    return true;
                }
                break; // Only check first existing file
            }
        }

        return false;
    }

    /**
     * v4.4/v4.5: Check if process is paused
     * Similar to isCancelled but for pause state
     * v4.5: Uses unified path resolution to find control.json
     */
    public function isPaused(): bool
    {
        // v4.5: Use unified path resolution
        $processDir = $this->findProcessDir();

        // Check control.json first
        $controlFile = $processDir . 'control.json';
        if (file_exists($controlFile)) {
            $control = @json_decode(file_get_contents($controlFile), true);
            if ($control && ($control['action'] ?? '') === 'pause') {
                return true;
            }
        }

        // Check progress.json for isPaused flag (try multiple formats)
        $progressFiles = [
            $processDir . $this->processId . '_progress.json',
            $processDir . 'progress.json',
            $this->progressFile, // Original path as fallback
        ];

        foreach ($progressFiles as $pFile) {
            if (file_exists($pFile)) {
                $progress = @json_decode(file_get_contents($pFile), true);
                if ($progress && ($progress['isPaused'] ?? false)) {
                    return true;
                }
                break; // Only check first existing file
            }
        }

        return false;
    }

    /**
     * Add log entry
     */
    public function addLogEntry(array $entry): void
    {
        $logs = $this->loadJson($this->logFile) ?? [];
        $logs[] = $entry;
        $this->saveJson($this->logFile, $logs);
    }

    /**
     * Get all log entries
     */
    public function getLogs(): array
    {
        return $this->loadJson($this->logFile) ?? [];
    }

    /**
     * Cleanup temp files
     * v4.5: Uses unified path resolution to ensure correct directory is cleaned
     */
    public function cleanup(): void
    {
        // v4.5: Use unified path resolution
        $processDir = $this->findProcessDir();

        // Delete all JSON files in process directory
        $files = glob($processDir . '*.json');
        foreach ($files as $file) {
            @unlink($file);
        }

        // Delete directory if empty
        if (is_dir($processDir)) {
            @rmdir($processDir);
        }

        // Also try original tempFolder if different (in case of path mismatch)
        if ($processDir !== $this->tempFolder && is_dir($this->tempFolder)) {
            $files = glob($this->tempFolder . '*.json');
            foreach ($files as $file) {
                @unlink($file);
            }
            @rmdir($this->tempFolder);
        }
    }

    /**
     * PERF-002: Flush all pending changes to disk in a single write
     * This should be called at the end of each batch processing
     */
    public function flushPendingChanges(): void
    {
        if (!$this->hasPendingChanges) {
            return;
        }

        $data = $this->loadProgress();

        // Apply pending domain stats
        if (!empty($this->pendingDomainStats)) {
            if (!isset($data['domain_stats'])) {
                $data['domain_stats'] = [];
            }

            foreach ($this->pendingDomainStats as $domain => $stats) {
                if (!isset($data['domain_stats'][$domain])) {
                    $data['domain_stats'][$domain] = [
                        'total' => 0,
                        'success' => 0,
                        'error' => 0,
                        'ignored' => 0,
                        'retry' => 0,
                        'total_response_time' => 0,
                        'avg_response_time' => 0,
                        'error_types' => [],
                        'http_codes' => [],
                        'min_response_time' => PHP_FLOAT_MAX,
                        'max_response_time' => 0,
                        'first_seen' => gmdate('Y-m-d H:i:s'),
                        'last_seen' => gmdate('Y-m-d H:i:s'),
                    ];
                }

                // Merge stats
                $data['domain_stats'][$domain]['total'] += $stats['total'];
                $data['domain_stats'][$domain]['success'] += $stats['success'];
                $data['domain_stats'][$domain]['error'] += $stats['error'];
                $data['domain_stats'][$domain]['ignored'] += $stats['ignored'];
                $data['domain_stats'][$domain]['retry'] += $stats['retry'];
                $data['domain_stats'][$domain]['total_response_time'] += $stats['total_response_time'];
                $data['domain_stats'][$domain]['last_seen'] = gmdate('Y-m-d H:i:s');

                // Merge error types
                foreach ($stats['error_types'] as $errorType => $count) {
                    if (!isset($data['domain_stats'][$domain]['error_types'][$errorType])) {
                        $data['domain_stats'][$domain]['error_types'][$errorType] = 0;
                    }
                    $data['domain_stats'][$domain]['error_types'][$errorType] += $count;
                }

                // Recalculate average
                if ($data['domain_stats'][$domain]['total'] > 0) {
                    $data['domain_stats'][$domain]['avg_response_time'] = round(
                        $data['domain_stats'][$domain]['total_response_time'] / $data['domain_stats'][$domain]['total'],
                        3
                    );
                }
            }
            $this->pendingDomainStats = [];
        }

        // Apply pending request stats
        if (!empty($this->pendingRequestStats)) {
            if (!isset($data['request_stats'])) {
                $data['request_stats'] = [
                    'total_requests' => 0,
                    'total_response_time' => 0,
                    'min_response_time' => PHP_FLOAT_MAX,
                    'max_response_time' => 0,
                    'response_times' => [],
                ];
            }
            if (!isset($data['http_codes'])) {
                $data['http_codes'] = [];
            }

            foreach ($this->pendingRequestStats as $stat) {
                $httpCode = $stat['http_code'];
                $responseTime = $stat['response_time'];
                $domain = $stat['domain'];

                // Update HTTP code tracking
                $codeStr = (string)$httpCode;
                $data['http_codes'][$codeStr] = ($data['http_codes'][$codeStr] ?? 0) + 1;

                // Update response time stats
                $data['request_stats']['total_requests']++;
                $data['request_stats']['total_response_time'] += $responseTime;

                if ($responseTime > 0) {
                    $data['request_stats']['min_response_time'] = min(
                        $data['request_stats']['min_response_time'],
                        $responseTime
                    );
                    $data['request_stats']['max_response_time'] = max(
                        $data['request_stats']['max_response_time'],
                        $responseTime
                    );

                    // Keep last 100 response times for percentile calculation
                    $data['request_stats']['response_times'][] = $responseTime;
                    if (count($data['request_stats']['response_times']) > 100) {
                        array_shift($data['request_stats']['response_times']);
                    }
                }

                // Update domain-level HTTP codes
                if ($domain && isset($data['domain_stats'][$domain])) {
                    if (!isset($data['domain_stats'][$domain]['http_codes'])) {
                        $data['domain_stats'][$domain]['http_codes'] = [];
                    }
                    $data['domain_stats'][$domain]['http_codes'][$codeStr] =
                        ($data['domain_stats'][$domain]['http_codes'][$codeStr] ?? 0) + 1;

                    // Track min/max response time per domain
                    if ($responseTime > 0) {
                        if (!isset($data['domain_stats'][$domain]['min_response_time'])) {
                            $data['domain_stats'][$domain]['min_response_time'] = PHP_FLOAT_MAX;
                        }
                        if (!isset($data['domain_stats'][$domain]['max_response_time'])) {
                            $data['domain_stats'][$domain]['max_response_time'] = 0;
                        }
                        $data['domain_stats'][$domain]['min_response_time'] = min(
                            $data['domain_stats'][$domain]['min_response_time'],
                            $responseTime
                        );
                        $data['domain_stats'][$domain]['max_response_time'] = max(
                            $data['domain_stats'][$domain]['max_response_time'],
                            $responseTime
                        );
                    }
                }
            }
            $this->pendingRequestStats = [];
        }

        // Apply pending retries
        if ($this->pendingRetries > 0) {
            if (!isset($data['retry_stats'])) {
                $data['retry_stats'] = [
                    'total_retries' => 0,
                    'retries_by_phase' => [0 => 0, 1 => 0, 2 => 0, 3 => 0],
                    'urls_with_retries' => 0,
                ];
            }
            $data['retry_stats']['total_retries'] += $this->pendingRetries;
            $phaseIndex = $data['phase_index'] ?? 0;
            $data['retry_stats']['retries_by_phase'][$phaseIndex] =
                ($data['retry_stats']['retries_by_phase'][$phaseIndex] ?? 0) + $this->pendingRetries;
            $this->pendingRetries = 0;
        }

        // Save progress data (single write)
        $this->saveProgress($data);

        // Apply pending log entries (separate file, single write)
        if (!empty($this->pendingLogEntries)) {
            $logs = $this->loadJson($this->logFile) ?? [];
            $startId = count($logs) + 1;

            foreach ($this->pendingLogEntries as $i => $entry) {
                $entry['id'] = $startId + $i;
                $logs[] = $entry;
            }

            $this->saveJson($this->logFile, $logs);
            $this->pendingLogEntries = [];
        }

        $this->hasPendingChanges = false;
    }

    /**
     * Save JSON file
     * OTIMIZADO: Removido JSON_PRETTY_PRINT para arquivo menor e mais rápido
     */
    private function saveJson(string $path, array $data): void
    {
        file_put_contents($path, json_encode($data));
    }

    /**
     * Load JSON file
     */
    private function loadJson(string $path): array
    {
        if (!file_exists($path)) {
            return [];
        }
        return json_decode(file_get_contents($path), true) ?? [];
    }

    /**
     * Get process ID
     */
    public function getProcessId(): string
    {
        return $this->processId;
    }

    /**
     * Update domain-level statistics
     * Tracks success/error/ignored counts per domain
     * PERF-002: Now buffers changes instead of writing immediately
     */
    public function updateDomainStats(string $domain, string $status, float $responseTime = 0, ?string $errorType = null): void
    {
        // PERF-002: Buffer changes instead of immediate write
        if (!isset($this->pendingDomainStats[$domain])) {
            $this->pendingDomainStats[$domain] = [
                'total' => 0,
                'success' => 0,
                'error' => 0,
                'ignored' => 0,
                'retry' => 0,
                'total_response_time' => 0,
                'error_types' => [],
            ];
        }

        $this->pendingDomainStats[$domain]['total']++;

        // Update status counter
        if (isset($this->pendingDomainStats[$domain][$status])) {
            $this->pendingDomainStats[$domain][$status]++;
        }

        // Update response time tracking
        if ($responseTime > 0) {
            $this->pendingDomainStats[$domain]['total_response_time'] += $responseTime;
        }

        // Track error types per domain
        if ($errorType && $status === 'error') {
            if (!isset($this->pendingDomainStats[$domain]['error_types'][$errorType])) {
                $this->pendingDomainStats[$domain]['error_types'][$errorType] = 0;
            }
            $this->pendingDomainStats[$domain]['error_types'][$errorType]++;
        }

        $this->hasPendingChanges = true;
    }

    /**
     * Get domain statistics
     */
    public function getDomainStats(): array
    {
        $data = $this->loadProgress();
        return $data['domain_stats'] ?? [];
    }

    /**
     * Get domain statistics sorted by total (descending)
     */
    public function getDomainStatsSorted(string $sortBy = 'total', string $direction = 'desc'): array
    {
        $stats = $this->getDomainStats();

        uasort($stats, function($a, $b) use ($sortBy, $direction) {
            $aVal = $a[$sortBy] ?? 0;
            $bVal = $b[$sortBy] ?? 0;
            return $direction === 'desc' ? $bVal <=> $aVal : $aVal <=> $bVal;
        });

        return $stats;
    }

    /**
     * Add detailed log entry with URL info
     * Enhanced for logs page functionality
     * PERF-002: Now buffers entries instead of writing immediately
     */
    public function addDetailedLogEntry(array $entry): void
    {
        // Add timestamp
        $entry['timestamp'] = $entry['timestamp'] ?? gmdate('Y-m-d H:i:s');

        // Extract domain from URL
        if (isset($entry['url'])) {
            $entry['domain'] = parse_url($entry['url'], PHP_URL_HOST) ?? 'unknown';
        }

        // PERF-002: Buffer log entry instead of immediate write
        $this->pendingLogEntries[] = $entry;
        $this->hasPendingChanges = true;
    }

    /**
     * Get filtered logs for logs page
     */
    public function getFilteredLogs(array $filters = [], int $page = 1, int $perPage = 50): array
    {
        $logs = $this->loadJson($this->logFile) ?? [];

        // Apply filters
        if (!empty($filters['status'])) {
            $logs = array_filter($logs, fn($log) => ($log['class'] ?? '') === $filters['status']);
        }

        if (!empty($filters['domain'])) {
            $logs = array_filter($logs, fn($log) => ($log['domain'] ?? '') === $filters['domain']);
        }

        if (!empty($filters['error_type'])) {
            $logs = array_filter($logs, fn($log) =>
                str_contains(strtolower($log['message'] ?? ''), strtolower($filters['error_type']))
            );
        }

        if (!empty($filters['search'])) {
            $search = strtolower($filters['search']);
            $logs = array_filter($logs, fn($log) =>
                str_contains(strtolower($log['url'] ?? ''), $search) ||
                str_contains(strtolower($log['message'] ?? ''), $search)
            );
        }

        // Re-index array
        $logs = array_values($logs);
        $total = count($logs);

        // Reverse to show newest first
        $logs = array_reverse($logs);

        // Paginate
        $offset = ($page - 1) * $perPage;
        $paginatedLogs = array_slice($logs, $offset, $perPage);

        return [
            'logs' => $paginatedLogs,
            'total' => $total,
            'page' => $page,
            'per_page' => $perPage,
            'total_pages' => ceil($total / $perPage),
        ];
    }

    /**
     * Get unique domains from logs
     */
    public function getLogDomains(): array
    {
        $logs = $this->loadJson($this->logFile) ?? [];
        $domains = [];

        foreach ($logs as $log) {
            if (isset($log['domain']) && !in_array($log['domain'], $domains)) {
                $domains[] = $log['domain'];
            }
        }

        sort($domains);
        return $domains;
    }

    /**
     * v2.1: Update request statistics (HTTP codes and response times)
     * PERF-002: Now buffers changes instead of writing immediately
     */
    public function updateRequestStats(int $httpCode, float $responseTime, ?string $domain = null): void
    {
        // PERF-002: Buffer changes instead of immediate write
        $this->pendingRequestStats[] = [
            'http_code' => $httpCode,
            'response_time' => $responseTime,
            'domain' => $domain,
        ];
        $this->hasPendingChanges = true;
    }

    /**
     * v2.1: Calculate response time percentiles
     */
    public function getResponseTimePercentiles(): array
    {
        $data = $this->loadProgress();
        $times = $data['request_stats']['response_times'] ?? [];

        if (empty($times)) {
            return ['p50' => 0, 'p90' => 0, 'p99' => 0];
        }

        sort($times);
        $count = count($times);

        return [
            'p50' => $times[(int)floor($count * 0.50)] ?? 0,
            'p90' => $times[(int)floor($count * 0.90)] ?? 0,
            'p99' => $times[(int)floor($count * 0.99)] ?? 0,
        ];
    }

    /**
     * v2.1: Increment retry count for a phase
     * PERF-002: Now buffers changes instead of writing immediately
     */
    public function incrementRetryCount(int $phaseIndex): void
    {
        // PERF-002: Buffer retry count - will be applied in flushPendingChanges
        $this->pendingRetries++;
        $this->hasPendingChanges = true;
    }

    /**
     * v2.1: Get average response time
     */
    public function getAverageResponseTime(): float
    {
        $data = $this->loadProgress();
        $stats = $data['request_stats'] ?? [];

        if (($stats['total_requests'] ?? 0) === 0) {
            return 0;
        }

        return round($stats['total_response_time'] / $stats['total_requests'], 3);
    }

    /**
     * v2.1: Get HTTP codes sorted by count (descending)
     */
    public function getHttpCodesSorted(): array
    {
        $data = $this->loadProgress();
        $codes = $data['http_codes'] ?? [];
        arsort($codes);
        return $codes;
    }

    /**
     * v2.1: Get success rate percentage
     */
    public function getSuccessRate(): float
    {
        $data = $this->loadProgress();
        $total = $data['total_links'] ?? 0;
        $imported = $data['imported_links'] ?? 0;

        if ($total === 0) {
            return 0;
        }

        return round(($imported / $total) * 100, 1);
    }

    /**
     * v2.1: Get problematic domains (high error rate)
     */
    public function getProblematicDomains(float $errorThreshold = 0.5): array
    {
        $stats = $this->getDomainStats();
        $problematic = [];

        foreach ($stats as $domain => $data) {
            if ($data['total'] >= 3) { // Only consider domains with at least 3 requests
                $errorRate = $data['error'] / $data['total'];
                if ($errorRate >= $errorThreshold) {
                    $problematic[$domain] = [
                        'total' => $data['total'],
                        'errors' => $data['error'],
                        'error_rate' => round($errorRate * 100, 1),
                        'avg_response_time' => $data['avg_response_time'] ?? 0,
                        'error_types' => $data['error_types'] ?? [],
                    ];
                }
            }
        }

        // Sort by error rate descending
        uasort($problematic, fn($a, $b) => $b['error_rate'] <=> $a['error_rate']);

        return $problematic;
    }

    /**
     * v2.1: Get slowest domains
     */
    public function getSlowestDomains(int $limit = 10): array
    {
        $stats = $this->getDomainStats();
        $slow = [];

        foreach ($stats as $domain => $data) {
            if (($data['avg_response_time'] ?? 0) > 0) {
                $slow[$domain] = $data['avg_response_time'];
            }
        }

        arsort($slow);
        return array_slice($slow, 0, $limit, true);
    }
}
