<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents\Services;

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Filesystem\Storage;
use App\Framework\Filesystem\StreamableStorage;
use App\Framework\Filesystem\StreamWriter;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use InvalidArgumentException;

/**
 * Chunk Assembler
 *
 * Fügt Upload-Chunks zu finaler Datei zusammen.
 * Nutzt Framework's Storage Interface mit Streaming für Memory-Effizienz.
 */
final readonly class ChunkAssembler
{
    private const int DEFAULT_BUFFER_SIZE = 8192; // 8KB

    private Byte $bufferSize;

    public function __construct(
        private Storage&StreamableStorage $fileStorage,
        ?Byte $bufferSize = null
    ) {
        $this->bufferSize = $bufferSize ?? Byte::fromBytes(self::DEFAULT_BUFFER_SIZE);
    }

    /**
     * Assemble chunks into final file
     *
     * @param string[] $chunkPaths Ordered array of chunk file paths
     * @param string $targetPath Target path for assembled file
     * @return void
     * @throws InvalidArgumentException If chunk missing or assembly fails
     */
    public function assemble(array $chunkPaths, string $targetPath): void
    {
        if (empty($chunkPaths)) {
            throw new InvalidArgumentException('No chunks provided for assembly');
        }

        // Validate all chunks exist
        foreach ($chunkPaths as $index => $chunkPath) {
            if (!$this->fileStorage->exists($chunkPath)) {
                throw new InvalidArgumentException(
                    sprintf('Chunk %d not found: %s', $index, $chunkPath)
                );
            }
        }

        // Open target file for writing (stream-based)
        $targetStream = $this->fileStorage->writeStream($targetPath);
        $targetWriter = new StreamWriter($targetStream);

        try {
            // Stream each chunk to target file
            foreach ($chunkPaths as $index => $chunkPath) {
                $this->appendChunk($chunkPath, $targetWriter, $index);
            }

            // Persist assembled content to storage
            // For InMemoryStorage, we need to explicitly call putStream()
            // to write the stream content back to the storage
            rewind($targetStream);
            $this->fileStorage->putStream($targetPath, $targetStream);
        } finally {
            $targetWriter->close();
        }
    }

    /**
     * Append chunk to target stream (memory-efficient)
     *
     * @param string $chunkPath Path to chunk file
     * @param StreamWriter $targetWriter Target stream writer
     * @param int $chunkIndex Chunk index (for error messages)
     * @return void
     * @throws InvalidArgumentException If chunk cannot be read
     */
    private function appendChunk(string $chunkPath, StreamWriter $targetWriter, int $chunkIndex): void
    {
        try {
            $chunkStream = $this->fileStorage->readStream($chunkPath);
        } catch (FileNotFoundException $e) {
            throw new InvalidArgumentException(
                sprintf('Failed to open chunk %d: %s', $chunkIndex, $chunkPath),
                previous: $e
            );
        }

        $chunkReader = new StreamWriter($chunkStream);

        try {
            // Stream chunk data to target file
            while (!$chunkReader->isEof()) {
                $buffer = $chunkReader->read($this->bufferSize);

                if ($buffer === '') {
                    continue; // Skip empty reads at EOF
                }

                $bytesWritten = $targetWriter->write($buffer);

                if ($bytesWritten->isEmpty()) {
                    throw new InvalidArgumentException(
                        sprintf('Failed to write chunk %d to target', $chunkIndex)
                    );
                }
            }
        } finally {
            $chunkReader->close();
        }
    }

    /**
     * Get assembled file size
     *
     * @param string[] $chunkPaths Chunk file paths
     * @return Byte Total size
     */
    public function calculateTotalSize(array $chunkPaths): Byte
    {
        $totalBytes = 0;
        foreach ($chunkPaths as $chunkPath) {
            if ($this->fileStorage->exists($chunkPath)) {
                $totalBytes += $this->fileStorage->size($chunkPath);
            }
        }
        return Byte::fromBytes($totalBytes);
    }
}
