<?php

namespace Core\App\Models;

use PDO;
use ZipArchive;

class SystemBackup
{
    protected PDO $db;
    protected string $backupDir;

    public function __construct()
    {
        $this->db = \db();

        $this->backupDir = rtrim(ROOT_PATH . '/storage/backups', '/');

        if (!is_dir($this->backupDir)) {
            mkdir($this->backupDir, 0750, true);
        }
    }

    /* ==========================
       TABLES
    ========================== */
    public function getTables(): array
    {
        $stmt = $this->db->query("SHOW TABLES");
        $tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
        return $tables ?: [];
    }

    private function quoteValue($value): string
    {
        if ($value === null) return 'NULL';
        if (is_bool($value)) return $value ? '1' : '0';
        if (is_int($value) || is_float($value)) return (string)$value;
        return $this->db->quote((string)$value);
    }

    private function safeFilename(string $name): string
    {
        $name = basename($name);
        $name = preg_replace('/[^a-zA-Z0-9\-\_\.\(\)]+/', '_', $name);
        return $name ?: 'backup.sql';
    }

    /* ==========================
       BACKUP DB (PDO)
    ========================== */
    public function createDatabaseBackup(array $onlyTables = [], bool $compress = true): ?string
    {
        $allTables = $this->getTables();

        $tables = $allTables;
        if (!empty($onlyTables)) {
            $tables = array_values(array_intersect($allTables, $onlyTables));
        }

        if (empty($tables)) {
            return null;
        }

        $date = date('Y-m-d_H-i-s');
        $filename = "backup_db_{$date}.sql";

        $sql = '';
        $sql .= "-- MTEngine Database Backup\n";
        $sql .= "-- Generated at: " . date('Y-m-d H:i:s') . "\n\n";
        $sql .= "SET NAMES utf8mb4;\n";
        $sql .= "SET FOREIGN_KEY_CHECKS=0;\n\n";

        foreach ($tables as $table) {

            $stmt = $this->db->query("SHOW CREATE TABLE `$table`");
            $row = $stmt->fetch(PDO::FETCH_ASSOC);

            $createSql = $row['Create Table'] ?? null;
            if (!$createSql) {
                continue;
            }

            $sql .= "-- ----------------------------\n";
            $sql .= "-- Table: `$table`\n";
            $sql .= "-- ----------------------------\n\n";
            $sql .= "DROP TABLE IF EXISTS `$table`;\n";
            $sql .= $createSql . ";\n\n";

            $dataStmt = $this->db->query("SELECT * FROM `$table`");
            while ($r = $dataStmt->fetch(PDO::FETCH_ASSOC)) {

                $cols = array_keys($r);
                $vals = array_map([$this, 'quoteValue'], array_values($r));

                $sql .= "INSERT INTO `$table` (`" . implode("`,`", $cols) . "`) VALUES (" . implode(",", $vals) . ");\n";
            }

            $sql .= "\n";
        }

        $sql .= "SET FOREIGN_KEY_CHECKS=1;\n";

        $fullPath = $this->backupDir . '/' . $filename;

        if (file_put_contents($fullPath, $sql) === false) {
            return null;
        }

        if ($compress) {
            $gzName = $filename . '.gz';
            $gzPath = $this->backupDir . '/' . $gzName;

            $data = file_get_contents($fullPath);
            if ($data === false) {
                return null;
            }

            $gz = gzopen($gzPath, 'wb9');
            if (!$gz) {
                return null;
            }

            gzwrite($gz, $data);
            gzclose($gz);

            @unlink($fullPath);

            return $gzName;
        }

        return $filename;
    }

    /* ==========================
       BACKUP FILES (ZIP)
    ========================== */
    public function createFilesBackup(array $folders): ?string
    {
        if (empty($folders)) {
            return null;
        }

        $date = date('Y-m-d_H-i-s');
        $zipName = "backup_files_{$date}.zip";
        $zipPath = $this->backupDir . '/' . $zipName;

        $zip = new ZipArchive();

        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            return null;
        }

        foreach ($folders as $folder) {
            $folder = trim((string)$folder);
            if ($folder === '') continue;

            $abs = rtrim(ROOT_PATH . '/' . ltrim($folder, '/'), '/');

            if (!is_dir($abs)) {
                continue;
            }

            $this->zipAddFolder($zip, $abs, $folder);
        }

        $zip->close();

        return file_exists($zipPath) ? $zipName : null;
    }

    /**
     * Sensitive paths excluded from every script/full backup.
     * Paths are relative to ROOT_PATH (no leading slash).
     */
    private const BACKUP_EXCLUSIONS = [
        '.env',
        'storage/credentials',
        'storage/settings',
        'storage/plg_lic_cache',
        'storage/backups',
    ];

    /**
     * ✅ NEW: Script backup for controller: createScriptBackupZip()
     * Zips the entire project ROOT_PATH, excluding sensitive files/dirs.
     */
    public function createScriptBackupZip(): ?string
    {
        $date = date('Y-m-d_H-i-s');
        $zipName = "backup_script_{$date}.zip";
        $zipPath = $this->backupDir . '/' . $zipName;

        $zip = new ZipArchive();

        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            return null;
        }

        // Zip the full project, excluding sensitive paths
        $this->zipAddFolder($zip, ROOT_PATH, '', self::BACKUP_EXCLUSIONS);

        $zip->close();

        return file_exists($zipPath) ? $zipName : null;
    }

    /* ==========================
       FULL BACKUP (DB + FILES)
    ========================== */
    public function createFullBackup(array $folders, array $onlyTables = [], bool $compressDb = true): ?string
    {
        $date = date('Y-m-d_H-i-s');
        $zipName = "backup_full_{$date}.zip";
        $zipPath = $this->backupDir . '/' . $zipName;

        $zip = new ZipArchive();

        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            return null;
        }

        // 1) DB backup as string
        $dbSqlContent = $this->generateDatabaseSql($onlyTables);

        if ($dbSqlContent === null) {
            $zip->close();
            @unlink($zipPath);
            return null;
        }

        if ($compressDb) {
            $zip->addFromString('database.sql.gz', gzencode($dbSqlContent, 9));
        } else {
            $zip->addFromString('database.sql', $dbSqlContent);
        }

        // 2) Add folders
        foreach ($folders as $folder) {
            $folder = trim((string)$folder);
            if ($folder === '') continue;

            $abs = rtrim(ROOT_PATH . '/' . ltrim($folder, '/'), '/');

            if (!is_dir($abs)) {
                continue;
            }

            $this->zipAddFolder($zip, $abs, $folder);
        }

        $zip->close();

        return file_exists($zipPath) ? $zipName : null;
    }

    /**
     * ✅ NEW: Full backup for controller: createFullBackupScriptAndDbZip()
     * This will generate DB + zip all project files in one ZIP.
     */
    public function createFullBackupScriptAndDbZip(array $onlyTables = [], bool $compressDb = true): ?string
    {
        $date = date('Y-m-d_H-i-s');
        $zipName = "backup_full_{$date}.zip";
        $zipPath = $this->backupDir . '/' . $zipName;

        $zip = new ZipArchive();

        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            return null;
        }

        // ✅ DB inside zip
        $dbSqlContent = $this->generateDatabaseSql($onlyTables);

        if ($dbSqlContent === null) {
            $zip->close();
            @unlink($zipPath);
            return null;
        }

        if ($compressDb) {
            $zip->addFromString('database.sql.gz', gzencode($dbSqlContent, 9));
        } else {
            $zip->addFromString('database.sql', $dbSqlContent);
        }

        // ✅ Script inside zip (full project, excluding sensitive paths)
        $this->zipAddFolder($zip, ROOT_PATH, 'script', self::BACKUP_EXCLUSIONS);

        $zip->close();

        return file_exists($zipPath) ? $zipName : null;
    }

    private function generateDatabaseSql(array $onlyTables = []): ?string
    {
        $allTables = $this->getTables();

        $tables = $allTables;
        if (!empty($onlyTables)) {
            $tables = array_values(array_intersect($allTables, $onlyTables));
        }

        if (empty($tables)) {
            return null;
        }

        $sql = '';
        $sql .= "-- MTEngine Database Backup\n";
        $sql .= "-- Generated at: " . date('Y-m-d H:i:s') . "\n\n";
        $sql .= "SET NAMES utf8mb4;\n";
        $sql .= "SET FOREIGN_KEY_CHECKS=0;\n\n";

        foreach ($tables as $table) {

            $stmt = $this->db->query("SHOW CREATE TABLE `$table`");
            $row = $stmt->fetch(PDO::FETCH_ASSOC);

            $createSql = $row['Create Table'] ?? null;
            if (!$createSql) {
                continue;
            }

            $sql .= "-- ----------------------------\n";
            $sql .= "-- Table: `$table`\n";
            $sql .= "-- ----------------------------\n\n";
            $sql .= "DROP TABLE IF EXISTS `$table`;\n";
            $sql .= $createSql . ";\n\n";

            $dataStmt = $this->db->query("SELECT * FROM `$table`");
            while ($r = $dataStmt->fetch(PDO::FETCH_ASSOC)) {

                $cols = array_keys($r);
                $vals = array_map([$this, 'quoteValue'], array_values($r));

                $sql .= "INSERT INTO `$table` (`" . implode("`,`", $cols) . "`) VALUES (" . implode(",", $vals) . ");\n";
            }

            $sql .= "\n";
        }

        $sql .= "SET FOREIGN_KEY_CHECKS=1;\n";

        return $sql;
    }

    /**
     * ✅ UPDATED: works with ROOT_PATH and empty relativeRoot.
     * Accepts an optional list of paths (relative to ROOT_PATH) to exclude.
     */
    private function zipAddFolder(ZipArchive $zip, string $absPath, string $relativeRoot, array $excludePaths = []): void
    {
        $absPath = rtrim(str_replace('\\', '/', $absPath), '/');
        $relativeRoot = trim($relativeRoot, '/');

        // Normalise exclusions to absolute paths (no trailing slash)
        $excludeAbs = array_map(function (string $rel) use ($absPath): string {
            return $absPath . '/' . ltrim(str_replace('\\', '/', $rel), '/');
        }, $excludePaths);

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($absPath, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $file) {
            $filePath = str_replace('\\', '/', $file->getPathname());

            // Skip if this path starts with any excluded path
            foreach ($excludeAbs as $excl) {
                if ($filePath === $excl || str_starts_with($filePath, $excl . '/')) {
                    continue 2;
                }
            }

            $subPath = ltrim(str_replace($absPath, '', $filePath), '/');

            $localPath = $relativeRoot !== ''
                ? ($relativeRoot . '/' . $subPath)
                : $subPath;

            if ($localPath === '') {
                continue;
            }

            if ($file->isDir()) {
                $zip->addEmptyDir($localPath);
            } else {
                $zip->addFile($filePath, $localPath);
            }
        }
    }

    /* ==========================
       BACKUPS MANAGER
    ========================== */
    public function listBackups(): array
    {
        $files = glob($this->backupDir . '/*') ?: [];

        $result = [];

        foreach ($files as $path) {
            if (!is_file($path)) continue;

            $name = basename($path);

            if (!preg_match('/\.(sql|gz|zip)$/i', $name)) {
                continue;
            }

            $size = filesize($path);

            $result[] = [
                'name'       => $name,
                'size'       => $size,
                'size_human' => $this->humanSize((int)$size),
                'date'       => date('Y-m-d H:i:s', filemtime($path)),
            ];
        }

        usort($result, function ($a, $b) {
            return strcmp($b['date'], $a['date']);
        });

        return $result;
    }

    public function deleteBackup(string $file): bool
    {
        $file = $this->safeFilename($file);
        $path = $this->backupDir . '/' . $file;

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

        return @unlink($path);
    }

    public function downloadBackup(string $file): void
    {
        $file = $this->safeFilename($file);
        $path = $this->backupDir . '/' . $file;

        if (!file_exists($path)) {
            http_response_code(404);
            exit;
        }

        $mime = 'application/octet-stream';

        if (str_ends_with($file, '.gz')) {
            $mime = 'application/gzip';
        } elseif (str_ends_with($file, '.sql')) {
            $mime = 'application/sql';
        } elseif (str_ends_with($file, '.zip')) {
            $mime = 'application/zip';
        }

        header('Content-Description: File Transfer');
        header('Content-Type: ' . $mime);
        header('Content-Disposition: attachment; filename="' . $file . '"');
        header('Content-Length: ' . filesize($path));
        header('Pragma: public');

        readfile($path);
        exit;
    }

    public function cleanupOldBackups(int $days = 30): int
    {
        $deleted = 0;
        $files = glob($this->backupDir . '/*') ?: [];

        $maxAge = time() - ($days * 86400);

        foreach ($files as $path) {
            if (!is_file($path)) continue;

            $name = basename($path);
            if (!preg_match('/\.(sql|gz|zip)$/i', $name)) {
                continue;
            }

            if (filemtime($path) < $maxAge) {
                if (@unlink($path)) {
                    $deleted++;
                }
            }
        }

        return $deleted;
    }

    private function humanSize(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $i = 0;

        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }

        return round($bytes, 2) . ' ' . $units[$i];
    }
}
