<?php

namespace Lcmigrator\LcMigrator\FileUpload;

use Lcmigrator\LcMigrator\Logger\Logger;
use Lcmigrator\LcMigrator\Migrator\BackupInProgress\BackupInProgress;

class GCP {
    // Minimum and maximum chunk sizes
    const MIN_CHUNK_SIZE = 1 * 1024 * 1024;  // 1MB
    const MAX_CHUNK_SIZE = 16 * 1024 * 1024; // 16MB
    const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024; // 8MB default
    
    const TARGET_CHUNK_COUNT = 1000; // Aim for ~1000 chunks for large files
    
    const MAX_RETRIES = 3;
    const CURL_TIMEOUT = 60;
    const CONNECT_TIMEOUT = 10;
    const PROGRESS_CHECK_INTERVAL = 30;
    const STATE_FILE_SUFFIX = '.upload_state';
    
    private Logger $logger;
    private $lastProgressTime;
    private $lastUploadedBytes;
    private $gcFrequency = 20;
    private $uploadStateFile;
    private $chunkSize;
    /** @var array|null Last init failure details (http_code, response_snippet, curl_error) for error reporting */
    private $lastInitError = null;

    public function __construct() {
        $this->logger = Logger::getInstance();
        $this->lastProgressTime = 0;
        $this->lastUploadedBytes = 0;
        $this->chunkSize = self::DEFAULT_CHUNK_SIZE;
    }

    /**
     * Upload large file to GCS using signed URL with chunked upload
     */
    public function uploadToSignedUrl($signedUrl, $filePath, $contentType, $siteId, $progressCallback = null) {
        // Set PHP execution time to prevent timeouts
        set_time_limit(0);
        
        // Increase memory limit for large file handling
        ini_set('memory_limit', '1024M');
        
        // Validate and sanitize signed URL (cURL 3 = malformed URL)
        $signedUrl = $this->sanitizeSignedUrl($signedUrl);
        if ($signedUrl === null) {
            return array(
                'error' => true,
                'message' => 'Invalid or empty GCS upload URL from server. The upload URL may be malformed or not returned correctly.',
            );
        }
        
        $this->logger->info("Uploading file to GCS: " . $filePath);
        if (!file_exists($filePath)) {
            $this->logger->error("File does not exist at path: " . $filePath);
            return array(
                'error' => true,
                'message' => 'File does not exist at path: ' . $filePath
            );
        }

        $fileSize = filesize($filePath);
        
        // Calculate optimal chunk size based on file size
        $this->chunkSize = $this->calculateOptimalChunkSize($fileSize);
        
        $this->uploadStateFile = $filePath . self::STATE_FILE_SUFFIX;
        $uploadedBytes = $this->loadUploadState($fileSize);
        
        // Only initialize a new session if we're starting from scratch
        $sessionUrl = null;
        $this->lastInitError = null;
        if ($uploadedBytes === 0) {
            $sessionUrl = $this->initializeResumableUpload($signedUrl, $contentType);
            if (!$sessionUrl) {
                return array(
                    'error' => true,
                    'message' => $this->getResumableInitErrorMessage(),
                );
            }
            // Save the session URL for potential future use
            $this->saveUploadState(0, $sessionUrl);
        } else {
            // Get the session URL from the state file
            $sessionUrl = $this->getSessionUrlFromState();
            if (!$sessionUrl) {
                // If we can't get it, start over
                $sessionUrl = $this->initializeResumableUpload($signedUrl, $contentType);
                if (!$sessionUrl) {
                    return array(
                        'error' => true,
                        'message' => $this->getResumableInitErrorMessage(),
                    );
                }
                $uploadedBytes = 0;
                $this->saveUploadState(0, $sessionUrl);
            }
            $this->logger->info("Resuming upload from byte $uploadedBytes");
        }

        $handle = fopen($filePath, 'rb');
        if ($uploadedBytes > 0) {
            // Seek to the position where we left off
            fseek($handle, $uploadedBytes);
        }
        
        $this->lastUploadedBytes = $uploadedBytes;
        $this->lastProgressTime = time();
        $uploadStartTime = time();

        $this->logger->info("File Size: " . $fileSize . " bytes (" . round($fileSize / (1024 * 1024 * 1024), 2) . " GB)");
        $this->logger->info("Using chunk size: " . $this->formatBytes($this->chunkSize));
        $totalChunks = ceil($fileSize / $this->chunkSize);
        $remainingChunks = ceil(($fileSize - $uploadedBytes) / $this->chunkSize);
        $this->logger->info("Total chunks: " . $totalChunks . ", Remaining chunks: " . $remainingChunks);
        $chunkIndex = floor($uploadedBytes / $this->chunkSize);

        // Calculate estimated time based on an estimated upload speed
        $estimatedMBps = 10; // Estimated 10MB/s upload speed, adjust as needed
        $estimatedSeconds = ($fileSize - $uploadedBytes) / ($estimatedMBps * 1024 * 1024);
        $this->logger->info("Estimated upload time: " . $this->formatTime($estimatedSeconds) . " (at {$estimatedMBps}MB/s)");

        // Adjust garbage collection frequency based on chunk size
        $this->gcFrequency = max(5, min(50, ceil(80 * 1024 * 1024 / $this->chunkSize)));
        $this->logger->info("Garbage collection frequency: every {$this->gcFrequency} chunks");

        while (!feof($handle)) {
            $chunkIndex++;
            
            // Progress monitoring - less frequent for large files
            if (time() - $this->lastProgressTime >= self::PROGRESS_CHECK_INTERVAL) {
                if ($uploadedBytes == $this->lastUploadedBytes && $uploadedBytes > 0) {
                    $this->logger->warning("Upload appears to be stalled. No progress in " . 
                                         self::PROGRESS_CHECK_INTERVAL . " seconds.");
                    gc_collect_cycles();
                }
                
                $this->lastUploadedBytes = $uploadedBytes;
                $this->lastProgressTime = time();
                
                // Calculate upload speed and ETA
                $elapsedTime = time() - $uploadStartTime;
                if ($elapsedTime > 0 && $uploadedBytes > 0) {
                    $bytesPerSecond = $uploadedBytes / $elapsedTime;
                    $remainingBytes = $fileSize - $uploadedBytes;
                    $remainingSeconds = ($bytesPerSecond > 0) ? $remainingBytes / $bytesPerSecond : 0;
                    
                    $percentComplete = round(($uploadedBytes / $fileSize) * 100, 2);
                    $uploadSpeed = round($bytesPerSecond / (1024 * 1024), 2);
                    
                    $this->logger->info(
                        "Upload progress: {$percentComplete}% complete. " .
                        "Speed: {$uploadSpeed} MB/s. " .
                        "ETA: " . $this->formatTime($remainingSeconds) . ". " .
                        "Elapsed: " . $this->formatTime($elapsedTime)
                    );
                }
            }
            
            // Check if backup has been force closed
            $this->logger->info("Checking force closed for site: " . $siteId);
            $backupInProgress = new BackupInProgress($siteId);
            $backupInProgress->refreshMetadata();
            $forceClosed = $backupInProgress->isForceClosed();
            $this->logger->info("Site Force Closed: " . $forceClosed);
            
            if ($forceClosed) {
                $this->logger->info("Backup force closed, stopping upload");
                fclose($handle);
                return array(
                    'success' => false,
                    'status' => 'backup_stopped',
                    'backup_id' => $backupInProgress->getBackupId(),
                    'closed_at_stage' => 'GCS upload',
                    'stage' => 9
                );
            }

            $chunk = fread($handle, $this->chunkSize);
            $chunkSize = strlen($chunk);
            $start = $uploadedBytes;
            $end = $uploadedBytes + $chunkSize - 1;
            
            // Selective logging
            if ($chunkIndex === 1 || $chunkIndex % 50 === 0 || $end + 1 >= $fileSize) {
                $this->logger->info("Uploading chunk {$chunkIndex}/{$totalChunks}: bytes {$start}-{$end} of {$fileSize}");
            }
            
            // Retry logic for failed chunks
            $retries = 0;
            $success = false;
            
            while (!$success && $retries < self::MAX_RETRIES) {
                if ($retries > 0) {
                    $retryDelay = $retries * 500000; // 0.5s, 1s, 1.5s
                    $this->logger->info("Retrying chunk upload (attempt {$retries} of " . self::MAX_RETRIES . ")");
                    usleep($retryDelay);
                }
                
                $ch = curl_init($sessionUrl);
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
                curl_setopt($ch, CURLOPT_POSTFIELDS, $chunk);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_TIMEOUT, self::CURL_TIMEOUT);
                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::CONNECT_TIMEOUT);
                curl_setopt($ch, CURLOPT_HTTPHEADER, array(
                    'Content-Length: ' . $chunkSize,
                    'Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize
                ));
                
                // Simplified progress tracking
                curl_setopt($ch, CURLOPT_NOPROGRESS, false);
                curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($downloadSize, $downloaded, $uploadSize, $uploaded) {
                    if ($uploaded > 0) {
                        $this->lastProgressTime = time();
                    }
                    return 0;
                });

                $response = curl_exec($ch);
                $curlError = curl_errno($ch);
                $curlErrorMessage = curl_error($ch);
                $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                curl_close($ch);
                
                if ($curlError !== 0) {
                    $this->logger->error("cURL error during chunk upload: {$curlError} - {$curlErrorMessage}");
                    $retries++;
                    continue;
                }

                if (in_array($httpCode, [200, 201, 308])) {
                    $success = true;
                } else {
                    $this->logger->error("Upload failed with HTTP code: {$httpCode}, Response: " . substr($response, 0, 200));
                    $retries++;
                }
            }
            
            if (!$success) {
                fclose($handle);
                return array(
                    'error' => true,
                    'message' => 'Upload failed after ' . self::MAX_RETRIES . ' retries for chunk ' . $chunkIndex . ' starting at byte ' . $start,
                    'response' => $response
                );
            }

            $uploadedBytes += $chunkSize;
            
            // Update state after every successful chunk
            $this->saveUploadState($uploadedBytes, $sessionUrl);
            
            // Free memory
            unset($chunk);
            if ($chunkIndex % $this->gcFrequency === 0) {
                gc_collect_cycles();
            }
            
            if ($progressCallback) {
                $progress = ($uploadedBytes / $fileSize) * 100;
                call_user_func($progressCallback, $progress);
            }
        }

        fclose($handle);
        
        // Clean up state file after successful upload
        if (file_exists($this->uploadStateFile)) {
            unlink($this->uploadStateFile);
        }
        
        $totalTime = time() - $uploadStartTime;
        $avgSpeed = ($fileSize - $this->lastUploadedBytes) / (($totalTime > 0) ? $totalTime : 1) / (1024 * 1024);
        $this->logger->info("Upload completed successfully in " . $this->formatTime($totalTime) . " at avg speed of {$avgSpeed} MB/s");
        
        return array(
            'success' => true,
            'status' => 'gcs_upload_completed',
            'backup_id' => $backupInProgress->getBackupId(),
            'closed_at_stage' => 'GCS upload',
            'stage' => 10,
            'total_time' => $totalTime,
            'total_size' => $fileSize,
            'avg_speed' => $avgSpeed,
            'chunk_size_used' => $this->chunkSize
        );
    }

    /**
     * Calculate optimal chunk size based on file size
     */
    private function calculateOptimalChunkSize($fileSize) {
        // For very small files (< 10MB), use 1MB chunks
        if ($fileSize < 10 * 1024 * 1024) {
            return self::MIN_CHUNK_SIZE;
        }
        
        // For small files (< 100MB), use 2MB chunks
        if ($fileSize < 100 * 1024 * 1024) {
            return 2 * 1024 * 1024;
        }
        
        // For medium files (< 1GB), use 4MB chunks
        if ($fileSize < 1024 * 1024 * 1024) {
            return 4 * 1024 * 1024;
        }
        
        // For large files (< 5GB), use 8MB chunks
        if ($fileSize < 5 * 1024 * 1024 * 1024) {
            return 8 * 1024 * 1024;
        }
        
        // For very large files (> 5GB), calculate chunk size to aim for TARGET_CHUNK_COUNT chunks
        // but cap at MAX_CHUNK_SIZE
        $calculatedSize = ceil($fileSize / self::TARGET_CHUNK_COUNT);
        
        // Round to nearest MB for cleaner chunk sizes
        $calculatedSize = ceil($calculatedSize / (1024 * 1024)) * 1024 * 1024;
        
        return min($calculatedSize, self::MAX_CHUNK_SIZE);
    }

    /**
     * Sanitize and validate the signed URL to prevent cURL 3 (malformed URL).
     * Backend may return empty, wrong type, or URL with whitespace/newlines.
     *
     * @param mixed $signedUrl URL string, or array/object from API response
     * @return string|null Valid URL string or null if invalid
     */
    private function sanitizeSignedUrl($signedUrl) {
        if (is_array($signedUrl) && !empty($signedUrl[0])) {
            $signedUrl = $signedUrl[0];
        }
        if (is_object($signedUrl)) {
            $signedUrl = isset($signedUrl->uploadUrl) ? (is_array($signedUrl->uploadUrl) ? ($signedUrl->uploadUrl[0] ?? '') : $signedUrl->uploadUrl) : '';
        }
        if (!is_string($signedUrl)) {
            $this->logger->error("GCS upload URL is not a string: " . gettype($signedUrl));
            return null;
        }
        $url = trim(str_replace(["\r", "\n"], '', $signedUrl));
        if ($url === '') {
            $this->logger->error("GCS upload URL is empty");
            return null;
        }
        if (!preg_match('#^https?://#i', $url)) {
            $this->logger->error("GCS upload URL does not start with http(s)");
            return null;
        }
        if (filter_var($url, FILTER_VALIDATE_URL) === false) {
            // Long GCS URLs with many query params can exceed PHP's validate length; allow if clearly https
            if (strlen($url) > 2048 || strpos($url, ' ') !== false) {
                $this->logger->error("GCS upload URL may be malformed (length or invalid characters)");
                return null;
            }
        }
        return $url;
    }

    /**
     * Initialize resumable upload session
     */
    private function initializeResumableUpload($signedUrl, $contentType) {
        $ch = curl_init($signedUrl);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, '');
        curl_setopt($ch, CURLOPT_TIMEOUT, self::CURL_TIMEOUT);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::CONNECT_TIMEOUT);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array(
            'Content-Type: ' . $contentType,
            'Content-Length: 0',
            'x-goog-resumable: start'
        ));

        $response = curl_exec($ch);
        $curlError = curl_errno($ch);
        $curlErrorMessage = curl_error($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($curlError !== 0) {
            $this->lastInitError = array('curl_error' => $curlError, 'curl_message' => $curlErrorMessage);
            $this->logger->error("cURL error initializing upload: {$curlError} - {$curlErrorMessage}");
            return null;
        }

        if ($httpCode !== 201) {
            $this->lastInitError = array(
                'http_code' => $httpCode,
                'response_snippet' => is_string($response) ? substr($response, 0, 500) : '',
            );
            $this->logger->error("Failed to initialize resumable upload. HTTP Code: {$httpCode}");
            $this->logger->error("Response: " . substr($response, 0, 200));
            return null;
        }

        if (!preg_match('/Location: (.+)/i', $response, $matches)) {
            $this->lastInitError = array('reason' => 'no_location_header', 'response_snippet' => is_string($response) ? substr($response, 0, 200) : '');
            $this->logger->error("No upload session URL found in response");
            return null;
        }

        return trim($matches[1]);
    }

    /**
     * Build a user- and log-friendly error message from last init failure
     */
    private function getResumableInitErrorMessage() {
        $base = 'Failed to initialize resumable upload session';
        if (empty($this->lastInitError)) {
            return $base;
        }
        $e = $this->lastInitError;
        if (isset($e['curl_error'], $e['curl_message'])) {
            return $base . ' (cURL ' . $e['curl_error'] . ': ' . $e['curl_message'] . ')';
        }
        if (isset($e['http_code'])) {
            $msg = $base . ' (HTTP ' . $e['http_code'] . ')';
            if (!empty($e['response_snippet'])) {
                $snippet = trim(preg_replace('/\s+/', ' ', substr($e['response_snippet'], 0, 150)));
                $msg .= '. Response: ' . $snippet;
            }
            return $msg;
        }
        if (isset($e['reason']) && $e['reason'] === 'no_location_header') {
            return $base . ' (GCS did not return upload session URL)';
        }
        return $base;
    }
    
    /**
     * Save the current upload state to allow resuming
     */
    private function saveUploadState($bytesUploaded, $sessionUrl) {
        $state = json_encode([
            'bytes_uploaded' => $bytesUploaded,
            'session_url' => $sessionUrl,
            'timestamp' => time(),
            'chunk_size' => $this->chunkSize
        ]);
        
        file_put_contents($this->uploadStateFile, $state);
    }
    
    /**
     * Load the previous upload state if available
     */
    private function loadUploadState($fileSize) {
        if (!file_exists($this->uploadStateFile)) {
            return 0;
        }
        
        $state = json_decode(file_get_contents($this->uploadStateFile), true);
        if (!$state || !isset($state['bytes_uploaded']) || !isset($state['session_url'])) {
            return 0;
        }
        
        // Verify that the state is not too old (24 hours max)
        if (time() - $state['timestamp'] > 86400) {
            $this->logger->info("Upload state is too old, starting from beginning");
            unlink($this->uploadStateFile);
            return 0;
        }
        
        // Verify that the uploaded bytes is sensible
        if ($state['bytes_uploaded'] >= $fileSize || $state['bytes_uploaded'] < 0) {
            return 0;
        }
        
        // Restore the chunk size if it was saved
        if (isset($state['chunk_size'])) {
            $this->chunkSize = $state['chunk_size'];
        }
        
        return $state['bytes_uploaded'];
    }
    
    /**
     * Get session URL from state file
     */
    private function getSessionUrlFromState() {
        if (!file_exists($this->uploadStateFile)) {
            return null;
        }
        
        $state = json_decode(file_get_contents($this->uploadStateFile), true);
        return isset($state['session_url']) ? $state['session_url'] : null;
    }
    
    /**
     * Format seconds into a human-readable time string
     */
    private function formatTime($seconds) {
        $seconds = (int)$seconds;
        $hours = floor($seconds / 3600);
        $minutes = floor(($seconds % 3600) / 60);
        $seconds = $seconds % 60;
        
        return sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
    }
    
    /**
     * Format bytes into a human-readable string
     */
    private function formatBytes($bytes, $precision = 2) {
        $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];
    }
}
