<?php

/**
 * ContentBackup — backup and restore the content directory.
 *
 * Supports:
 * - ZIP backup of the entire content directory
 * - Restore from an uploaded ZIP file
 * - Optional git-based versioning (init / commit / log / restore)
 */
class ContentBackup
{
    private string $contentDir;
    private string $projectRoot;
    private bool $gitAvailable;

    public function __construct(string $contentDir, string $projectRoot = '')
    {
        $this->contentDir = rtrim($contentDir, '/');
        $this->projectRoot = $projectRoot !== '' ? rtrim($projectRoot, '/') : dirname(__DIR__, 3);
        $this->gitAvailable = $this->detectGit();
    }

    /**
     * Check whether git is available and the content dir is inside a git repo.
     */
    public function isGitAvailable(): bool
    {
        return $this->gitAvailable;
    }

    /**
     * Create a ZIP backup of the content directory.
     *
     * @param string $outputPath Where to write the ZIP file
     * @return bool True on success
     */
    public function createZipBackup(string $outputPath): bool
    {
        if (!class_exists('ZipArchive')) {
            return false;
        }
        if (!is_dir($this->contentDir)) {
            return false;
        }

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

        $this->addDirToZip($zip, $this->contentDir, 'content');
        return $zip->close();
    }

    /**
     * Restore content from a ZIP file.
     *
     * @param string $zipPath Path to the uploaded ZIP file
     * @return array ['success' => bool, 'message' => string]
     */
    public function restoreFromZip(string $zipPath): array
    {
        if (!class_exists('ZipArchive')) {
            return ['success' => false, 'message' => 'ZipArchive niet beschikbaar.'];
        }
        if (!file_exists($zipPath)) {
            return ['success' => false, 'message' => 'ZIP bestand niet gevonden.'];
        }

        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            return ['success' => false, 'message' => 'Kan ZIP bestand niet openen.'];
        }

        $tempDir = $this->projectRoot . '/var/tmp/restore_' . date('YmdHis');
        if (!@mkdir($tempDir, 0755, true)) {
            $zip->close();
            return ['success' => false, 'message' => 'Kan tijdelijke map niet aanmaken.'];
        }

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

        // Determine source: either $tempDir/content/ or $tempDir/ itself
        $sourceDir = is_dir($tempDir . '/content') ? $tempDir . '/content' : $tempDir;

        // Backup current content before overwriting
        $backupSubDir = $this->contentDir . '.bak.' . date('YmdHis');
        if (is_dir($this->contentDir)) {
            rename($this->contentDir, $backupSubDir);
        }

        if (!@mkdir($this->contentDir, 0755, true)) {
            // Restore old content if mkdir fails
            if (is_dir($backupSubDir)) {
                rename($backupSubDir, $this->contentDir);
            }
            $this->removeDir($tempDir);
            return ['success' => false, 'message' => 'Kan content map niet aanmaken.'];
        }

        $this->copyDir($sourceDir, $this->contentDir);

        // Clean up temp dir
        $this->removeDir($tempDir);

        // Remove old backup (keep last one)
        $this->cleanupOldBackups();

        return ['success' => true, 'message' => 'Content succesvol hersteld.'];
    }

    /**
     * Initialize git in the content directory.
     *
     * @return array ['success' => bool, 'message' => string]
     */
    public function gitInit(): array
    {
        if (!$this->gitAvailable) {
            return ['success' => false, 'message' => 'Git is niet beschikbaar.'];
        }

        $result = $this->execGitCapture(['init'], $this->contentDir);
        if ($result['exit'] !== 0) {
            return ['success' => false, 'message' => 'Git init mislukt: ' . trim($result['error'])];
        }

        // Configure a default identity so commits work without global git config
        $this->execGitCapture(['config', 'user.email', 'content@codepress.local'], $this->contentDir);
        $this->execGitCapture(['config', 'user.name', 'CodePress CMS'], $this->contentDir);

        return ['success' => true, 'message' => 'Git repository geïnitialiseerd in content/.'];
    }

    /**
     * Commit all changes in the content directory.
     *
     * @param string $message Commit message
     * @return array ['success' => bool, 'message' => string]
     */
    public function gitCommit(string $message): array
    {
        if (!$this->gitAvailable) {
            return ['success' => false, 'message' => 'Git is niet beschikbaar.'];
        }

        if (!$this->hasGitRepo()) {
            return ['success' => false, 'message' => 'Geen git repository in content/. Voer eerst git init uit.'];
        }

        $msg = trim($message) !== '' ? $message : 'Content update ' . date('Y-m-d H:i:s');

        // Add all files
        $addResult = $this->execGitCapture(['add', '-A'], $this->contentDir);
        if ($addResult['exit'] !== 0) {
            return ['success' => false, 'message' => 'Git add mislukt: ' . trim($addResult['error'])];
        }

        // Check if there are changes to commit
        $statusResult = $this->execGitCapture(['status', '--porcelain'], $this->contentDir);
        if (trim($statusResult['output']) === '') {
            return ['success' => true, 'message' => 'Geen wijzigingen om te committen.'];
        }

        $commitResult = $this->execGitCapture(['commit', '-m', $msg], $this->contentDir);
        if ($commitResult['exit'] !== 0) {
            return ['success' => false, 'message' => 'Git commit mislukt: ' . trim($commitResult['error'])];
        }

        return ['success' => true, 'message' => 'Wijzigingen gecommit: ' . $msg];
    }

    /**
     * Get the git log for the content directory.
     *
     * @param int $limit Maximum number of entries
     * @return array ['success' => bool, 'commits' => array, 'message' => string]
     */
    public function gitLog(int $limit = 20): array
    {
        if (!$this->gitAvailable) {
            return ['success' => false, 'commits' => [], 'message' => 'Git is niet beschikbaar.'];
        }

        if (!$this->hasGitRepo()) {
            return ['success' => false, 'commits' => [], 'message' => 'Geen git repository.'];
        }

        $result = $this->execGitCapture(
            ['log', '--pretty=format:%H|%h|%ai|%s', '-' . (string)$limit],
            $this->contentDir
        );

        if ($result['exit'] !== 0) {
            // No commits yet is not an error in our context
            if (str_contains($result['error'], 'does not have any commits')) {
                return ['success' => true, 'commits' => [], 'message' => ''];
            }
            return ['success' => false, 'commits' => [], 'message' => 'Kan git log niet uitlezen: ' . trim($result['error'])];
        }

        $commits = [];
        $lines = explode("\n", trim($result['output']));
        if ($lines !== ['']) {
            foreach ($lines as $line) {
                $parts = explode('|', $line, 4);
                if (count($parts) === 4) {
                    $commits[] = [
                        'hash' => $parts[0],
                        'short' => $parts[1],
                        'date' => $parts[2],
                        'message' => $parts[3],
                    ];
                }
            }
        }

        return ['success' => true, 'commits' => $commits, 'message' => ''];
    }

    /**
     * Restore content to a specific git commit.
     *
     * @param string $commitHash Commit hash to restore to
     * @return array ['success' => bool, 'message' => string]
     */
    public function gitRestore(string $commitHash): array
    {
        if (!$this->gitAvailable) {
            return ['success' => false, 'message' => 'Git is niet beschikbaar.'];
        }

        if (!$this->hasGitRepo()) {
            return ['success' => false, 'message' => 'Geen git repository.'];
        }

        $hash = preg_replace('/[^a-f0-9]/', '', $commitHash);
        if ($hash === '') {
            return ['success' => false, 'message' => 'Ongeldige commit hash.'];
        }

        // checkout <hash> -- . restores files from that commit into the working tree
        $result = $this->execGitCapture(['checkout', $hash, '--', '.'], $this->contentDir);
        if ($result['exit'] !== 0) {
            return ['success' => false, 'message' => 'Git restore mislukt: ' . trim($result['error'])];
        }

        return ['success' => true, 'message' => 'Content hersteld naar commit ' . substr($hash, 0, 7)];
    }

    /**
     * Check if the content directory has a git repository.
     */
    public function hasGitRepo(): bool
    {
        return is_dir($this->contentDir . '/.git');
    }

    // -----------------------------------------------------------------------
    // Private helpers
    // -----------------------------------------------------------------------

    private function detectGit(): bool
    {
        $result = $this->execGitCapture(['--version']);
        return $result['exit'] === 0;
    }

    /**
     * Execute a git command and capture output, error, and exit code separately.
     *
     * @return array ['output' => string, 'error' => string, 'exit' => int]
     */
    private function execGitCapture(array $args, ?string $cwd = null): array
    {
        $escaped = array_map('escapeshellarg', $args);
        $command = 'git ' . implode(' ', $escaped);

        $descriptors = [
            0 => ['pipe', 'r'],   // stdin
            1 => ['pipe', 'w'],   // stdout
            2 => ['pipe', 'w'],   // stderr
        ];

        $process = proc_open($command, $descriptors, $pipes, $cwd ?? null);

        if (!is_resource($process)) {
            return ['output' => '', 'error' => 'Kan git proces niet starten.', 'exit' => -1];
        }

        $output = stream_get_contents($pipes[1]);
        $error = stream_get_contents($pipes[2]);
        fclose($pipes[0]);
        fclose($pipes[1]);
        fclose($pipes[2]);

        $exitCode = proc_close($process);

        return [
            'output' => $output !== false ? $output : '',
            'error' => $error !== false ? $error : '',
            'exit' => $exitCode,
        ];
    }

    private function addDirToZip(ZipArchive $zip, string $dir, string $prefix): void
    {
        $entries = scandir($dir);
        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $path = $dir . '/' . $entry;
            $zipPath = $prefix . '/' . $entry;

            if (is_dir($path)) {
                $zip->addEmptyDir($zipPath);
                $this->addDirToZip($zip, $path, $zipPath);
            } elseif (is_file($path)) {
                $zip->addFile($path, $zipPath);
            }
        }
    }

    private function copyDir(string $src, string $dst): void
    {
        if (!is_dir($src)) {
            return;
        }

        if (!is_dir($dst)) {
            @mkdir($dst, 0755, true);
        }

        $entries = scandir($src);
        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $srcPath = $src . '/' . $entry;
            $dstPath = $dst . '/' . $entry;

            if (is_dir($srcPath)) {
                $this->copyDir($srcPath, $dstPath);
            } elseif (is_file($srcPath)) {
                copy($srcPath, $dstPath);
            }
        }
    }

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

        $entries = scandir($dir);
        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $path = $dir . '/' . $entry;

            if (is_dir($path)) {
                $this->removeDir($path);
            } else {
                unlink($path);
            }
        }

        rmdir($dir);
    }

    private function cleanupOldBackups(): void
    {
        $parentDir = dirname($this->contentDir);
        $baseName = basename($this->contentDir);
        $pattern = $parentDir . '/' . $baseName . '.bak.*';

        $backups = glob($pattern);
        if ($backups === false || count($backups) <= 1) {
            return;
        }

        // Sort by modification time (oldest first)
        usort($backups, function ($a, $b) {
            return filemtime($a) - filemtime($b);
        });

        // Remove all but the last backup
        $toRemove = array_slice($backups, 0, count($backups) - 1);
        foreach ($toRemove as $backup) {
            $this->removeDir($backup);
        }
    }
}