<?php

declare(strict_types=1);

namespace App\Framework\Filesystem;

use App\Framework\Async\FiberManager;
use App\Framework\Core\PathProvider;
use App\Framework\DateTime\SystemClock;
use App\Framework\DateTime\SystemTimer;
use App\Framework\Filesystem\Exceptions\DirectoryCreateException;
use App\Framework\Filesystem\Exceptions\DirectoryListException;
use App\Framework\Filesystem\Exceptions\FileCopyException;
use App\Framework\Filesystem\Exceptions\FileDeleteException;
use App\Framework\Filesystem\Exceptions\FileMetadataException;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FilePermissionException;
use App\Framework\Filesystem\Exceptions\FileReadException;
use App\Framework\Filesystem\Exceptions\FileWriteException;
use App\Framework\Filesystem\Traits\AppendableStorageTrait;
use App\Framework\Filesystem\ValueObjects\FileMetadata;
use App\Framework\Filesystem\Traits\AtomicStorageTrait;
use App\Framework\Filesystem\Traits\CompressibleStorageTrait;
use App\Framework\Filesystem\Traits\LockableStorageTrait;
use App\Framework\Filesystem\Traits\StreamableStorageTrait;
use App\Framework\Core\ValueObjects\FileSize;
use App\Framework\Filesystem\ValueObjects\FileOperation;
use App\Framework\Filesystem\ValueObjects\FileOperationContext;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;

final readonly class FileStorage implements Storage, AtomicStorage, AppendableStorage, StreamableStorage, LockableStorage, CompressibleStorage
{
    use StorageTrait;
    use AtomicStorageTrait;
    use AppendableStorageTrait;
    use StreamableStorageTrait;
    use LockableStorageTrait;
    use CompressibleStorageTrait;

    public PermissionChecker $permissions;

    public FiberManager $fiberManager;

    public ?FileValidator $validator;

    public ?Logger $logger;

    public function __construct(
        private string $basePath = '/',
        ?FiberManager $fiberManager = null,
        ?FileValidator $validator = null,
        ?Logger $logger = null,
        ?PathProvider $pathProvider = null
    ) {
        // PathProvider aus DI Container nutzen, Fallback auf basePath
        $pathProvider = $pathProvider ?? new PathProvider($this->basePath);
        $this->permissions = new PermissionChecker($pathProvider);
        $this->fiberManager = $fiberManager ?? $this->createDefaultFiberManager();
        $this->validator = $validator;
        $this->logger = $logger;
    }

    private function createDefaultFiberManager(): FiberManager
    {
        // Create minimal dependencies for FiberManager
        $clock = new SystemClock();
        $timer = new SystemTimer($clock);

        return new FiberManager($clock, $timer);
    }

    private function resolvePath(string $path): string
    {
        // Absolute path bleibt unverändert
        if (str_starts_with($path, '/')) {
            return $path;
        }

        // Relative paths werden an basePath gehängt
        return rtrim($this->basePath, '/') . '/' . ltrim($path, '/');
    }

    /**
     * Log FileOperation context wenn Logger verfügbar
     */
    private function logOperation(FileOperationContext $context): void
    {
        if ($this->logger === null) {
            return;
        }

        $logContext = LogContext::fromArray($context->toArray());

        if ($context->isHighSeverity()) {
            $this->logger->framework->warning($context->toString(), $logContext);
        } elseif ($context->isLargeOperation()) {
            $this->logger->framework->info($context->toString(), $logContext);
        } else {
            $this->logger->framework->debug($context->toString(), $logContext);
        }
    }

    public function get(string $path): string
    {
        $resolvedPath = $this->resolvePath($path);

        // Validate with FileValidator if available
        if ($this->validator !== null) {
            $this->validator->validateRead($resolvedPath);
        }

        if (! is_file($resolvedPath)) {
            throw new FileNotFoundException($path);
        }

        if (! is_readable($resolvedPath)) {
            throw FilePermissionException::read($path, 'File is not readable');
        }

        $content = @file_get_contents($resolvedPath);
        if ($content === false) {
            $error = error_get_last();
            if ($error && str_contains($error['message'], 'Permission denied')) {
                throw FilePermissionException::read($path, $error['message']);
            }

            throw new FileReadException($path);
        }

        // Log successful read operation
        $this->logOperation(FileOperationContext::forRead(
            path: $resolvedPath,
            bytesRead: FileSize::fromBytes(strlen($content))
        ));

        return $content;
    }

    public function put(string $path, string $content): void
    {
        $resolvedPath = $this->resolvePath($path);
        $dir = dirname($resolvedPath);

        // Validate with FileValidator if available
        if ($this->validator !== null) {
            $fileSize = FileSize::fromBytes(strlen($content));
            $this->validator->validateWrite($resolvedPath, $fileSize);
        }

        // Clear stat cache before directory operations to ensure fresh info
        clearstatcache(true, $dir);

        // Prüfe Directory-Permissions
        if (! is_dir($dir)) {
            if (! @mkdir($dir, 0777, true) && ! is_dir($dir)) {
                $error = error_get_last();
                if ($error && str_contains($error['message'], 'Permission denied')) {
                    throw FilePermissionException::createDirectory($dir, $error['message']);
                }

                throw new DirectoryCreateException($dir);
            }
        } elseif (! is_writable($dir)) {
            throw FilePermissionException::write($path, 'Directory is not writable: ' . $dir);
        }

        // Prüfe File-Permissions wenn Datei bereits existiert
        if (is_file($resolvedPath) && ! is_writable($resolvedPath)) {
            throw FilePermissionException::write($path, 'File is not writable');
        }

        if (@file_put_contents($resolvedPath, $content) === false) {
            $error = error_get_last();
            if ($error && str_contains($error['message'], 'Permission denied')) {
                throw FilePermissionException::write($path, $error['message']);
            }

            throw new FileWriteException($path);
        }

        // Set readable permissions for files (0644 = owner rw, group r, others r)
        // This ensures cache files and other storage files are always readable
        $this->permissions->setPermissions(
            $path,
            ValueObjects\FilePermissions::readWriteOwnerReadAll()
        );

        // Log successful write operation
        $this->logOperation(FileOperationContext::forWrite(
            path: $resolvedPath,
            bytesWritten: FileSize::fromBytes(strlen($content))
        ));
    }

    public function exists(string $path): bool
    {
        return is_file($this->resolvePath($path));
    }

    public function delete(string $path): void
    {
        $resolvedPath = $this->resolvePath($path);

        // Validate with FileValidator if available
        if ($this->validator !== null) {
            $this->validator->validatePath($resolvedPath);
            $this->validator->validateExists($resolvedPath);
        }

        if (! is_file($resolvedPath)) {
            throw new FileNotFoundException($path);
        }

        // Prüfe Directory-Permissions
        $dir = dirname($resolvedPath);
        if (! is_writable($dir)) {
            throw FilePermissionException::delete($path, 'Directory is not writable: ' . $dir);
        }

        // Prüfe File-Permissions
        if (! is_writable($resolvedPath)) {
            throw FilePermissionException::delete($path, 'File is not writable');
        }

        if (! @unlink($resolvedPath)) {
            $error = error_get_last();
            if ($error && str_contains($error['message'], 'Permission denied')) {
                throw FilePermissionException::delete($path, $error['message']);
            }

            throw new FileDeleteException($path);
        }

        // Log successful delete operation
        $this->logOperation(FileOperationContext::forOperation(
            operation: FileOperation::DELETE,
            path: $resolvedPath
        ));
    }

    public function copy(string $source, string $destination): void
    {
        $resolvedSource = $this->resolvePath($source);
        $resolvedDestination = $this->resolvePath($destination);

        // Validate source with FileValidator if available
        if ($this->validator !== null) {
            $this->validator->validateRead($resolvedSource);
        }

        // Validate destination with FileValidator if available
        if ($this->validator !== null) {
            $this->validator->validatePath($resolvedDestination);
            $this->validator->validateExtension($resolvedDestination);
        }

        if (! is_file($resolvedSource)) {
            throw new FileNotFoundException($source);
        }

        // Stelle sicher, dass das Zielverzeichnis existiert
        $dir = dirname($resolvedDestination);
        if (! is_dir($dir)) {
            $this->createDirectory($destination);
        }

        if (! @copy($resolvedSource, $resolvedDestination)) {
            throw new FileCopyException($source, $destination);
        }

        // Log successful copy operation
        $this->logOperation(FileOperationContext::forOperationWithDestination(
            operation: FileOperation::COPY,
            sourcePath: $resolvedSource,
            destinationPath: $resolvedDestination
        ));
    }

    public function size(string $path): int
    {
        $resolvedPath = $this->resolvePath($path);
        if (! is_file($resolvedPath)) {
            throw new FileNotFoundException($path);
        }

        $size = @filesize($resolvedPath);
        if ($size === false) {
            throw new FileReadException($path);
        }

        return $size;
    }

    public function lastModified(string $path): int
    {
        $resolvedPath = $this->resolvePath($path);
        if (! is_file($resolvedPath)) {
            throw new FileNotFoundException($path);
        }

        $time = @filemtime($resolvedPath);
        if ($time === false) {
            throw new FileReadException($path);
        }

        return $time;
    }

    public function getMimeType(string $path): string
    {
        $resolvedPath = $this->resolvePath($path);
        if (! is_file($resolvedPath)) {
            throw new FileNotFoundException($path);
        }

        $mimeType = @mime_content_type($resolvedPath);
        if ($mimeType === false) {
            throw new FileMetadataException($path);
        }

        return $mimeType;
    }

    public function isReadable(string $path): bool
    {
        $resolvedPath = $this->resolvePath($path);
        if (! is_file($resolvedPath)) {
            throw new FileNotFoundException($path);
        }

        return is_readable($resolvedPath);
    }

    public function isWritable(string $path): bool
    {
        $resolvedPath = $this->resolvePath($path);
        if (! is_file($resolvedPath)) {
            throw new FileNotFoundException($path);
        }

        return is_writable($resolvedPath);
    }

    public function listDirectory(string $directory): array
    {
        $resolvedDirectory = $this->resolvePath($directory);
        if (! is_dir($resolvedDirectory)) {
            throw new DirectoryCreateException($directory);
        }

        $files = @scandir($resolvedDirectory);
        if ($files === false) {
            throw new DirectoryListException($directory);
        }

        // Entferne . und .. Einträge
        $files = array_filter($files, function ($file) {
            return $file !== '.' && $file !== '..';
        });

        // Relative Pfade zurückgeben (ohne basePath)
        $result = [];
        foreach ($files as $file) {
            $fullPath = $resolvedDirectory . DIRECTORY_SEPARATOR . $file;
            // Entferne basePath für relativen Pfad
            $relativePath = str_starts_with($fullPath, $this->basePath)
                ? substr($fullPath, strlen(rtrim($this->basePath, '/')) + 1)
                : $fullPath;
            $result[] = $relativePath;
        }

        return array_values($result);
    }

    public function createDirectory(string $path, int $permissions = 0755, bool $recursive = true): void
    {
        $resolvedPath = $this->resolvePath($path);
        if (is_dir($resolvedPath)) {
            return; // Verzeichnis existiert bereits
        }

        // Prüfe Parent-Directory Permissions
        $parentDir = dirname($resolvedPath);
        if (is_dir($parentDir) && ! is_writable($parentDir)) {
            throw FilePermissionException::createDirectory($path, 'Parent directory is not writable: ' . $parentDir);
        }

        if (! @mkdir($resolvedPath, $permissions, $recursive) && ! is_dir($resolvedPath)) {
            $error = error_get_last();
            if ($error && str_contains($error['message'], 'Permission denied')) {
                throw FilePermissionException::createDirectory($path, $error['message']);
            }

            throw new DirectoryCreateException($path);
        }
    }

    // Batch-Operationen für explizite Parallelverarbeitung

    public function batch(array $operations): array
    {
        return $this->fiberManager->batch($operations);
    }

    public function getMultiple(array $paths): array
    {
        $operations = [];
        foreach ($paths as $path) {
            $operations[$path] = fn () => $this->get($path);
        }

        return $this->batch($operations);
    }

    public function putMultiple(array $files): void
    {
        $operations = [];
        foreach ($files as $path => $content) {
            $operations[$path] = fn () => $this->put($path, $content);
        }

        $this->batch($operations);
    }

    public function getMetadataMultiple(array $paths): array
    {
        $operations = [];
        foreach ($paths as $path) {
            $operations[$path] = fn () => new FileMetadata(
                size: $this->size($path),
                lastModified: $this->lastModified($path),
                mimeType: $this->getMimeType($path),
                isReadable: $this->isReadable($path),
                isWritable: $this->isWritable($path)
            );
        }

        return $this->batch($operations);
    }
}
