<?php
namespace EasyChat\Library\Security;

use EasyChat\Library\SafeRuntime;







final class PrivateFileArea {
    private $directory;
    private $directory_device;
    private $directory_inode;
    private $allow_acl_owner;

    




    public function __construct(string $directory, bool $allow_acl_owner = false) {
        if (!$this->isAbsolute($directory) || strpos($directory, "\0") !== false || is_link($directory)) {
            throw new \RuntimeException('private_storage_path_invalid');
        }
        $previous = umask(0077);
        try {
            if (!is_dir($directory) && !SafeRuntime::mkdir($directory, 0700, true)) {
                throw new \RuntimeException('private_storage_create_failed');
            }
        } finally {
            umask($previous);
        }
        if (!SafeRuntime::chmod($directory, 0700)) {
            throw new \RuntimeException('private_storage_permissions_failed');
        }
        clearstatcache(true, $directory);
        $real = realpath($directory);
        $stat = SafeRuntime::call(function () use ($directory) { return stat($directory); }, false);
        if (!is_string($real) || is_link($directory) || !is_array($stat) || (((int)$stat['mode']) & 0077) !== 0) {
            throw new \RuntimeException('private_storage_unsafe');
        }
        $this->allow_acl_owner = $allow_acl_owner;
        $this->directory = rtrim($real, DIRECTORY_SEPARATOR);
        $this->directory_device = $stat['dev'] ?? null;
        $this->directory_inode = $stat['ino'] ?? null;
        $this->assertOwner($stat, true);
        $this->assertDirectory();
    }

    public function path(string $name): string {
        if (preg_match('/\A[a-zA-Z0-9][a-zA-Z0-9._-]{0,190}\z/D', $name) !== 1) {
            throw new \InvalidArgumentException('private_storage_name_invalid');
        }
        return $this->directory . DIRECTORY_SEPARATOR . $name;
    }

    public function read(string $path, int $maximum_bytes): ?string {
        $this->assertChild($path);
        clearstatcache(true, $path);
        if (!file_exists($path) && !is_link($path)) return null;
        $handle = $this->openExisting($path, 'rb');
        try {
            $stat = fstat($handle);
            if (!is_array($stat) || (int)($stat['size'] ?? -1) < 0 || (int)$stat['size'] > $maximum_bytes) {
                throw new \RuntimeException('private_storage_size_invalid');
            }
            $data = '';
            while (!feof($handle)) {
                $remaining = $maximum_bytes - strlen($data);
                if ($remaining <= 0) throw new \RuntimeException('private_storage_size_invalid');
                $chunk = fread($handle, min(8192, $remaining));
                if (!is_string($chunk) || ($chunk === '' && !feof($handle))) {
                    throw new \RuntimeException('private_storage_read_failed');
                }
                $data .= $chunk;
            }
            return $data;
        } finally {
            fclose($handle);
        }
    }

    public function writeAtomic(string $path, string $data): void {
        $this->assertChild($path);
        $temporary = $path . '.tmp-' . bin2hex(random_bytes(16));
        $previous = umask(0077);
        try {
            $handle = SafeRuntime::call(function () use ($temporary) { return fopen($temporary, 'x+b'); }, false);
        } finally {
            umask($previous);
        }
        if (!is_resource($handle)) throw new \RuntimeException('private_storage_temp_create_failed');
        try {
            if (!SafeRuntime::chmod($temporary, 0600)) throw new \RuntimeException('private_storage_temp_permissions_failed');
            $offset = 0;
            $length = strlen($data);
            while ($offset < $length) {
                $written = fwrite($handle, substr($data, $offset, 8192));
                if (!is_int($written) || $written < 1) throw new \RuntimeException('private_storage_write_failed');
                $offset += $written;
            }
            if (!fflush($handle)) throw new \RuntimeException('private_storage_flush_failed');
            if (function_exists('fsync') && !fsync($handle)) throw new \RuntimeException('private_storage_fsync_failed');
            $this->assertOpen($handle, $temporary);
            fclose($handle);
            $handle = null;
            if (!SafeRuntime::rename($temporary, $path) || !SafeRuntime::chmod($path, 0600)) {
                throw new \RuntimeException('private_storage_publish_failed');
            }
            $this->assertDirectory();
            $check = $this->openExisting($path, 'rb');
            fclose($check);
        } finally {
            if (is_resource($handle)) fclose($handle);
            if (file_exists($temporary) || is_link($temporary)) SafeRuntime::unlink($temporary);
        }
    }

    public function delete(string $path): void {
        $this->assertChild($path);
        if (!file_exists($path) && !is_link($path)) return;
        $handle = $this->openExisting($path, 'rb');
        fclose($handle);
        if (!SafeRuntime::unlink($path)) throw new \RuntimeException('private_storage_delete_failed');
    }

    public function openLock(string $path, bool $non_blocking = false) {
        $this->assertChild($path);
        clearstatcache(true, $path);
        if (!file_exists($path) && !is_link($path)) {
            $previous = umask(0077);
            try {
                $handle = SafeRuntime::call(function () use ($path) { return fopen($path, 'x+b'); }, false);
            } finally {
                umask($previous);
            }
            if (is_resource($handle)) {
                if (!SafeRuntime::chmod($path, 0600)) {
                    fclose($handle);
                    SafeRuntime::unlink($path);
                    throw new \RuntimeException('private_storage_lock_permissions_failed');
                }
                $this->assertOpen($handle, $path);
            } else {
                $handle = $this->openExisting($path, 'r+b');
            }
        } else {
            $handle = $this->openExisting($path, 'r+b');
        }
        $operation = LOCK_EX | ($non_blocking ? LOCK_NB : 0);
        if (!flock($handle, $operation)) {
            fclose($handle);
            return null;
        }
        return $handle;
    }

    private function openExisting(string $path, string $mode) {
        $before = SafeRuntime::call(function () use ($path) { return lstat($path); }, false);
        if (!is_array($before) || $this->isSymlinkMode((int)($before['mode'] ?? 0))) {
            throw new \RuntimeException('private_storage_file_unsafe');
        }
        $handle = SafeRuntime::call(function () use ($path, $mode) { return fopen($path, $mode); }, false);
        if (!is_resource($handle)) throw new \RuntimeException('private_storage_open_failed');
        try {
            $this->assertOpen($handle, $path, $before);
            return $handle;
        } catch (\Throwable $e) {
            fclose($handle);
            throw $e;
        }
    }

    private function assertOpen($handle, string $path, ?array $expected = null): void {
        $opened = fstat($handle);
        $current = SafeRuntime::call(function () use ($path) { return lstat($path); }, false);
        if (!is_array($opened) || !is_array($current)
            || (((int)$opened['mode']) & 0170000) !== 0100000
            || (int)($opened['nlink'] ?? 0) !== 1
            || (((int)$opened['mode']) & 0077) !== 0
            || ($opened['dev'] ?? null) !== ($current['dev'] ?? null)
            || ($opened['ino'] ?? null) !== ($current['ino'] ?? null)
            || ($expected !== null && (($opened['dev'] ?? null) !== ($expected['dev'] ?? null) || ($opened['ino'] ?? null) !== ($expected['ino'] ?? null)))) {
            throw new \RuntimeException('private_storage_file_validation_failed');
        }
        $this->assertOwner($opened, false);
    }

    private function assertChild(string $path): void {
        $this->assertDirectory();
        $prefix = $this->directory . DIRECTORY_SEPARATOR;
        if (dirname($path) !== $this->directory || strncmp($path, $prefix, strlen($prefix)) !== 0) {
            throw new \InvalidArgumentException('private_storage_child_invalid');
        }
    }

    





    private function assertDirectory(): void {
        clearstatcache(true, $this->directory);
        $stat = SafeRuntime::call(function () { return lstat($this->directory); }, false);
        if (!is_array($stat)
            || (((int)($stat['mode'] ?? 0)) & 0170000) !== 0040000
            || $this->isSymlinkMode((int)($stat['mode'] ?? 0))
            || (($stat['dev'] ?? null) !== $this->directory_device)
            || (($stat['ino'] ?? null) !== $this->directory_inode)
            || (((int)($stat['mode'] ?? 0)) & 0077) !== 0) {
            throw new \RuntimeException('private_storage_directory_changed');
        }
        $this->assertOwner($stat, false);
    }

    private function assertOwner(array $stat, bool $prove_acl_access): void {
        if (!function_exists('posix_geteuid') || !isset($stat['uid']) || (int)$stat['uid'] === (int)posix_geteuid()) {
            return;
        }
        if (!$this->allow_acl_owner) {
            throw new \RuntimeException('private_storage_owner_invalid');
        }
        if ($prove_acl_access) {
            $this->proveAclControl();
        }
    }

    





    private function proveAclControl(): void {
        $path = $this->directory . DIRECTORY_SEPARATOR . '.acl-probe-' . bin2hex(random_bytes(16));
        $token = random_bytes(32);
        $previous = umask(0077);
        try {
            $handle = SafeRuntime::call(function () use ($path) { return fopen($path, 'x+b'); }, false);
        } finally {
            umask($previous);
        }
        if (!is_resource($handle)) throw new \RuntimeException('private_storage_acl_access_denied');
        try {
            if (!SafeRuntime::chmod($path, 0600)
                || fwrite($handle, $token) !== strlen($token)
                || !fflush($handle)
                || fseek($handle, 0) !== 0
                || fread($handle, strlen($token)) !== $token) {
                throw new \RuntimeException('private_storage_acl_access_denied');
            }
            $opened = fstat($handle);
            $current = SafeRuntime::call(function () use ($path) { return lstat($path); }, false);
            if (!is_array($opened) || !is_array($current)
                || (((int)($opened['mode'] ?? 0)) & 0170000) !== 0100000
                || (((int)($opened['mode'] ?? 0)) & 0077) !== 0
                || (int)($opened['nlink'] ?? 0) !== 1
                || ($opened['dev'] ?? null) !== ($current['dev'] ?? null)
                || ($opened['ino'] ?? null) !== ($current['ino'] ?? null)) {
                throw new \RuntimeException('private_storage_acl_probe_invalid');
            }
        } finally {
            fclose($handle);
            if (file_exists($path) || is_link($path)) SafeRuntime::unlink($path);
        }
        if (file_exists($path) || is_link($path)) {
            throw new \RuntimeException('private_storage_acl_cleanup_failed');
        }
    }

    private function isSymlinkMode(int $mode): bool { return ($mode & 0170000) === 0120000; }
    private function isAbsolute(string $path): bool {
        return $path !== '' && ($path[0] === DIRECTORY_SEPARATOR || preg_match('/^[A-Za-z]:[\\\\\/]/', $path) === 1);
    }
}
