<?php

namespace NoahVet\Reef\Domain\Http\Multipart;

class ContentDecoder implements ContentDecoderInterface
{
    public function __construct(
        protected readonly HeaderValueParserInterface $headerValueParser = new HeaderValueParser(),
    ) {
    }

    #[\Override]
    public function decode(string $rawBody, string $encoding, ?string $contentType): string
    {
        $body = match (\strtolower($encoding)) {
            'base64' => \base64_decode($rawBody),
            'quoted-printable' => \quoted_printable_decode($rawBody),
            default => $rawBody,
        };

        if ($this->shouldConvertCharset($encoding, $contentType)) {
            $body = $this->convertToUtf8($body, $contentType);
        }

        return $body;
    }

    /**
     * @param resource $stream
     *
     * @return resource
     */
    #[\Override]
    public function decodeToStream(mixed $stream, int $bodyOffset, string $encoding, ?string $contentType): mixed
    {
        \fseek($stream, $bodyOffset);
        $partStream = \fopen('php://temp', 'rb+');
        if (!\is_resource($partStream)) {
            throw new \RuntimeException('Failed to open temporary stream');
        }
        \stream_copy_to_stream($stream, $partStream);
        \rewind($partStream);

        switch (\strtolower($encoding)) {
            case 'base64':
                \stream_filter_append($partStream, 'convert.base64-decode');
                $partStream = $this->drainToSeekableStream($partStream);
                break;
            case 'quoted-printable':
                \stream_filter_append($partStream, 'convert.quoted-printable-decode');
                $partStream = $this->drainToSeekableStream($partStream);
                break;
        }

        if (!$this->shouldConvertCharset($encoding, $contentType)) {
            return $partStream;
        }

        $charset = $this->headerValueParser->getOption($contentType, 'charset');

        if (null === $charset) {
            \fclose($partStream);
            $rawBody = (string) \stream_get_contents($stream, -1, $bodyOffset);
            $decoded = $this->decode($rawBody, $encoding, $contentType);
            $fallback = \fopen('php://memory', 'rb+');
            if (!\is_resource($fallback)) {
                throw new \RuntimeException('Failed to open memory stream');
            }
            \fwrite($fallback, $decoded);
            \rewind($fallback);

            return $fallback;
        }

        if ('utf-8' !== \strtolower($charset)) {
            \stream_filter_append($partStream, "convert.iconv.$charset/UTF-8");
        }

        return $partStream;
    }

    /**
     * Drains a filtered (non-seekable) stream into a fresh seekable php://temp stream.
     *
     * @param resource $filteredStream
     *
     * @return resource
     */
    protected function drainToSeekableStream(mixed $filteredStream): mixed
    {
        $seekable = \fopen('php://temp', 'rb+');
        if (!\is_resource($seekable)) {
            throw new \RuntimeException('Failed to open temporary stream');
        }
        \stream_copy_to_stream($filteredStream, $seekable);
        \fclose($filteredStream);
        \rewind($seekable);

        return $seekable;
    }

    protected function shouldConvertCharset(string $encoding, ?string $contentType): bool
    {
        if (\in_array(\strtolower($encoding), ['binary', '7bit'], true)) {
            return false;
        }

        $mimeType = \strtolower(\trim(\explode(';', (string) $contentType)[0]));

        return '' === $mimeType || \str_starts_with($mimeType, 'text/');
    }

    protected function convertToUtf8(string $body, ?string $contentType): string
    {
        $charset = $this->headerValueParser->getOption($contentType, 'charset');

        if (null === $charset) {
            $detected = \mb_detect_encoding($body);
            $charset = false !== $detected ? $detected : 'utf-8';
        }

        if ('utf-8' === \strtolower($charset)) {
            return $body;
        }

        return (string) \mb_convert_encoding($body, 'utf-8', $charset);
    }
}
