<?php

declare(strict_types=1);

namespace PpPhp\Core;

/**
 * Version History — automatic, restorable snapshots of a project's design.
 *
 * A "version" is a bundle of the project's design/settings state: every
 * `config/*.json` module file (Tables, Pages, Fields, Designer, Style, Misc,
 * Security, Events, Query, Output). Connection secrets (`database.php`),
 * build/deploy history and the per-user resume pointer are deliberately
 * excluded so going back to a version never breaks the database link or the
 * deployment setup — it only restores what the user designed.
 *
 * Snapshots live on disk next to the project config:
 *   projects/{user}/{slug}/versions/index.json   (metadata, one row per version)
 *   projects/{user}/{slug}/versions/{id}.json     (the bundle: file → contents)
 *
 * Filesystem storage keeps this in lock-step with how the app already reads
 * and writes project state (ModuleState), and leaves the door open for future
 * work (diffs between versions, richer labels from designer actions, etc.).
 */
class VersionHistory
{
    /** Keep the history focused and the folder small. */
    private const MAX_VERSIONS = 40;

    /** Minimum spacing between automatic snapshots (seconds). */
    private const AUTO_THROTTLE = 480; // 8 minutes

    /** State files that must NOT be captured or overwritten by a restore. */
    private const EXCLUDE = ['_resume.json', 'database.php', 'finished.json', 'deploy.json'];

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

    /**
     * Capture the current design as a new version.
     *
     * @param string $type One of: auto | checkpoint | build | safety.
     * @return array|null  The new version's metadata, or null if nothing was
     *                     captured (empty project, or an unchanged auto-save).
     */
    public static function snapshot(string $slug, string $description, string $type = 'checkpoint'): ?array
    {
        $bundle = self::bundleCurrent($slug);
        if ($bundle === []) {
            return null;
        }

        $hash  = self::hashBundle($bundle);
        $index = self::readIndex($slug);

        // An automatic snapshot (auto-save or build) that would be identical to
        // the newest version is pointless noise — skip it. Manual checkpoints
        // and safety copies are always kept.
        if (($type === 'auto' || $type === 'build') && $index !== []) {
            $latest = $index[array_key_last($index)];
            if (($latest['hash'] ?? '') === $hash) {
                return null;
            }
        }

        $id  = date('Ymd_His') . '_' . substr(bin2hex(random_bytes(4)), 0, 6);
        $dir = self::versionsDir($slug);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        file_put_contents($dir . "/{$id}.json", json_encode($bundle, JSON_UNESCAPED_SLASHES));

        $meta = [
            'id'          => $id,
            'ts'          => time(),
            'description' => $description,
            'type'        => $type,
            'hash'        => $hash,
            'files'       => count($bundle),
            'bytes'       => array_sum(array_map('strlen', $bundle)),
            'by'          => (string) ($_SESSION['user']['username'] ?? ''),
        ];
        $index[] = $meta;
        self::prune($slug, $index);
        self::writeIndex($slug, $index);

        return $meta;
    }

    /**
     * Create an automatic snapshot, but only if enough time has passed since
     * the last one. Safe to call on every save.
     */
    public static function maybeAutoSnapshot(string $slug): void
    {
        $index = self::readIndex($slug);
        if ($index !== []) {
            $latest = $index[array_key_last($index)];
            if (time() - (int) ($latest['ts'] ?? 0) < self::AUTO_THROTTLE) {
                return;
            }
        }
        self::snapshot($slug, 'Auto-save', 'auto');
    }

    /** All versions, newest first (metadata only — no bundle contents). */
    public static function list(string $slug): array
    {
        $index = self::readIndex($slug);
        usort($index, static fn(array $a, array $b): int => ($b['ts'] ?? 0) <=> ($a['ts'] ?? 0));
        return array_map(static function (array $m): array {
            unset($m['hash']);
            return $m;
        }, $index);
    }

    public static function count(string $slug): int
    {
        return count(self::readIndex($slug));
    }

    /**
     * Bring the project back to a saved version. The current design is first
     * captured as a "safety" version, so nothing the user has now is ever lost.
     *
     * @return bool true on success, false if the version was not found.
     */
    public static function restore(string $slug, string $id): bool
    {
        $id = self::safeId($id);
        $bundleFile = self::versionsDir($slug) . "/{$id}.json";
        if ($id === '' || !is_file($bundleFile)) {
            return false;
        }
        $bundle = json_decode((string) file_get_contents($bundleFile), true);
        if (!is_array($bundle)) {
            return false;
        }

        // Preserve current work before overwriting it.
        self::snapshot($slug, 'Before you went back to an earlier version', 'safety');

        $dir = self::configDir($slug);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        foreach ($bundle as $file => $contents) {
            $safe = basename((string) $file);
            if (!str_ends_with($safe, '.json') || in_array($safe, self::EXCLUDE, true)) {
                continue;
            }
            file_put_contents($dir . '/' . $safe, (string) $contents);
        }
        return true;
    }

    public static function delete(string $slug, string $id): void
    {
        $id = self::safeId($id);
        if ($id === '') {
            return;
        }
        $index = array_values(array_filter(
            self::readIndex($slug),
            static fn(array $m): bool => ($m['id'] ?? '') !== $id
        ));
        @unlink(self::versionsDir($slug) . "/{$id}.json");
        self::writeIndex($slug, $index);
    }

    // ── Internals ──────────────────────────────────────────────────────

    private static function bundleCurrent(string $slug): array
    {
        $dir = self::configDir($slug);
        if (!is_dir($dir)) {
            return [];
        }
        $bundle = [];
        foreach (scandir($dir) ?: [] as $file) {
            if ($file === '.' || $file === '..' || in_array($file, self::EXCLUDE, true)) {
                continue;
            }
            if (!str_ends_with($file, '.json')) {
                continue;
            }
            $path = $dir . '/' . $file;
            if (is_file($path)) {
                $bundle[$file] = (string) file_get_contents($path);
            }
        }
        return $bundle;
    }

    private static function hashBundle(array $bundle): string
    {
        ksort($bundle);
        return md5((string) json_encode($bundle));
    }

    /** Remove the oldest auto-saves first, then the oldest overall, past the cap. */
    private static function prune(string $slug, array &$index): void
    {
        if (count($index) <= self::MAX_VERSIONS) {
            return;
        }
        usort($index, static fn(array $a, array $b): int => ($a['ts'] ?? 0) <=> ($b['ts'] ?? 0));
        while (count($index) > self::MAX_VERSIONS) {
            $removeAt = null;
            foreach ($index as $i => $m) {
                if (($m['type'] ?? '') === 'auto') {
                    $removeAt = $i;
                    break;
                }
            }
            if ($removeAt === null) {
                $removeAt = 0;
            }
            @unlink(self::versionsDir($slug) . '/' . ($index[$removeAt]['id'] ?? '') . '.json');
            array_splice($index, $removeAt, 1);
        }
    }

    private static function readIndex(string $slug): array
    {
        $file = self::versionsDir($slug) . '/index.json';
        if (!is_file($file)) {
            return [];
        }
        $data = json_decode((string) file_get_contents($file), true);
        return is_array($data) ? array_values(array_filter($data, 'is_array')) : [];
    }

    private static function writeIndex(string $slug, array $index): void
    {
        $dir = self::versionsDir($slug);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        file_put_contents($dir . '/index.json', json_encode(array_values($index), JSON_PRETTY_PRINT));
    }

    private static function baseDir(string $slug): string
    {
        $username = (string) ($_SESSION['user']['username'] ?? 'default');
        return constant('BASE_PATH') . "/projects/{$username}/{$slug}";
    }

    private static function configDir(string $slug): string
    {
        return self::baseDir($slug) . '/config';
    }

    private static function versionsDir(string $slug): string
    {
        return self::baseDir($slug) . '/versions';
    }

    private static function safeId(string $id): string
    {
        return (string) preg_replace('/[^a-zA-Z0-9_]/', '', $id);
    }
}
