<?php

namespace WHPBackup;

require_once('/docker/whp/web/libs/mysqlmgmt.php');

use Exception;
use PDO;

/**
 * Manages local backup storage before remote upload
 * Handles storage allocation, cleanup policies, and space management
 */
class BackupLocalStorage {
    private $db;
    private $config;
    private $basePath;

    public function __construct(PDO $db = null) {
        if ($db === null) {
            $mysql = new \mysqlmgmt();
            $this->db = $mysql->getMySQLConnection();
            $this->db->exec("USE whp");
        } else {
            $this->db = $db;
        }

        $this->loadConfig();
        $this->ensureStorageDirectory();
    }

    private function loadConfig() {
        $stmt = $this->db->prepare("SELECT * FROM backup_local_storage WHERE is_active = 1 ORDER BY id LIMIT 1");
        $stmt->execute();
        $this->config = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$this->config) {
            // Self-heal: the default config row is seeded by the 2025.09.27
            // migration, but a rebuilt server (DB recreated from
            // complete-database-setup.sql, which ships this table EMPTY) or a
            // fresh install that never crossed that migration boundary ends up
            // with no row. That previously crashed BOTH the creation worker
            // (new BackupJob() -> here) and the upload worker with "No active
            // local storage configuration found", leaving every backup stuck
            // at status 'claimed'. Seed the default and re-load.
            $this->seedDefaultConfig();
            $stmt->execute();
            $this->config = $stmt->fetch(PDO::FETCH_ASSOC);
        }

        if (!$this->config) {
            throw new Exception("No active local storage configuration found");
        }

        $this->basePath = rtrim($this->config['storage_path'], '/');
    }

    /**
     * Seed the default local-storage config when none exists. Only acts on a
     * completely empty table — if rows exist but none are active, that's a
     * deliberate admin state and we leave it alone (loadConfig() then surfaces
     * the original error). Values mirror the 2025.09.27 migration default.
     */
    private function seedDefaultConfig() {
        $count = (int)$this->db->query("SELECT COUNT(*) FROM backup_local_storage")->fetchColumn();
        if ($count > 0) {
            return;
        }
        $this->db->exec(
            "INSERT INTO backup_local_storage "
            . "(storage_path, max_size_gb, cleanup_policy, retention_hours, is_active) "
            . "VALUES ('/docker/backups/local', 100, 'after_upload', 24, 1)"
        );
        error_log("BackupLocalStorage: seeded default local storage config (/docker/backups/local) — table was empty");
    }

    private function ensureStorageDirectory() {
        if (!is_dir($this->basePath)) {
            if (!mkdir($this->basePath, 0755, true)) {
                throw new Exception("Failed to create local storage directory: {$this->basePath}");
            }
        }

        // Create subdirectories for organization
        $subdirs = ['temp', 'ready', 'failed'];
        foreach ($subdirs as $subdir) {
            $path = $this->basePath . '/' . $subdir;
            if (!is_dir($path)) {
                if (!mkdir($path, 0755, true)) {
                    throw new Exception("Failed to create subdirectory: {$path}");
                }
            }
        }
    }

    /**
     * Allocate space for a new backup and return the local path
     * @param string $backupName Name of the backup file
     * @param int $estimatedSize Estimated size in bytes
     * @return string Local file path for the backup
     */
    public function allocateSpace($backupName, $estimatedSize = 0) {
        // Check if we have enough space
        $this->ensureSufficientSpace($estimatedSize);

        // Generate unique filename to avoid conflicts
        $timestamp = date('Y-m-d_H-i-s');
        $randomSuffix = bin2hex(random_bytes(4));
        $filename = "{$timestamp}_{$randomSuffix}_{$backupName}";

        // Start in temp directory during creation
        $localPath = $this->basePath . '/temp/' . $filename;

        return $localPath;
    }

    /**
     * Mark backup creation as completed and move to ready directory
     * @param string $tempPath Current temp path
     * @param int $actualSize Actual file size
     * @return string Final ready path
     */
    public function markCreated($tempPath, $actualSize) {
        $filename = basename($tempPath);
        $readyPath = $this->basePath . '/ready/' . $filename;

        if (!file_exists($tempPath)) {
            throw new Exception("Temp backup file not found: {$tempPath}");
        }

        // Move from temp to ready
        if (!rename($tempPath, $readyPath)) {
            throw new Exception("Failed to move backup from temp to ready: {$tempPath} -> {$readyPath}");
        }

        // Update storage usage
        $this->updateStorageUsage($actualSize);

        return $readyPath;
    }

    /**
     * Mark backup as failed and move to failed directory for investigation
     * @param string $tempPath Current temp path
     * @return string Failed path
     */
    public function markFailed($tempPath) {
        if (!file_exists($tempPath)) {
            return null; // Already cleaned up
        }

        $filename = basename($tempPath);
        $failedPath = $this->basePath . '/failed/' . $filename;

        if (!rename($tempPath, $failedPath)) {
            // If move fails, just delete the temp file
            unlink($tempPath);
            return null;
        }

        return $failedPath;
    }

    /**
     * Delete local backup file after successful upload
     * @param string $localPath Path to local backup file
     * @param int $backupId Backup ID for tracking
     */
    public function deleteAfterUpload($localPath, $backupId) {
        if (!file_exists($localPath)) {
            return; // Already deleted
        }

        $fileSize = filesize($localPath);

        if (unlink($localPath)) {
            // Update storage usage
            $this->updateStorageUsage(-$fileSize);

            // Update database to mark local file as deleted
            $stmt = $this->db->prepare("
                UPDATE backup_history
                SET local_deleted_at = NOW()
                WHERE id = ?
            ");
            $stmt->execute([$backupId]);

            error_log("Deleted local backup after upload: {$localPath} (freed " . number_format($fileSize/1024/1024, 2) . " MB)");
        } else {
            error_log("Failed to delete local backup: {$localPath}");
        }
    }

    /**
     * Get available space information
     * @return array Space information
     */
    public function getSpaceInfo() {
        $maxSizeBytes = $this->config['max_size_gb'] * 1024 * 1024 * 1024;
        $currentSizeBytes = $this->getCurrentUsage();
        $availableBytes = $maxSizeBytes - $currentSizeBytes;

        // Sync database with actual disk usage
        $this->syncStorageUsage($currentSizeBytes);

        return [
            'max_size_bytes' => $maxSizeBytes,
            'max_size_gb' => $this->config['max_size_gb'],
            'current_size_bytes' => $currentSizeBytes,
            'current_size_gb' => $currentSizeBytes / (1024 * 1024 * 1024),
            'available_bytes' => $availableBytes,
            'available_gb' => $availableBytes / (1024 * 1024 * 1024),
            'usage_percentage' => ($currentSizeBytes / $maxSizeBytes) * 100
        ];
    }

    private function syncStorageUsage($actualBytes) {
        try {
            $stmt = $this->db->prepare("
                UPDATE backup_local_storage
                SET current_size_bytes = ?
                WHERE id = ?
            ");
            $stmt->execute([$actualBytes, $this->config['id']]);
        } catch (Exception $e) {
            error_log("Error syncing storage usage: " . $e->getMessage());
        }
    }

    /**
     * Cleanup old backups based on configured policy
     * @return array Cleanup results
     */
    public function performCleanup() {
        $results = [
            'files_deleted' => 0,
            'bytes_freed' => 0,
            'errors' => []
        ];

        switch ($this->config['cleanup_policy']) {
            case 'immediate':
                // Don't keep any local files (handled by deleteAfterUpload)
                break;

            case 'after_upload':
                // Default behavior - delete after successful upload
                $results = array_merge($results, $this->cleanupUploadedBackups());
                break;

            case 'age_based':
                $results = array_merge($results, $this->cleanupByAge());
                break;

            case 'size_based':
                $results = array_merge($results, $this->cleanupBySize());
                break;
        }

        // Always cleanup failed backups older than 7 days
        $failedResults = $this->cleanupFailedBackups();
        $results['files_deleted'] += $failedResults['files_deleted'];
        $results['bytes_freed'] += $failedResults['bytes_freed'];
        $results['errors'] = array_merge($results['errors'], $failedResults['errors']);

        return $results;
    }

    private function ensureSufficientSpace($estimatedSize) {
        $spaceInfo = $this->getSpaceInfo();

        // If we need more space than available, trigger cleanup
        if ($estimatedSize > $spaceInfo['available_bytes']) {
            $this->performCleanup();

            // Recheck after cleanup
            $spaceInfo = $this->getSpaceInfo();
            if ($estimatedSize > $spaceInfo['available_bytes']) {
                throw new Exception("Insufficient local storage space. Need: " .
                    number_format($estimatedSize/1024/1024, 2) . " MB, Available: " .
                    number_format($spaceInfo['available_bytes']/1024/1024, 2) . " MB");
            }
        }
    }

    private function getCurrentUsage() {
        // Get actual disk usage for accuracy
        $totalSize = 0;
        $directories = ['temp', 'ready', 'failed'];

        foreach ($directories as $dir) {
            $dirPath = $this->basePath . '/' . $dir;
            if (is_dir($dirPath)) {
                $totalSize += $this->getDirectorySize($dirPath);
            }
        }

        return $totalSize;
    }

    private function getDirectorySize($directory) {
        $size = 0;
        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory));

        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $size += $file->getSize();
            }
        }

        return $size;
    }

    private function updateStorageUsage($sizeDelta) {
        // Update current usage in database
        $stmt = $this->db->prepare("
            UPDATE backup_local_storage
            SET current_size_bytes = GREATEST(0, current_size_bytes + ?)
            WHERE id = ?
        ");
        $stmt->execute([$sizeDelta, $this->config['id']]);
    }

    private function cleanupUploadedBackups() {
        $results = ['files_deleted' => 0, 'bytes_freed' => 0, 'errors' => []];

        // Find backups that have been uploaded but local file still exists
        $stmt = $this->db->prepare("
            SELECT id, local_path, local_size
            FROM backup_history
            WHERE upload_status = 'uploaded'
            AND local_path IS NOT NULL
            AND local_deleted_at IS NULL
        ");
        $stmt->execute();

        while ($backup = $stmt->fetch(PDO::FETCH_ASSOC)) {
            if (file_exists($backup['local_path'])) {
                $this->deleteAfterUpload($backup['local_path'], $backup['id']);
                $results['files_deleted']++;
                $results['bytes_freed'] += $backup['local_size'] ?? 0;
            }
        }

        return $results;
    }

    private function cleanupByAge() {
        $results = ['files_deleted' => 0, 'bytes_freed' => 0, 'errors' => []];
        $maxAge = $this->config['retention_hours'];

        $cutoffTime = date('Y-m-d H:i:s', strtotime("-{$maxAge} hours"));

        $stmt = $this->db->prepare("
            SELECT id, local_path, local_size
            FROM backup_history
            WHERE local_path IS NOT NULL
            AND local_deleted_at IS NULL
            AND started_at < ?
        ");
        $stmt->execute([$cutoffTime]);

        while ($backup = $stmt->fetch(PDO::FETCH_ASSOC)) {
            if (file_exists($backup['local_path'])) {
                try {
                    $this->deleteAfterUpload($backup['local_path'], $backup['id']);
                    $results['files_deleted']++;
                    $results['bytes_freed'] += $backup['local_size'] ?? 0;
                } catch (Exception $e) {
                    $results['errors'][] = "Failed to delete {$backup['local_path']}: " . $e->getMessage();
                }
            }
        }

        return $results;
    }

    private function cleanupBySize() {
        $results = ['files_deleted' => 0, 'bytes_freed' => 0, 'errors' => []];
        $spaceInfo = $this->getSpaceInfo();
        $lowWatermarkBytes = $this->config['low_watermark_gb'] * 1024 * 1024 * 1024;

        if ($spaceInfo['current_size_bytes'] > $lowWatermarkBytes) {
            // Delete oldest files first until we're under the watermark
            $stmt = $this->db->prepare("
                SELECT id, local_path, local_size
                FROM backup_history
                WHERE local_path IS NOT NULL
                AND local_deleted_at IS NULL
                ORDER BY started_at ASC
            ");
            $stmt->execute();

            while ($backup = $stmt->fetch(PDO::FETCH_ASSOC)) {
                if (file_exists($backup['local_path'])) {
                    try {
                        $this->deleteAfterUpload($backup['local_path'], $backup['id']);
                        $results['files_deleted']++;
                        $results['bytes_freed'] += $backup['local_size'] ?? 0;

                        // Check if we're now under the watermark
                        $spaceInfo = $this->getSpaceInfo();
                        if ($spaceInfo['current_size_bytes'] <= $lowWatermarkBytes) {
                            break;
                        }
                    } catch (Exception $e) {
                        $results['errors'][] = "Failed to delete {$backup['local_path']}: " . $e->getMessage();
                    }
                }
            }
        }

        return $results;
    }

    private function cleanupFailedBackups() {
        $results = ['files_deleted' => 0, 'bytes_freed' => 0, 'errors' => []];
        $failedDir = $this->basePath . '/failed';

        if (!is_dir($failedDir)) {
            return $results;
        }

        $cutoffTime = time() - (7 * 24 * 3600); // 7 days ago

        $files = glob($failedDir . '/*');
        foreach ($files as $file) {
            if (is_file($file) && filemtime($file) < $cutoffTime) {
                $fileSize = filesize($file);
                if (unlink($file)) {
                    $results['files_deleted']++;
                    $results['bytes_freed'] += $fileSize;
                } else {
                    $results['errors'][] = "Failed to delete old failed backup: {$file}";
                }
            }
        }

        return $results;
    }
}