<?php

namespace App\Services\Marketplace;

/**
 * Creates pre-install snapshots and restores them on failure.
 *
 * Backup layout inside ZIP:
 *   source/   → plugins/{slug}/ or themes/{slug}/
 *   public/   → public/plugins/{slug}/ or public/themes/{slug}/
 *   backup.json → metadata
 */
class RollbackManager
{
    private string $basePath;
    private string $backupRoot;

    public function __construct(string $basePath)
    {
        $this->basePath   = rtrim($basePath, '/\\');
        $this->backupRoot = $this->basePath . '/storage/marketplace/backups';
    }

    // ─────────────────────────────────────────────────────────
    // Public API
    // ─────────────────────────────────────────────────────────

    /**
     * Snapshot the current state of a package before install/update.
     *
     * @param string $type 'plugin' or 'theme'
     * @param string $slug Package slug
     * @return string Backup ZIP path, or '' if nothing exists to back up.
     */
    public function snapshot(string $type, string $slug): string
    {
        [$sourceDir, $publicDir] = $this->resolvePaths($type, $slug);

        if (!is_dir($sourceDir) && !is_dir($publicDir)) {
            return '';
        }

        if (!class_exists('ZipArchive')) {
            return '';
        }

        $backupDir = $this->backupRoot . '/' . $type . '/' . $slug;
        if (!is_dir($backupDir) && !mkdir($backupDir, 0750, true)) {
            return '';
        }

        $version   = $this->detectVersion($type, $slug);
        $timestamp = date('YmdHis');
        $zipPath   = $backupDir . '/' . $slug . '-' . $version . '-' . $timestamp . '.zip';

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

        if (is_dir($sourceDir)) {
            $this->addDirectoryToZip($zip, $sourceDir, 'source');
        }

        if (is_dir($publicDir)) {
            $this->addDirectoryToZip($zip, $publicDir, 'public');
        }

        $meta = json_encode([
            'slug'        => $slug,
            'type'        => $type,
            'version'     => $version,
            'cms_version' => cms_version(),
            'created_at'  => date('c'),
        ], JSON_UNESCAPED_UNICODE);
        $zip->addFromString('backup.json', $meta);

        $zip->close();

        return $zipPath;
    }

    /**
     * Restore a package from a backup ZIP.
     *
     * @param string $backupZip Absolute path to the backup ZIP.
     * @param string $type      'plugin' or 'theme'
     * @param string $slug      Package slug
     */
    public function restore(string $backupZip, string $type, string $slug): void
    {
        if (!is_file($backupZip)) {
            throw new \RuntimeException("Backup file not found: {$backupZip}");
        }

        if (!class_exists('ZipArchive')) {
            throw new \RuntimeException('ZipArchive is required for restore.');
        }

        [$sourceDir, $publicDir] = $this->resolvePaths($type, $slug);

        // Remove current installation
        if (is_dir($sourceDir)) {
            $this->removeDirectory($sourceDir);
        }
        if (is_dir($publicDir)) {
            $this->removeDirectory($publicDir);
        }

        $zip = new \ZipArchive();
        if ($zip->open($backupZip) !== true) {
            throw new \RuntimeException("Could not open backup ZIP: {$backupZip}");
        }

        $tempDir = sys_get_temp_dir() . '/iw_restore_' . bin2hex(random_bytes(8));
        if (!mkdir($tempDir, 0700, true)) {
            $zip->close();
            throw new \RuntimeException('Could not create temp directory for restore.');
        }

        try {
            $zip->extractTo($tempDir);
            $zip->close();

            if (is_dir($tempDir . '/source')) {
                $this->copyDirectory($tempDir . '/source', $sourceDir);
            }
            if (is_dir($tempDir . '/public')) {
                $this->copyDirectory($tempDir . '/public', $publicDir);
            }
        } finally {
            $this->removeDirectory($tempDir);
        }
    }

    /**
     * Delete old backups, keeping only the $keep most recent.
     */
    public function cleanupOldBackups(string $type, string $slug, int $keep = 3): void
    {
        $backups = $this->listBackups($type, $slug);

        if (count($backups) <= $keep) {
            return;
        }

        // Already sorted newest-first; remove the oldest ones
        $toDelete = array_slice($backups, $keep);
        foreach ($toDelete as $backup) {
            if (is_file($backup['path'])) {
                @unlink($backup['path']);
            }
        }
    }

    /**
     * List available backups for a slug, newest first.
     *
     * @return array<array{path: string, filename: string, created_at: string, size: int}>
     */
    public function listBackups(string $type, string $slug): array
    {
        $backupDir = $this->backupRoot . '/' . $type . '/' . $slug;

        if (!is_dir($backupDir)) {
            return [];
        }

        $files = glob($backupDir . '/*.zip');
        if ($files === false || $files === []) {
            return [];
        }

        $backups = [];
        foreach ($files as $path) {
            $backups[] = [
                'path'       => $path,
                'filename'   => basename($path),
                'created_at' => date('c', filemtime($path)),
                'size'       => filesize($path),
            ];
        }

        // Sort newest first (by mtime desc)
        usort($backups, fn($a, $b) => strcmp($b['created_at'], $a['created_at']));

        return $backups;
    }

    // ─────────────────────────────────────────────────────────
    // Private helpers
    // ─────────────────────────────────────────────────────────

    private function resolvePaths(string $type, string $slug): array
    {
        if ($type === 'theme') {
            return [
                $this->basePath . '/themes/' . $slug,
                $this->basePath . '/public/themes/' . $slug,
            ];
        }

        return [
            $this->basePath . '/plugins/' . $slug,
            $this->basePath . '/public/plugins/' . $slug,
        ];
    }

    private function detectVersion(string $type, string $slug): string
    {
        [$sourceDir] = $this->resolvePaths($type, $slug);

        $manifestFile = $type === 'theme'
            ? $sourceDir . '/theme.json'
            : $sourceDir . '/plugin.json';

        if (!is_file($manifestFile)) {
            return '0.0.0';
        }

        $data = json_decode(file_get_contents($manifestFile), true);
        return trim((string) ($data['version'] ?? '0.0.0'));
    }

    private function addDirectoryToZip(\ZipArchive $zip, string $dir, string $prefix): void
    {
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            $relative = $prefix . '/' . ltrim(substr($item->getPathname(), strlen($dir)), '/\\');
            $relative = str_replace('\\', '/', $relative);

            if ($item->isDir()) {
                $zip->addEmptyDir($relative);
            } else {
                $zip->addFile($item->getPathname(), $relative);
            }
        }
    }

    private function copyDirectory(string $src, string $dst): void
    {
        if (!is_dir($dst)) {
            mkdir($dst, 0755, true);
        }

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

        foreach ($iterator as $item) {
            $relative = substr($item->getPathname(), strlen($src));
            $target   = $dst . $relative;

            if ($item->isDir()) {
                if (!is_dir($target)) {
                    mkdir($target, 0755, true);
                }
            } else {
                $targetDir = dirname($target);
                if (!is_dir($targetDir)) {
                    mkdir($targetDir, 0755, true);
                }
                copy($item->getPathname(), $target);
            }
        }
    }

    private function removeDirectory(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($iterator as $item) {
            if ($item->isDir()) {
                rmdir($item->getPathname());
            } else {
                unlink($item->getPathname());
            }
        }

        rmdir($dir);
    }
}
