<?php

declare(strict_types=1);

namespace App\Framework\Filesystem\ValueObjects;

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Hash;
use App\Framework\Core\ValueObjects\HashAlgorithm;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Filesystem\FilePath;
use App\Framework\Filesystem\Storage;
use InvalidArgumentException;

/**
 * Kapselt Metadaten einer Datei.
 * Erweitert mit Funktionalität für Discovery-Modul und nutzt Value Objects.
 */
final readonly class FileMetadata
{
    /**
     * @param FilePath|string $path Vollständiger Pfad zur Datei
     * @param Byte $size Dateigröße
     * @param Timestamp $lastModified Zeitstempel der letzten Änderung
     * @param Hash $checksum Prüfsumme des Dateiinhalts (optional)
     * @param Timestamp $scanTime Zeitstempel des Scans
     * @param string $mimeType MIME-Typ der Datei
     * @param bool $isReadable Ob die Datei lesbar ist
     * @param bool $isWritable Ob die Datei schreibbar ist
     * @param array<string, mixed> $additionalData Zusätzliche Metadaten
     */
    public function __construct(
        public FilePath|string $path = '',
        public Byte|int $size = 0,
        public Timestamp|int $lastModified = 0,
        public ?Hash $checksum = null,
        public ?Timestamp $scanTime = null,
        public string $mimeType = '',
        public bool $isReadable = false,
        public bool $isWritable = false,
        public array $additionalData = []
    ) {
    }

    /**
     * Erstellt Metadaten aus einer Datei unter Verwendung des Storage-Interfaces
     *
     * @param FilePath|string $filePath Pfad zur Datei
     * @param Storage $storage Storage-Implementierung
     * @param bool $calculateChecksum Ob eine Prüfsumme berechnet werden soll
     * @return self
     */
    public static function fromFile(FilePath|string $filePath, Storage $storage, bool $calculateChecksum = true): self
    {
        $path = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
        $pathString = $path->toString();

        if (! $storage->exists($pathString)) {
            throw new InvalidArgumentException("File does not exist: {$pathString}");
        }

        $size = Byte::fromBytes($storage->size($pathString));
        $lastModified = Timestamp::fromFloat((float)$storage->lastModified($pathString));
        $scanTime = Timestamp::now();
        $mimeType = $storage->getMimeType($pathString);
        $isReadable = $storage->isReadable($pathString);
        $isWritable = $storage->isWritable($pathString);

        // Berechne Prüfsumme nur wenn gewünscht (kann bei großen Dateien teuer sein)
        $checksum = $calculateChecksum
            ? self::calculateChecksum($pathString, $storage)
            : self::emptyHash(HashAlgorithm::XXHASH64);

        // Zusätzliche Metadaten sammeln
        $additionalData = [
            'extension' => pathinfo($pathString, PATHINFO_EXTENSION),
        ];

        return new self(
            path: $path,
            size: $size,
            lastModified: $lastModified,
            checksum: $checksum,
            scanTime: $scanTime,
            mimeType: $mimeType,
            isReadable: $isReadable,
            isWritable: $isWritable,
            additionalData: $additionalData
        );
    }

    /**
     * Erstellt eine leere Hash-Instanz mit dem angegebenen Algorithmus
     */
    public static function emptyHash(HashAlgorithm $algorithm): Hash
    {
        // Erzeuge einen leeren String mit der richtigen Länge für den Algorithmus
        $emptyValue = str_repeat('0', $algorithm->getLength());

        return new Hash($emptyValue, $algorithm);
    }

    /**
     * Berechnet die Prüfsumme einer Datei
     */
    private static function calculateChecksum(string $filePath, Storage $storage): Hash
    {
        $content = $storage->get($filePath);

        return Hash::create($content, HashAlgorithm::XXHASH64);
    }

    /**
     * Erstellt Metadaten aus einem Array (für Deserialisierung)
     */
    public static function fromArray(array $data): self
    {
        return new self(
            path: $data['path'],
            size: Byte::fromBytes($data['size']),
            lastModified: Timestamp::fromFloat((float)$data['last_modified']),
            checksum: isset($data['checksum']) ? new Hash($data['checksum'], HashAlgorithm::XXHASH64) : null,
            scanTime: isset($data['scan_time']) ? Timestamp::fromFloat((float)$data['scan_time']) : null,
            mimeType: $data['mime_type'] ?? '',
            isReadable: $data['is_readable'] ?? false,
            isWritable: $data['is_writable'] ?? false,
            additionalData: $data['additional_data'] ?? []
        );
    }

    /**
     * Konvertiert Metadaten in ein Array (für Serialisierung)
     */
    public function toArray(): array
    {
        return [
            'path' => $this->path instanceof FilePath ? $this->path->toString() : $this->path,
            'size' => $this->size instanceof Byte ? $this->size->toBytes() : $this->size,
            'last_modified' => $this->lastModified instanceof Timestamp ? $this->lastModified->toTimestamp() : $this->lastModified,
            'checksum' => $this->checksum?->toString(),
            'scan_time' => $this->scanTime?->toTimestamp(),
            'mime_type' => $this->mimeType,
            'is_readable' => $this->isReadable,
            'is_writable' => $this->isWritable,
            'additional_data' => $this->additionalData,
        ];
    }

    /**
     * Prüft, ob sich die Datei seit dem letzten Scan geändert hat
     * Verwendet mehrere Kriterien für zuverlässigere Erkennung
     *
     * @param FilePath|string $filePath Pfad zur Datei
     * @param Storage $storage Storage-Implementierung
     * @return bool True wenn die Datei sich geändert hat
     */
    public function hasChanged(FilePath|string $filePath, Storage $storage): bool
    {
        $pathString = $filePath instanceof FilePath ? $filePath->toString() : $filePath;

        if (! $storage->exists($pathString)) {
            return true; // Datei wurde gelöscht
        }

        // Prüfe Größe und Änderungszeit (schnelle Prüfung)
        $currentSize = Byte::fromBytes($storage->size($pathString));
        $currentLastModified = Timestamp::fromFloat((float)$storage->lastModified($pathString));

        if (! $currentSize->equals($this->size) || ! $currentLastModified->equals($this->lastModified)) {
            return true;
        }

        // Wenn Prüfsumme vorhanden ist, verwende sie für präzisere Erkennung
        if ($this->checksum !== null && ! $this->isEmptyChecksum()) {
            $currentChecksum = self::calculateChecksum($pathString, $storage);

            return ! $currentChecksum->equals($this->checksum);
        }

        // Fallback: Nur Größe und Änderungszeit
        return false;
    }

    /**
     * Prüft, ob die Prüfsumme leer ist (nur Nullen)
     */
    private function isEmptyChecksum(): bool
    {
        if ($this->checksum === null) {
            return true;
        }

        $emptyHash = self::emptyHash($this->checksum->getAlgorithm());

        return $this->checksum->equals($emptyHash);
    }

    /**
     * Aktualisiert die Metadaten für eine Datei
     *
     * @param FilePath|string $filePath Pfad zur Datei
     * @param Storage $storage Storage-Implementierung
     * @param bool $calculateChecksum Ob eine Prüfsumme berechnet werden soll
     * @return self Neue Instanz mit aktualisierten Metadaten
     */
    public function update(FilePath|string $filePath, Storage $storage, bool $calculateChecksum = true): self
    {
        return self::fromFile($filePath, $storage, $calculateChecksum);
    }

    /**
     * Gibt den relativen Pfad zurück (für bessere Lesbarkeit)
     *
     * @param FilePath|string $basePath Basis-Pfad
     * @return string Relativer Pfad
     */
    public function getRelativePath(FilePath|string $basePath): string
    {
        $pathString = $this->path instanceof FilePath ? $this->path->toString() : $this->path;
        $basePathString = $basePath instanceof FilePath ? $basePath->toString() : $basePath;

        return str_replace($basePathString, '', $pathString);
    }

    /**
     * Gibt eine lesbare Darstellung der Dateigröße zurück
     */
    public function getFormattedSize(): string
    {
        return $this->size instanceof Byte ? $this->size->toHumanReadable(2) : Byte::fromBytes($this->size)->toHumanReadable(2);
    }

    /**
     * Gibt einen lesbaren Zeitstempel zurück
     */
    public function getFormattedTime(): string
    {
        return $this->lastModified instanceof Timestamp
            ? $this->lastModified->format('Y-m-d H:i:s')
            : date('Y-m-d H:i:s', $this->lastModified);
    }

    /**
     * Erstellt eine Instanz mit dem aktuellen Zeitstempel als scanTime
     */
    public function withCurrentScanTime(): self
    {
        return new self(
            path: $this->path,
            size: $this->size,
            lastModified: $this->lastModified,
            checksum: $this->checksum,
            scanTime: Timestamp::now(),
            mimeType: $this->mimeType,
            isReadable: $this->isReadable,
            isWritable: $this->isWritable,
            additionalData: $this->additionalData
        );
    }
}
