<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents\ValueObjects;

use App\Framework\Core\ValueObjects\Byte;
use DateTimeImmutable;
use InvalidArgumentException;

/**
 * Upload Session State
 *
 * Vollständiger State einer Chunked-Upload-Session.
 * Immutable Value Object für Session-Tracking.
 */
final readonly class UploadSession
{
    /**
     * @param UploadSessionId $sessionId Eindeutige Session ID
     * @param string $componentId LiveComponent ID
     * @param string $fileName Original-Dateiname
     * @param Byte $totalSize Gesamtgröße der Datei
     * @param int $totalChunks Anzahl Chunks
     * @param ChunkMetadata[] $chunks Array von Chunk-Metadaten
     * @param ChunkHash|null $expectedFileHash Erwarteter finaler File-Hash
     * @param QuarantineStatus $quarantineStatus Quarantäne-Status
     * @param DateTimeImmutable $createdAt Session-Erstellungszeitpunkt
     * @param DateTimeImmutable|null $completedAt Abschluss-Zeitpunkt
     * @param DateTimeImmutable $expiresAt Session-Ablaufzeit
     */
    public function __construct(
        public UploadSessionId $sessionId,
        public string $componentId,
        public string $fileName,
        public Byte $totalSize,
        public int $totalChunks,
        public array $chunks = [],
        public ?ChunkHash $expectedFileHash = null,
        public QuarantineStatus $quarantineStatus = QuarantineStatus::PENDING,
        public DateTimeImmutable $createdAt = new DateTimeImmutable(),
        public ?DateTimeImmutable $completedAt = null,
        public ?DateTimeImmutable $expiresAt = null
    ) {
        if (empty($componentId)) {
            throw new InvalidArgumentException('Component ID cannot be empty');
        }

        if (empty($fileName)) {
            throw new InvalidArgumentException('File name cannot be empty');
        }

        if ($totalChunks < 1) {
            throw new InvalidArgumentException('Total chunks must be at least 1');
        }

        if (count($chunks) > $totalChunks) {
            throw new InvalidArgumentException('Number of chunks cannot exceed total chunks');
        }

        // Validate all chunks are ChunkMetadata
        foreach ($chunks as $chunk) {
            if (!$chunk instanceof ChunkMetadata) {
                throw new InvalidArgumentException('All chunks must be ChunkMetadata instances');
            }
        }
    }

    public static function create(
        UploadSessionId $sessionId,
        string $componentId,
        string $fileName,
        Byte $totalSize,
        int $totalChunks,
        ?ChunkHash $expectedFileHash = null
    ): self {
        $createdAt = new DateTimeImmutable();
        $expiresAt = $createdAt->modify('+24 hours');

        return new self(
            sessionId: $sessionId,
            componentId: $componentId,
            fileName: $fileName,
            totalSize: $totalSize,
            totalChunks: $totalChunks,
            expectedFileHash: $expectedFileHash,
            createdAt: $createdAt,
            expiresAt: $expiresAt
        );
    }

    public function withChunk(ChunkMetadata $chunk): self
    {
        // Find existing chunk by index or append
        $chunks = $this->chunks;
        $found = false;

        foreach ($chunks as $i => $existingChunk) {
            if ($existingChunk->index === $chunk->index) {
                $chunks[$i] = $chunk;
                $found = true;
                break;
            }
        }

        if (!$found) {
            $chunks[] = $chunk;
        }

        // Sort by index
        usort($chunks, fn($a, $b) => $a->index <=> $b->index);

        return new self(
            $this->sessionId,
            $this->componentId,
            $this->fileName,
            $this->totalSize,
            $this->totalChunks,
            $chunks,
            $this->expectedFileHash,
            $this->quarantineStatus,
            $this->createdAt,
            $this->completedAt,
            $this->expiresAt
        );
    }

    public function withQuarantineStatus(QuarantineStatus $status): self
    {
        if (!$this->quarantineStatus->canTransitionTo($status)) {
            throw new InvalidArgumentException(
                sprintf(
                    'Cannot transition from %s to %s',
                    $this->quarantineStatus->value,
                    $status->value
                )
            );
        }

        return new self(
            $this->sessionId,
            $this->componentId,
            $this->fileName,
            $this->totalSize,
            $this->totalChunks,
            $this->chunks,
            $this->expectedFileHash,
            $status,
            $this->createdAt,
            $this->completedAt,
            $this->expiresAt
        );
    }

    public function withCompleted(): self
    {
        return new self(
            $this->sessionId,
            $this->componentId,
            $this->fileName,
            $this->totalSize,
            $this->totalChunks,
            $this->chunks,
            $this->expectedFileHash,
            $this->quarantineStatus,
            $this->createdAt,
            new DateTimeImmutable(),
            $this->expiresAt
        );
    }

    public function getUploadedChunks(): array
    {
        return array_values(array_filter($this->chunks, fn($chunk) => $chunk->isUploaded()));
    }

    public function getUploadedBytes(): Byte
    {
        $total = 0;
        foreach ($this->getUploadedChunks() as $chunk) {
            $total += $chunk->size->toBytes();
        }
        return Byte::fromBytes($total);
    }

    public function getProgress(): float
    {
        if ($this->totalSize->isEmpty()) {
            return 0.0;
        }

        return ($this->getUploadedBytes()->toBytes() / $this->totalSize->toBytes()) * 100;
    }

    public function isComplete(): bool
    {
        return count($this->getUploadedChunks()) === $this->totalChunks;
    }

    public function isExpired(): bool
    {
        if ($this->expiresAt === null) {
            return false;
        }
        return new DateTimeImmutable() > $this->expiresAt;
    }

    public function getMissingChunkIndices(): array
    {
        $uploaded = array_map(fn($chunk) => $chunk->index, $this->getUploadedChunks());
        $all = range(0, $this->totalChunks - 1);
        return array_values(array_diff($all, $uploaded));
    }

    public function toArray(): array
    {
        return [
            'session_id' => $this->sessionId->toString(),
            'component_id' => $this->componentId,
            'file_name' => $this->fileName,
            'total_size' => $this->totalSize->toBytes(),
            'total_size_human' => $this->totalSize->toHumanReadable(),
            'total_chunks' => $this->totalChunks,
            'uploaded_chunks' => count($this->getUploadedChunks()),
            'uploaded_bytes' => $this->getUploadedBytes()->toBytes(),
            'progress' => round($this->getProgress(), 2),
            'is_complete' => $this->isComplete(),
            'quarantine_status' => $this->quarantineStatus->value,
            'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
            'completed_at' => $this->completedAt?->format('Y-m-d H:i:s'),
            'expires_at' => $this->expiresAt->format('Y-m-d H:i:s'),
            'is_expired' => $this->isExpired(),
            'missing_chunks' => $this->getMissingChunkIndices(),
        ];
    }
}
