<?php

declare(strict_types=1);

namespace App\Framework\Filesystem;

use App\Framework\Core\ValueObjects\FileSize;
use App\Framework\Exception\Core\FilesystemErrorCode;
use App\Framework\Filesystem\Exceptions\FileValidationException;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FilePermissionException;
use App\Framework\Filesystem\Exceptions\DirectoryCreateException;
use App\Framework\Filesystem\ValueObjects\FilePath;

/**
 * Zentrale Validierung für Filesystem-Operationen
 *
 * Validiert Pfade, Permissions, File Sizes und andere Constraints
 * bevor Filesystem-Operationen ausgeführt werden
 */
final readonly class FileValidator
{
    private ?array $allowedExtensionsMap;
    private ?array $blockedExtensionsMap;
    private string $pathTraversalPattern;

    public function __construct(
        private ?array $allowedExtensions = null,
        private ?array $blockedExtensions = null,
        private ?FileSize $maxFileSize = null,
        private ?string $baseDirectory = null
    ) {
        // Optimize extension lookups: O(n) → O(1)
        $this->allowedExtensionsMap = $allowedExtensions !== null
            ? array_flip($allowedExtensions)
            : null;

        $this->blockedExtensionsMap = $blockedExtensions !== null
            ? array_flip($blockedExtensions)
            : null;

        // Compile path traversal patterns: 6 str_contains() → 1 regex
        $this->pathTraversalPattern = '#(?:\.\./)' .
            '|(?:\.\.\\\\)' .
            '|(?:%2e%2e/)' .
            '|(?:%2e%2e\\\\)' .
            '|(?:\.\.%2f)' .
            '|(?:\.\.%5c)#i';
    }

    /**
     * Factory für Standard-Validator mit sicheren Defaults
     */
    public static function createDefault(): self
    {
        return new self(
            allowedExtensions: null, // Keine Einschränkung
            blockedExtensions: ['exe', 'bat', 'sh', 'cmd', 'com'], // Executable files blockieren
            maxFileSize: FileSize::fromMegabytes(100), // 100MB max
            baseDirectory: null
        );
    }

    /**
     * Factory für strict Validator (nur whitelisted extensions)
     */
    public static function createStrict(array $allowedExtensions): self
    {
        return new self(
            allowedExtensions: $allowedExtensions,
            blockedExtensions: null,
            maxFileSize: FileSize::fromMegabytes(50),
            baseDirectory: null
        );
    }

    /**
     * Factory für Upload-Validator
     */
    public static function forUploads(?FileSize $maxSize = null): self
    {
        return new self(
            allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt', 'csv', 'json'],
            blockedExtensions: ['exe', 'bat', 'sh', 'cmd', 'com', 'php', 'phtml'],
            maxFileSize: $maxSize ?? FileSize::fromMegabytes(10),
            baseDirectory: null
        );
    }

    /**
     * Factory für Image-Validator
     */
    public static function forImages(?FileSize $maxSize = null): self
    {
        return new self(
            allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
            blockedExtensions: null,
            maxFileSize: $maxSize ?? FileSize::fromMegabytes(5),
            baseDirectory: null
        );
    }

    /**
     * Validiert Dateipfad
     *
     * @throws FileValidationException
     */
    public function validatePath(string $path): void
    {
        // Empty path
        if (empty($path)) {
            throw FileValidationException::invalidPath(
                $path,
                'File path cannot be empty'
            );
        }

        // Null bytes (security)
        if (str_contains($path, "\0")) {
            throw FileValidationException::invalidPath(
                $path,
                'File path contains null bytes'
            );
        }

        // Path traversal attempt
        if ($this->containsPathTraversal($path)) {
            throw FileValidationException::invalidPath(
                $path,
                'Path traversal attempt detected',
                FilesystemErrorCode::PATH_TRAVERSAL_ATTEMPT
            );
        }

        // Base directory constraint
        if ($this->baseDirectory !== null) {
            $realPath = realpath($path);
            $realBase = realpath($this->baseDirectory);

            if ($realPath === false || $realBase === false) {
                throw FileValidationException::invalidPath(
                    $path,
                    'Cannot resolve real path'
                );
            }

            if (!str_starts_with($realPath, $realBase)) {
                throw FileValidationException::invalidPath(
                    $realPath,
                    'Path is outside base directory',
                    FilesystemErrorCode::PATH_TRAVERSAL_ATTEMPT
                );
            }
        }
    }

    /**
     * Validiert Dateiendung
     *
     * @throws FileValidationException
     */
    public function validateExtension(string $path): void
    {
        $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));

        if (empty($extension)) {
            // Keine Extension ist ok wenn keine Whitelist definiert
            if ($this->allowedExtensions !== null) {
                throw FileValidationException::invalidPath(
                    $path,
                    'File has no extension'
                );
            }

            return;
        }

        // Whitelist check - O(1) lookup mit array_flip
        if ($this->allowedExtensionsMap !== null) {
            if (!isset($this->allowedExtensionsMap[$extension])) {
                throw FileValidationException::invalidExtension(
                    $path,
                    $extension,
                    $this->allowedExtensions
                );
            }
        }

        // Blacklist check - O(1) lookup mit array_flip
        if ($this->blockedExtensionsMap !== null) {
            if (isset($this->blockedExtensionsMap[$extension])) {
                throw FileValidationException::blockedExtension(
                    $path,
                    $extension,
                    $this->blockedExtensions
                );
            }
        }
    }

    /**
     * Validiert Dateigröße
     *
     * @throws FileValidationException
     */
    public function validateFileSize(FileSize $fileSize): void
    {
        if ($this->maxFileSize === null) {
            return;
        }

        if ($fileSize->exceedsLimit($this->maxFileSize)) {
            throw FileValidationException::fileSizeExceeded(
                $fileSize->toHumanReadable(),
                $this->maxFileSize->toHumanReadable(),
                $fileSize->toBytes(),
                $this->maxFileSize->toBytes()
            );
        }
    }

    /**
     * Validiert ob Datei existiert
     *
     * @throws FileNotFoundException
     */
    public function validateExists(string $path): void
    {
        if (!file_exists($path)) {
            throw new FileNotFoundException($path);
        }
    }

    /**
     * Validiert ob Pfad beschreibbar ist
     *
     * @throws FilePermissionException
     */
    public function validateWritable(string $path): void
    {
        if (!is_writable($path)) {
            throw FilePermissionException::write($path, 'Path is not writable');
        }
    }

    /**
     * Validiert ob Pfad lesbar ist
     *
     * @throws FilePermissionException
     */
    public function validateReadable(string $path): void
    {
        if (!is_readable($path)) {
            throw FilePermissionException::read($path, 'Path is not readable');
        }
    }

    /**
     * Vollständige Validierung für File-Upload
     *
     * @throws FilesystemException
     */
    public function validateUpload(string $path, FileSize $fileSize): void
    {
        $this->validatePath($path);
        $this->validateExtension($path);
        $this->validateFileSize($fileSize);
    }

    /**
     * Vollständige Validierung für File-Read
     *
     * @throws FilesystemException
     */
    public function validateRead(string $path): void
    {
        $this->validatePath($path);
        $this->validateExists($path);
        $this->validateReadable($path);
    }

    /**
     * Vollständige Validierung für File-Write
     *
     * @throws FileValidationException
     * @throws DirectoryCreateException
     * @throws FilePermissionException
     */
    public function validateWrite(string $path, ?FileSize $fileSize = null): void
    {
        $this->validatePath($path);
        $this->validateExtension($path);

        if ($fileSize !== null) {
            $this->validateFileSize($fileSize);
        }

        $directory = dirname($path);
        if (!is_dir($directory)) {
            throw new DirectoryCreateException($directory);
        }

        $this->validateWritable($directory);
    }

    /**
     * Prüft auf Path Traversal Patterns
     *
     * Performance: Compiled regex (1 call) statt 6 str_contains() Calls
     */
    private function containsPathTraversal(string $path): bool
    {
        return preg_match($this->pathTraversalPattern, $path) === 1;
    }

    /**
     * Gibt erlaubte Extensions zurück
     *
     * @return array<string>|null
     */
    public function getAllowedExtensions(): ?array
    {
        return $this->allowedExtensions;
    }

    /**
     * Gibt blockierte Extensions zurück
     *
     * @return array<string>|null
     */
    public function getBlockedExtensions(): ?array
    {
        return $this->blockedExtensions;
    }

    /**
     * Gibt maximale Dateigröße zurück
     */
    public function getMaxFileSize(): ?FileSize
    {
        return $this->maxFileSize;
    }

    /**
     * Prüft ob Extension erlaubt ist (ohne Exception)
     */
    public function isExtensionAllowed(string $extension): bool
    {
        $extension = strtolower(ltrim($extension, '.'));

        // O(1) lookup mit optimierten Maps
        if ($this->allowedExtensionsMap !== null) {
            return isset($this->allowedExtensionsMap[$extension]);
        }

        if ($this->blockedExtensionsMap !== null) {
            return !isset($this->blockedExtensionsMap[$extension]);
        }

        return true; // Keine Einschränkungen
    }
}
