<?php
declare(strict_types=1);

namespace SafeTun\Backend\Storage;

final class SecureFiles
{
    public static function ensureDirectory(string $path): void
    {
        if (is_link($path)) {
            throw new \RuntimeException("Refusing symlinked state directory: {$path}");
        }
        if (!is_dir($path) && !@mkdir($path, 0700, true) && !is_dir($path)) {
            throw new \RuntimeException("Unable to create state directory: {$path}");
        }
        if (!is_writable($path)) {
            throw new \RuntimeException("State directory is not writable: {$path}");
        }
        @chmod($path, 0700);
    }

    /** @param resource $handle */
    public static function writeAll($handle, string $bytes): void
    {
        $offset = 0;
        $length = strlen($bytes);
        while ($offset < $length) {
            $written = fwrite($handle, substr($bytes, $offset));
            if (!is_int($written) || $written <= 0) {
                throw new \RuntimeException('Unable to write file-state record.');
            }
            $offset += $written;
        }
        if (!fflush($handle)) {
            throw new \RuntimeException('Unable to flush file-state record.');
        }
        if (function_exists('fsync') && !fsync($handle)) {
            throw new \RuntimeException('Unable to synchronize file-state record.');
        }
    }

    /** @param resource $handle */
    public static function readBounded($handle, int $maximumBytes = 65536): string
    {
        rewind($handle);
        $data = stream_get_contents($handle, $maximumBytes + 1);
        if (!is_string($data) || strlen($data) > $maximumBytes) {
            throw new \RuntimeException('File-state record is unreadable or oversized.');
        }
        return $data;
    }

    /** @param resource $handle */
    public static function replaceLocked($handle, string $bytes): void
    {
        if (!ftruncate($handle, 0) || !rewind($handle)) {
            throw new \RuntimeException('Unable to reset file-state record.');
        }
        self::writeAll($handle, $bytes);
    }

    public static function encodeJson(array $value): string
    {
        try {
            return json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES) . "\n";
        } catch (\JsonException $exception) {
            throw new \RuntimeException('Unable to encode file-state record.', 0, $exception);
        }
    }

    /** @return array<string,mixed> */
    public static function decodeJson(string $value): array
    {
        try {
            $decoded = json_decode($value, true, 16, JSON_THROW_ON_ERROR);
        } catch (\JsonException $exception) {
            throw new \RuntimeException('Corrupt file-state record.', 0, $exception);
        }
        if (!is_array($decoded) || array_is_list($decoded)) {
            throw new \RuntimeException('File-state record must be a JSON object.');
        }
        /** @var array<string,mixed> $decoded */
        return $decoded;
    }
}
