<?php
declare(strict_types=1);

namespace SafeTun\Backend\Storage;

final class ShardedStore
{
    public function __construct(private readonly string $root)
    {
        SecureFiles::ensureDirectory($root);
    }

    public function pathFor(string $namespace, string $key, bool $createDirectories = true): string
    {
        if (!preg_match('/^[a-z][a-z0-9_-]{0,31}$/D', $namespace)) {
            throw new \InvalidArgumentException('Invalid file-state namespace.');
        }
        $hash = hash('sha256', $namespace . "\0" . $key);
        $directory = rtrim($this->root, '/\\') . DIRECTORY_SEPARATOR . $namespace
            . DIRECTORY_SEPARATOR . substr($hash, 0, 2) . DIRECTORY_SEPARATOR . substr($hash, 2, 2);
        if ($createDirectories) {
            SecureFiles::ensureDirectory($directory);
        }
        return $directory . DIRECTORY_SEPARATOR . $hash . '.json';
    }

    /**
     * Return the stable per-shard coordination inode. Data records may be
     * removed by cleanup, but this lock file is never unlinked, so processes
     * that opened it before cleanup cannot split onto different lock inodes.
     */
    public function lockPathFor(string $namespace, string $key, bool $createDirectories = true): string
    {
        return dirname($this->pathFor($namespace, $key, $createDirectories))
            . DIRECTORY_SEPARATOR . '.lock';
    }

    public function cleanup(int $olderThan, int $maximumFiles = 200): int
    {
        if ($maximumFiles < 1 || $maximumFiles > 10000) {
            throw new \InvalidArgumentException('Invalid cleanup file limit.');
        }
        $removed = 0;
        $seen = 0;
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($this->root, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY,
        );
        foreach ($iterator as $file) {
            if (!$file->isFile() || $file->isLink() || !str_ends_with($file->getFilename(), '.json')) {
                continue;
            }
            if (++$seen > $maximumFiles) {
                break;
            }
            $path = $file->getPathname();
            $lockPath = $file->getPath() . DIRECTORY_SEPARATOR . '.lock';
            $lock = @fopen($lockPath, 'c+b');
            if (!is_resource($lock)) {
                continue;
            }
            @chmod($lockPath, 0600);
            try {
                if (!flock($lock, LOCK_EX | LOCK_NB)) {
                    continue;
                }
                // The directory entry and age may have changed after the
                // iterator snapshot. Revalidate both while holding the same
                // stable shard lock used by every writer.
                clearstatcache(true, $path);
                $information = @lstat($path);
                if (is_array($information) && !is_link($path)
                    && ($information['mode'] & 0170000) === 0100000
                    && $information['mtime'] < $olderThan
                    && @unlink($path)) {
                    $removed++;
                }
                flock($lock, LOCK_UN);
            } finally {
                fclose($lock);
            }
        }
        return $removed;
    }
}
