<?php
/**
 * ===========================================
 * FLOWBOT DCI - PROCESS MANAGER v3.0
 * ===========================================
 * Manages process lifecycle: pause, resume, cancel
 *
 * Features:
 * - Pause running processes
 * - Resume paused processes
 * - Cancel processes with cleanup
 * - Get real-time process state
 * - Process control via state files
 */

declare(strict_types=1);

namespace FlowbotDCI\Services;

use FlowbotDCI\Core\Database;

class ProcessManager
{
    private Database $db;
    private string $tempDir;
    private DashboardService $dashboard;

    public function __construct(Database $db, string $tempDir, DashboardService $dashboard)
    {
        $this->db = $db;
        $this->tempDir = $tempDir;
        $this->dashboard = $dashboard;
    }

    /**
     * Pause a running process
     * v5.0 FIX: Added null check and logging for debugging
     */
    public function pause(string $processId): bool
    {
        $processDir = $this->getProcessDir($processId);

        // v5.0 FIX: Handle null return from getProcessDir
        if ($processDir === null) {
            error_log("ProcessManager::pause() - Directory not found for process: {$processId}");
            return false;
        }

        if (!is_dir($processDir)) {
            error_log("ProcessManager::pause() - Directory does not exist: {$processDir}");
            return false;
        }

        // v4.6: Try both filename formats (with and without processId prefix)
        $progressFile = $processDir . '/' . $processId . '_progress.json';
        if (!file_exists($progressFile)) {
            $progressFile = $processDir . '/progress.json';
        }
        $controlFile = $processDir . '/control.json';

        if (!file_exists($progressFile)) {
            return false;
        }

        // Read current progress
        $progress = json_decode(file_get_contents($progressFile), true);

        if (!$progress || ($progress['isComplete'] ?? false)) {
            return false;
        }

        // Set pause flag
        $progress['isPaused'] = true;
        $progress['pausedAt'] = date('Y-m-d H:i:s');
        file_put_contents($progressFile, json_encode($progress, JSON_PRETTY_PRINT));

        // Write control file
        $control = [
            'action' => 'pause',
            'timestamp' => date('Y-m-d H:i:s'),
        ];
        file_put_contents($controlFile, json_encode($control, JSON_PRETTY_PRINT));

        // Update database
        $this->dashboard->updateProcess($processId, [
            'status' => 'paused',
        ]);

        // Log activity
        $this->dashboard->logActivity(
            'process_paused',
            "Process #{$processId} was paused",
            $processId,
            'info'
        );

        return true;
    }

    /**
     * Resume a paused process
     * v5.0 FIX: Added null check and logging for debugging
     */
    public function resume(string $processId): bool
    {
        $processDir = $this->getProcessDir($processId);

        // v5.0 FIX: Handle null return from getProcessDir
        if ($processDir === null) {
            error_log("ProcessManager::resume() - Directory not found for process: {$processId}");
            return false;
        }

        if (!is_dir($processDir)) {
            error_log("ProcessManager::resume() - Directory does not exist: {$processDir}");
            return false;
        }

        // v4.6: Try both filename formats (with and without processId prefix)
        $progressFile = $processDir . '/' . $processId . '_progress.json';
        if (!file_exists($progressFile)) {
            $progressFile = $processDir . '/progress.json';
        }
        $controlFile = $processDir . '/control.json';

        if (!file_exists($progressFile)) {
            return false;
        }

        // Read current progress
        $progress = json_decode(file_get_contents($progressFile), true);

        if (!$progress || ($progress['isComplete'] ?? false)) {
            return false;
        }

        if (!($progress['isPaused'] ?? false)) {
            return true; // Already running
        }

        // Clear pause flag
        $progress['isPaused'] = false;
        $progress['resumedAt'] = date('Y-m-d H:i:s');
        unset($progress['pausedAt']);
        file_put_contents($progressFile, json_encode($progress, JSON_PRETTY_PRINT));

        // Write control file
        $control = [
            'action' => 'resume',
            'timestamp' => date('Y-m-d H:i:s'),
        ];
        file_put_contents($controlFile, json_encode($control, JSON_PRETTY_PRINT));

        // Update database
        $this->dashboard->updateProcess($processId, [
            'status' => 'running',
        ]);

        // Log activity
        $this->dashboard->logActivity(
            'process_resumed',
            "Process #{$processId} was resumed",
            $processId,
            'info'
        );

        return true;
    }

    /**
     * Cancel a process with cleanup
     * v5.0 FIX: Added null check and logging for debugging
     */
    public function cancel(string $processId): bool
    {
        $processDir = $this->getProcessDir($processId);

        // v5.0 FIX: Handle null return from getProcessDir
        if ($processDir === null) {
            error_log("ProcessManager::cancel() - Directory not found for process: {$processId}");
            // Still update database to cancelled status even if files not found
            $this->dashboard->updateProcess($processId, [
                'status' => 'cancelled',
                'completed_at' => date('Y-m-d H:i:s'),
            ]);
            return true; // Consider it cancelled even without files
        }

        if (!is_dir($processDir)) {
            error_log("ProcessManager::cancel() - Directory does not exist: {$processDir}");
            // Still update database
            $this->dashboard->updateProcess($processId, [
                'status' => 'cancelled',
                'completed_at' => date('Y-m-d H:i:s'),
            ]);
            return true;
        }

        // v4.6: Try both filename formats (with and without processId prefix)
        $progressFile = $processDir . '/' . $processId . '_progress.json';
        if (!file_exists($progressFile)) {
            $progressFile = $processDir . '/progress.json';
        }
        $controlFile = $processDir . '/control.json';

        // Read current progress for logging
        $progress = [];
        if (file_exists($progressFile)) {
            $progress = json_decode(file_get_contents($progressFile), true) ?: [];
        }

        // Mark as cancelled
        $progress['isCancelled'] = true;
        $progress['isComplete'] = true;
        $progress['cancelledAt'] = date('Y-m-d H:i:s');
        file_put_contents($progressFile, json_encode($progress, JSON_PRETTY_PRINT));

        // Write control file
        $control = [
            'action' => 'cancel',
            'timestamp' => date('Y-m-d H:i:s'),
        ];
        file_put_contents($controlFile, json_encode($control, JSON_PRETTY_PRINT));

        // Update database
        $this->dashboard->updateProcess($processId, [
            'status' => 'cancelled',
            'completed_at' => date('Y-m-d H:i:s'),
        ]);

        // Log activity
        $processed = $progress['processedUrls'] ?? 0;
        $total = $progress['totalUrls'] ?? 0;
        $this->dashboard->logActivity(
            'process_cancelled',
            "Process #{$processId} was cancelled at {$processed}/{$total} URLs",
            $processId,
            'warning',
            ['processed' => $processed, 'total' => $total]
        );

        return true;
    }

    /**
     * Get current process state
     * Supports both legacy and new progress file formats
     */
    public function getState(string $processId): ?array
    {
        $processDir = $this->getProcessDir($processId);

        if (!is_dir($processDir)) {
            // Try to get from database
            return $this->dashboard->getProcess($processId);
        }

        // Try both progress file formats
        $progressFile = $processDir . '/' . $processId . '_progress.json';
        if (!file_exists($progressFile)) {
            $progressFile = $processDir . '/progress.json';
        }

        if (!file_exists($progressFile)) {
            return null;
        }

        $progress = json_decode(file_get_contents($progressFile), true);

        if (!$progress) {
            return null;
        }

        // Support both field naming conventions
        $total = $progress['total_links'] ?? $progress['totalUrls'] ?? 0;
        $processed = $progress['processed_links'] ?? $progress['processedUrls'] ?? 0;
        $success = $progress['imported_links'] ?? $progress['successUrls'] ?? 0;
        $failed = $progress['error_links'] ?? $progress['failedUrls'] ?? 0;
        $ignored = $progress['ignored_links'] ?? 0;
        $phase = $progress['phase_index'] ?? $progress['currentPhase'] ?? 0;
        $elapsedTime = $progress['elapsed_time'] ?? 0;

        // Calculate ETA from progress data or estimate
        $eta = null;
        $speed = null;

        if (isset($progress['remaining_time']) && $progress['remaining_time'] > 0) {
            $eta = (int)$progress['remaining_time'];
        }

        if (isset($progress['processing_rate']) && $progress['processing_rate'] > 0) {
            $speed = round($progress['processing_rate'] * 60, 1);
        } elseif (isset($progress['startTime']) && $processed > 0) {
            $elapsed = time() - strtotime($progress['startTime']);
            $urlsPerSecond = $processed / max(1, $elapsed);
            $remaining = $total - $processed;
            $eta = $urlsPerSecond > 0 ? (int)($remaining / $urlsPerSecond) : null;
            $speed = round($urlsPerSecond * 60, 1);
        }

        // Determine completion by checking phase queues
        $isComplete = false;
        if (isset($progress['phase_queues'])) {
            $remaining = 0;
            foreach ($progress['phase_queues'] as $queue) {
                $remaining += count($queue);
            }
            $isComplete = $remaining === 0 && $processed > 0;
        } else {
            $isComplete = $progress['isComplete'] ?? false;
        }

        // v3.1 FIX: Use determineStatus() to correctly check isCancelled FIRST
        $status = $this->determineStatus($progress);
        if ($isComplete && $status === 'running') {
            $status = 'completed';
        }

        return [
            'id' => $processId,
            'total_urls' => $total,
            'processed_urls' => $processed,
            'success_count' => $success,
            'failed_count' => $failed,
            'ignored_count' => $ignored,
            'elapsed_time' => $elapsedTime,
            'progress_percent' => $total > 0 ? round(($processed / $total) * 100, 1) : 0,
            'phase' => $phase,
            'current_url' => $progress['currentUrl'] ?? null,
            'status' => $status,
            'is_paused' => $progress['isPaused'] ?? false,
            'is_complete' => $isComplete,
            'is_cancelled' => $progress['isCancelled'] ?? false,
            'started_at' => $progress['startTime'] ?? null,
            'eta_seconds' => $eta,
            'speed_per_minute' => $speed,
            'domain_stats' => $progress['domain_stats'] ?? $progress['domainStats'] ?? [],
            'last_log' => $this->getLastLogEntries($processId, 50),
        ];
    }

    /**
     * Determine process status from progress data
     */
    private function determineStatus(array $progress): string
    {
        if ($progress['isCancelled'] ?? false) {
            return 'cancelled';
        }
        if ($progress['isComplete'] ?? false) {
            return 'completed';
        }
        if ($progress['isPaused'] ?? false) {
            return 'paused';
        }
        return 'running';
    }

    /**
     * Get last log entries for process
     */
    private function getLastLogEntries(string $processId, int $limit = 50): array
    {
        $processDir = $this->getProcessDir($processId);
        $logFile = $processDir . '/logs.json';

        if (!file_exists($logFile)) {
            return [];
        }

        $logs = json_decode(file_get_contents($logFile), true) ?: [];

        // Return last N entries
        return array_slice($logs, -$limit);
    }

    /**
     * Set process options
     */
    public function setOptions(string $processId, array $options): bool
    {
        $processDir = $this->getProcessDir($processId);
        $optionsFile = $processDir . '/options.json';

        // Merge with existing options
        $existing = [];
        if (file_exists($optionsFile)) {
            $existing = json_decode(file_get_contents($optionsFile), true) ?: [];
        }

        $merged = array_merge($existing, $options);
        file_put_contents($optionsFile, json_encode($merged, JSON_PRETTY_PRINT));

        // Update database
        $this->dashboard->updateProcess($processId, [
            'options' => json_encode($merged),
        ]);

        return true;
    }

    /**
     * Get process options
     * v5.0 FIX: Handle null processDir
     */
    public function getOptions(string $processId): array
    {
        $processDir = $this->getProcessDir($processId);

        // v5.0 FIX: Handle null return from getProcessDir
        if ($processDir === null) {
            return [];
        }

        $optionsFile = $processDir . '/options.json';

        if (!file_exists($optionsFile)) {
            return [];
        }

        return json_decode(file_get_contents($optionsFile), true) ?: [];
    }

    /**
     * Check if process is paused (for use in processing loop)
     * v5.0 FIX: Handle null processDir
     */
    public function isPaused(string $processId): bool
    {
        $processDir = $this->getProcessDir($processId);

        // v5.0 FIX: Handle null return from getProcessDir
        if ($processDir === null) {
            return false;
        }

        $controlFile = $processDir . '/control.json';

        if (file_exists($controlFile)) {
            $control = json_decode(file_get_contents($controlFile), true);
            if ($control && ($control['action'] ?? '') === 'pause') {
                return true;
            }
        }

        // v4.6: Try both filename formats (with and without processId prefix)
        $progressFile = $processDir . '/' . $processId . '_progress.json';
        if (!file_exists($progressFile)) {
            $progressFile = $processDir . '/progress.json';
        }
        if (file_exists($progressFile)) {
            $progress = json_decode(file_get_contents($progressFile), true);
            return $progress['isPaused'] ?? false;
        }

        return false;
    }

    /**
     * Check if process is cancelled (for use in processing loop)
     * v5.0 FIX: Handle null processDir
     */
    public function isCancelled(string $processId): bool
    {
        $processDir = $this->getProcessDir($processId);

        // v5.0 FIX: Handle null return from getProcessDir
        if ($processDir === null) {
            return false;
        }

        $controlFile = $processDir . '/control.json';

        if (file_exists($controlFile)) {
            $control = json_decode(file_get_contents($controlFile), true);
            if ($control && ($control['action'] ?? '') === 'cancel') {
                return true;
            }
        }

        // v4.6: Try both filename formats (with and without processId prefix)
        $progressFile = $processDir . '/' . $processId . '_progress.json';
        if (!file_exists($progressFile)) {
            $progressFile = $processDir . '/progress.json';
        }
        if (file_exists($progressFile)) {
            $progress = json_decode(file_get_contents($progressFile), true);
            return $progress['isCancelled'] ?? false;
        }

        return false;
    }

    /**
     * Clear control flag after processing it
     * v5.0 FIX: Handle null processDir
     */
    public function clearControlFlag(string $processId): void
    {
        $processDir = $this->getProcessDir($processId);

        // v5.0 FIX: Handle null return from getProcessDir
        if ($processDir === null) {
            return;
        }

        $controlFile = $processDir . '/control.json';

        if (file_exists($controlFile)) {
            unlink($controlFile);
        }
    }

    /**
     * Get all active process IDs
     */
    public function getActiveProcessIds(): array
    {
        $ids = [];

        if (!is_dir($this->tempDir)) {
            return $ids;
        }

        $dirs = glob($this->tempDir . '/process_*', GLOB_ONLYDIR);

        foreach ($dirs as $dir) {
            $progressFile = $dir . '/progress.json';
            if (file_exists($progressFile)) {
                $progress = json_decode(file_get_contents($progressFile), true);
                if ($progress && !($progress['isComplete'] ?? false)) {
                    $ids[] = str_replace('process_', '', basename($dir));
                }
            }
        }

        return $ids;
    }

    /**
     * Clean up old completed processes
     */
    public function cleanupOldProcesses(int $maxAgeDays = 30): int
    {
        $deleted = 0;

        if (!is_dir($this->tempDir)) {
            return 0;
        }

        $dirs = glob($this->tempDir . '/process_*', GLOB_ONLYDIR);
        $maxAge = time() - ($maxAgeDays * 86400);

        foreach ($dirs as $dir) {
            $progressFile = $dir . '/progress.json';

            if (file_exists($progressFile)) {
                $progress = json_decode(file_get_contents($progressFile), true);

                // Only clean up completed processes
                if ($progress && ($progress['isComplete'] ?? false)) {
                    $mtime = filemtime($progressFile);
                    if ($mtime < $maxAge) {
                        $this->deleteDirectory($dir);
                        $deleted++;
                    }
                }
            }
        }

        return $deleted;
    }

    /**
     * Get process directory path
     * v5.0 FIX: Returns NULL if no directory found (instead of non-existent default path)
     * SEC-004 FIX: Validate processId format to prevent directory traversal
     */
    private function getProcessDir(string $processId): ?string
    {
        // SEC-004: Validate processId format to prevent directory traversal attacks
        // Only allow alphanumeric characters, underscores, and hyphens (max 64 chars)
        if (!preg_match('/^[a-zA-Z0-9_-]{1,64}$/', $processId)) {
            error_log("ProcessManager::getProcessDir() - SEC-004: Invalid processId format rejected: {$processId}");
            return null;
        }

        // Support both formats: batch_* and process_* directories
        // First check if the directory exists as-is (e.g., batch_123)
        $directDir = $this->tempDir . '/' . $processId;
        if (is_dir($directDir)) {
            return $directDir;
        }

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

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

        // v5.0 FIX: Return NULL if no directory found (not default path!)
        // This forces callers to handle the case where process files don't exist
        return null;
    }

    /**
     * Recursively delete directory
     */
    private function deleteDirectory(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }

        $files = array_diff(scandir($dir), ['.', '..']);
        foreach ($files as $file) {
            $path = $dir . '/' . $file;
            is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
        }
        rmdir($dir);
    }

    /**
     * Get live progress for real-time updates
     * Returns fields compatible with both old and new frontend formats
     * v3.1 FIX: Now includes phase_queues and detailed stats for proper UI updates
     */
    public function getLiveProgress(string $processId): ?array
    {
        $processDir = $this->getProcessDir($processId);

        // Read full progress data from file (includes phase_queues)
        $progressFile = $processDir . '/' . $processId . '_progress.json';
        if (!file_exists($progressFile)) {
            $progressFile = $processDir . '/progress.json';
        }

        if (!file_exists($progressFile)) {
            // Fall back to state method if no file
            $state = $this->getState($processId);
            if (!$state) {
                return null;
            }
            return [
                'id' => $processId,
                'total_links' => $state['total_urls'],
                'processed_links' => $state['processed_urls'],
                'imported_links' => $state['success_count'],
                'ignored_links' => $state['ignored_count'] ?? 0,
                'error_links' => $state['failed_count'],
                'phase_index' => $state['phase'] ?? 0,
                'is_complete' => $state['is_complete'],
                'status' => $state['status'],
            ];
        }

        $progress = json_decode(file_get_contents($progressFile), true);

        if (!$progress) {
            return null;
        }

        // Calculate timing
        $timeFile = $processDir . '/' . $processId . '_time.json';
        if (!file_exists($timeFile)) {
            $timeFile = $processDir . '/time.json';
        }

        $timeData = file_exists($timeFile) ? json_decode(file_get_contents($timeFile), true) : [];
        $startTime = $timeData['start_time'] ?? microtime(true);
        $elapsedTime = microtime(true) - $startTime;

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

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

        $remainingTime = $avgTimePerLink * $remainingInQueues;

        // Determine if complete
        $isComplete = $remainingInQueues === 0 && $processedCount > 0;

        // v3.1 FIX: Use determineStatus() to correctly check isCancelled FIRST
        $status = $this->determineStatus($progress);
        if ($isComplete && $status === 'running') {
            $status = 'completed';
        }

        // Return full progress data for UI
        return [
            'id' => $processId,
            'timestamp' => date('Y-m-d H:i:s'),
            'is_complete' => $isComplete || ($progress['isCancelled'] ?? false),
            'status' => $status,

            // Core stats
            'total_links' => $progress['total_links'] ?? 0,
            'processed_links' => $progress['processed_links'] ?? 0,
            'imported_links' => $progress['imported_links'] ?? 0,
            'ignored_links' => $progress['ignored_links'] ?? 0,
            'error_links' => $progress['error_links'] ?? 0,
            'batches_processed' => $progress['batches_processed'] ?? 0,

            // Timing
            'elapsed_time' => $elapsedTime,
            'remaining_time' => $remainingTime,
            'processing_rate' => ($processedCount > 0) ? round($processedCount / $elapsedTime, 2) : 0,

            // Phase data - CRITICAL for UI
            'phase_index' => $progress['phase_index'] ?? 0,
            'phase_queues' => $progress['phase_queues'] ?? [[], [], [], []],
            'phase_stats' => $progress['phase_stats'] ?? [],

            // Detailed stats for v3.1 UI
            'ignored_details' => $progress['ignored_details'] ?? [],
            'error_details' => $progress['error_details'] ?? [],
            'request_stats' => $progress['request_stats'] ?? [],
            'http_codes' => $progress['http_codes'] ?? [],
            'domain_stats' => $progress['domain_stats'] ?? [],
            'retry_stats' => $progress['retry_stats'] ?? [],
        ];
    }
}
