<?php
namespace FlysystemOffload\StreamWrapper;

use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToReadFile;
use League\Flysystem\UnableToWriteFile;

class FlysystemStreamWrapper
{
    private static array $filesystems = [];
    private static array $prefixes = [];

    private $stream;
    private string $protocol;
    private string $path;
    private string $mode;
    private string $flyPath;

    public static function register(FilesystemOperator $filesystem, string $protocol, string $prefix): void
    {
        if (in_array($protocol, stream_get_wrappers(), true)) {
            stream_wrapper_unregister($protocol);
        }

        self::$filesystems[$protocol] = $filesystem;
        self::$prefixes[$protocol] = $prefix;

        stream_wrapper_register($protocol, static::class, STREAM_IS_URL);
    }

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
    {
        $this->protocol = strtok($path, ':');
        $this->path = $path;
        $this->mode = $mode;
        $this->flyPath = $this->resolveFlyPath($path);

        $filesystem = $this->filesystem();

        if (strpbrk($mode, 'waxc')) {
            $this->stream = fopen('php://temp', str_contains($mode, 'b') ? 'w+b' : 'w+');

            if (str_contains($mode, 'a') && $filesystem->fileExists($this->flyPath)) {
                $contents = $filesystem->readStream($this->flyPath);
                stream_copy_to_stream($contents, $this->stream);
                fclose($contents);
            }

            rewind($this->stream);
            return true;
        }

        try {
            $resource = $filesystem->readStream($this->flyPath);
            $this->stream = $resource;
            return is_resource($resource);
        } catch (UnableToReadFile $e) {
            return false;
        }
    }

    public function stream_read(int $count): string|false
    {
        return fread($this->stream, $count);
    }

    public function stream_write(string $data): int|false
    {
        return fwrite($this->stream, $data);
    }

    public function stream_flush(): bool
    {
        if (!strpbrk($this->mode, 'waxc')) {
            return true;
        }

        $filesystem = $this->filesystem();

        try {
            rewind($this->stream);
            $filesystem->writeStream($this->flyPath, $this->stream);
            return true;
        } catch (UnableToWriteFile $e) {
            error_log('[Flysystem Offload] Unable to flush stream: ' . $e->getMessage());
            return false;
        }
    }

    public function stream_close(): void
    {
        $this->stream_flush();

        if (is_resource($this->stream)) {
            fclose($this->stream);
        }
    }

    public function stream_stat(): array|false
    {
        return $this->url_stat($this->path, 0);
    }

    public function url_stat(string $path, int $flags): array|false
    {
        $filesystem = $this->filesystem();
        $flyPath = $this->resolveFlyPath($path);

        try {
            if (!$filesystem->fileExists($flyPath) && !$filesystem->directoryExists($flyPath)) {
                if ($flags & STREAM_URL_STAT_QUIET) {
                    return false;
                }
                trigger_error("File or directory not found: {$path}", E_USER_WARNING);
                return false;
            }

            $isDir = $filesystem->directoryExists($flyPath);
            $size = $isDir ? 0 : $filesystem->fileSize($flyPath);
            $mtime = $filesystem->lastModified($flyPath);

            return [
                'dev' => 0,
                'ino' => 0,
                'mode' => $isDir ? 0040777 : 0100777,
                'nlink' => 0,
                'uid' => 0,
                'gid' => 0,
                'rdev' => 0,
                'size' => $size,
                'atime' => $mtime,
                'mtime' => $mtime,
                'ctime' => $mtime,
                'blksize' => -1,
                'blocks' => -1
            ];
        } catch (FilesystemException $e) {
            if (!($flags & STREAM_URL_STAT_QUIET)) {
                trigger_error($e->getMessage(), E_USER_WARNING);
            }
            return false;
        }
    }

    public function unlink(string $path): bool
    {
        $filesystem = $this->filesystem();
        $flyPath = $this->resolveFlyPath($path);

        try {
            $filesystem->delete($flyPath);
            return true;
        } catch (UnableToDeleteFile $e) {
            return false;
        }
    }

    public function mkdir(string $path, int $mode, int $options): bool
    {
        $filesystem = $this->filesystem();
        $flyPath = $this->resolveFlyPath($path);

        try {
            $filesystem->createDirectory($flyPath);
            return true;
        } catch (FilesystemException $e) {
            return false;
        }
    }

    public function rmdir(string $path, int $options): bool
    {
        $filesystem = $this->filesystem();
        $flyPath = $this->resolveFlyPath($path);

        try {
            $filesystem->deleteDirectory($flyPath);
            return true;
        } catch (FilesystemException $e) {
            return false;
        }
    }

    public function dir_opendir(string $path, int $options): bool
    {
        $this->protocol = strtok($path, ':');
        $this->flyPath = $this->resolveFlyPath($path);
        $this->stream = $this->filesystem()->listContents($this->flyPath, false)->getIterator();

        return true;
    }

    public function dir_readdir(): string|false
    {
        if ($this->stream instanceof \Iterator) {
            if ($this->stream->valid()) {
                $current = $this->stream->current();
                $this->stream->next();
                return $current['basename'] ?? $current['path'] ?? false;
            }
        }

        return false;
    }

    public function dir_rewinddir(): bool
    {
        if ($this->stream instanceof \Iterator) {
            $this->stream->rewind();
            return true;
        }

        return false;
    }

    public function dir_closedir(): bool
    {
        $this->stream = null;
        return true;
    }

    public function rename(string $oldPath, string $newPath): bool
    {
        $filesystem = $this->filesystem();
        $from = $this->resolveFlyPath($oldPath);
        $to = $this->resolveFlyPath($newPath);

        try {
            $filesystem->move($from, $to);
            return true;
        } catch (FilesystemException $e) {
            return false;
        }
    }

    private function filesystem(): FilesystemOperator
    {
        if (!isset(self::$filesystems[$this->protocol])) {
            throw new \RuntimeException('No filesystem registered for protocol: ' . $this->protocol);
        }

        return self::$filesystems[$this->protocol];
    }

    private function resolveFlyPath(string $path): string
    {
        $protocol = strtok($path, ':');
        $prefix = self::$prefixes[$protocol] ?? '';
        $raw = preg_replace('#^[^:]+://#', '', $path);

        return ltrim($prefix . ltrim($raw, '/'), '/');
    }
}
