<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents\Services;

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Filesystem\Storage;
use App\Framework\LiveComponents\ValueObjects\ChunkHash;
use App\Framework\LiveComponents\ValueObjects\ChunkMetadata;
use App\Framework\LiveComponents\ValueObjects\UploadSession;
use App\Framework\LiveComponents\ValueObjects\UploadSessionId;
use InvalidArgumentException;

/**
 * Chunked Upload Manager
 *
 * Orchestriert den gesamten Chunked-Upload-Prozess:
 * - Session-Initialisierung
 * - Chunk-Upload und Validierung
 * - Progress-Tracking
 * - Session-Abschluss und Cleanup
 */
final readonly class ChunkedUploadManager
{
    public function __construct(
        private UploadSessionIdGenerator $sessionIdGenerator,
        private UploadSessionStore $sessionStore,
        private IntegrityValidator $integrityValidator,
        private ChunkAssembler $chunkAssembler,
        private Storage $fileStorage,
        private UploadProgressTrackerInterface $progressTracker,
        private string $uploadBasePath = '/tmp/chunked-uploads'
    ) {
    }

    /**
     * Initialize new upload session
     *
     * @param string $componentId LiveComponent ID
     * @param string $fileName Original filename
     * @param Byte $totalSize Total file size
     * @param Byte $chunkSize Size per chunk
     * @param ChunkHash|null $expectedFileHash Expected final file hash for integrity
     * @param string|null $userId User ID for SSE progress tracking (optional)
     * @return UploadSession Initialized session
     */
    public function initializeUpload(
        string $componentId,
        string $fileName,
        Byte $totalSize,
        Byte $chunkSize,
        ?ChunkHash $expectedFileHash = null,
        ?string $userId = null
    ): UploadSession {
        // Validate inputs
        if ($totalSize->isEmpty()) {
            throw new InvalidArgumentException('Total size must be greater than zero');
        }

        if ($chunkSize->isEmpty()) {
            throw new InvalidArgumentException('Chunk size must be greater than zero');
        }

        // Calculate total chunks needed
        $totalChunks = (int) ceil($totalSize->toBytes() / $chunkSize->toBytes());

        // Generate unique session ID
        $sessionId = $this->sessionIdGenerator->generate();

        // Create session
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: $componentId,
            fileName: $fileName,
            totalSize: $totalSize,
            totalChunks: $totalChunks,
            expectedFileHash: $expectedFileHash
        );

        // Create upload directory for this session
        $sessionPath = $this->getSessionPath($sessionId);
        $this->fileStorage->createDirectory($sessionPath);

        // Persist session
        $this->sessionStore->save($session);

        // Broadcast initialization via SSE if userId provided
        if ($userId !== null) {
            $this->progressTracker->broadcastInitialized($session, $userId);
        }

        return $session;
    }

    /**
     * Upload a chunk
     *
     * @param UploadSessionId $sessionId Session identifier
     * @param int $chunkIndex 0-based chunk index
     * @param string $chunkData Raw chunk data
     * @param ChunkHash $providedHash Client-provided chunk hash
     * @param string|null $userId User ID for SSE progress tracking (optional)
     * @return UploadSession Updated session
     * @throws InvalidArgumentException If chunk invalid or hash mismatch
     */
    public function uploadChunk(
        UploadSessionId $sessionId,
        int $chunkIndex,
        string $chunkData,
        ChunkHash $providedHash,
        ?string $userId = null
    ): UploadSession {
        // Load session
        $session = $this->sessionStore->get($sessionId);
        if ($session === null) {
            throw new InvalidArgumentException("Session not found: {$sessionId->toString()}");
        }

        if ($session->isExpired()) {
            throw new InvalidArgumentException("Session expired: {$sessionId->toString()}");
        }

        if ($session->isComplete()) {
            throw new InvalidArgumentException("Session already complete: {$sessionId->toString()}");
        }

        // Validate chunk index
        if ($chunkIndex < 0 || $chunkIndex >= $session->totalChunks) {
            throw new InvalidArgumentException(
                sprintf('Invalid chunk index %d (total chunks: %d)', $chunkIndex, $session->totalChunks)
            );
        }

        // Note: We don't validate chunk size because:
        // 1. Client determines chunk size, we only calculate totalChunks from it
        // 2. Last chunk can be smaller
        // 3. Hash validation provides sufficient integrity guarantee
        // 4. Different clients might use different chunking strategies

        // Verify chunk integrity
        if (!$this->integrityValidator->verifyChunk($chunkData, $providedHash)) {
            throw new InvalidArgumentException(
                sprintf('Chunk %d hash mismatch', $chunkIndex)
            );
        }

        // Check for chunk conflict (same index, different data)
        $newHash = ChunkHash::fromData($chunkData);
        foreach ($session->chunks as $existingChunk) {
            if ($existingChunk->index === $chunkIndex) {
                if (!$existingChunk->hash->equals($newHash)) {
                    throw new InvalidArgumentException(
                        sprintf(
                            'Chunk %d already uploaded with different data (existing hash: %s, new hash: %s)',
                            $chunkIndex,
                            $existingChunk->hash->toString(),
                            $newHash->toString()
                        )
                    );
                }
                // Same hash = idempotent re-upload, allow it
                return $session;
            }
        }

        // Save chunk to disk
        $chunkPath = $this->getChunkPath($sessionId, $chunkIndex);
        $this->fileStorage->put($chunkPath, $chunkData);

        // Calculate actual chunk hash (from saved data for extra safety)
        // Note: We read back from storage to ensure hash matches what was actually stored
        $storedData = $this->fileStorage->get($chunkPath);
        $actualHash = ChunkHash::fromData($storedData);

        // Create chunk metadata
        $chunkMetadata = ChunkMetadata::create(
            index: $chunkIndex,
            size: Byte::fromBytes(strlen($chunkData)),
            hash: $actualHash
        )->withUploaded();

        // Update session with uploaded chunk
        $updatedSession = $session->withChunk($chunkMetadata);

        // Persist updated session
        $this->sessionStore->save($updatedSession);

        // Broadcast progress update via SSE if userId provided
        if ($userId !== null) {
            $this->progressTracker->broadcastChunkUploaded($updatedSession, $userId);
        }

        return $updatedSession;
    }

    /**
     * Complete upload and assemble final file
     *
     * @param UploadSessionId $sessionId Session identifier
     * @param string $targetPath Target path for assembled file
     * @param string|null $userId User ID for SSE progress tracking (optional)
     * @return UploadSession Completed session
     * @throws InvalidArgumentException If session incomplete or assembly fails
     */
    public function completeUpload(
        UploadSessionId $sessionId,
        string $targetPath,
        ?string $userId = null
    ): UploadSession {
        // Load session
        $session = $this->sessionStore->get($sessionId);
        if ($session === null) {
            throw new InvalidArgumentException("Session not found: {$sessionId->toString()}");
        }

        if (!$session->isComplete()) {
            $missing = $session->getMissingChunkIndices();
            throw new InvalidArgumentException(
                sprintf('Upload incomplete. Missing chunks: %s', implode(', ', $missing))
            );
        }

        // Assemble chunks into final file
        $chunkPaths = $this->getChunkPaths($sessionId, $session->totalChunks);
        $this->chunkAssembler->assemble($chunkPaths, $targetPath);

        // Verify final file integrity if expected hash provided
        if ($session->expectedFileHash !== null) {
            // Read assembled file and verify hash
            $assembledData = $this->fileStorage->get($targetPath);
            $actualHash = ChunkHash::fromData($assembledData);

            if (!$actualHash->equals($session->expectedFileHash)) {
                // Clean up assembled file
                $this->fileStorage->delete($targetPath);
                throw new InvalidArgumentException('Final file hash mismatch');
            }
        }

        // Mark session as completed
        $completedSession = $session->withCompleted();
        $this->sessionStore->save($completedSession);

        // Cleanup chunks (keep session for potential queries)
        $this->cleanupChunks($sessionId);

        // Broadcast completion via SSE if userId provided
        if ($userId !== null) {
            $this->progressTracker->broadcastCompleted($completedSession, $userId);
        }

        return $completedSession;
    }

    /**
     * Abort upload and cleanup
     *
     * @param UploadSessionId $sessionId Session identifier
     * @param string|null $userId User ID for SSE progress tracking (optional)
     * @param string $reason Abort reason
     * @return void
     */
    public function abortUpload(UploadSessionId $sessionId, ?string $userId = null, string $reason = 'User cancelled'): void
    {
        // Broadcast abort via SSE if userId provided (before cleanup)
        if ($userId !== null) {
            $this->progressTracker->broadcastAborted($sessionId, $userId, $reason);
        }
        // Cleanup chunks
        $this->cleanupChunks($sessionId);

        // Remove session directory
        $sessionPath = $this->getSessionPath($sessionId);
        if (is_dir($sessionPath)) {
            rmdir($sessionPath);
        }

        // Delete session from store
        $this->sessionStore->delete($sessionId);
    }

    /**
     * Get upload status
     *
     * @param UploadSessionId $sessionId Session identifier
     * @return UploadSession|null Session or null if not found
     */
    public function getStatus(UploadSessionId $sessionId): ?UploadSession
    {
        return $this->sessionStore->get($sessionId);
    }

    /**
     * Get session path on disk
     */
    private function getSessionPath(UploadSessionId $sessionId): string
    {
        return $this->uploadBasePath . '/' . $sessionId->toString();
    }

    /**
     * Get chunk file path
     */
    private function getChunkPath(UploadSessionId $sessionId, int $chunkIndex): string
    {
        return $this->getSessionPath($sessionId) . '/chunk_' . $chunkIndex;
    }

    /**
     * Get all chunk paths for session
     *
     * @return string[]
     */
    private function getChunkPaths(UploadSessionId $sessionId, int $totalChunks): array
    {
        $paths = [];
        for ($i = 0; $i < $totalChunks; $i++) {
            $paths[] = $this->getChunkPath($sessionId, $i);
        }
        return $paths;
    }

    /**
     * Cleanup chunk files
     */
    private function cleanupChunks(UploadSessionId $sessionId): void
    {
        $sessionPath = $this->getSessionPath($sessionId);

        // Get all chunk files from session directory
        try {
            $files = $this->fileStorage->listDirectory($sessionPath);
            foreach ($files as $file) {
                if (str_starts_with(basename($file), 'chunk_')) {
                    $this->fileStorage->delete($file);
                }
            }
        } catch (\Exception $e) {
            // Session directory might not exist, ignore
        }
    }

}
