<?php

/**
 * Builds multipart/form-data request bodies with configurable boundary strings.
 * Encodes fields and file attachments into a single stream for HTTP client uploads.
 * Used by the Http client when sending as_multipart requests.
 *
 * @package    Framework
 * @subpackage Http\Client
 * @since      1.0.0
 */
namespace Kirki\Framework\Http\Client;

\defined('ABSPATH') || exit;
use Kirki\Framework\Collections\Collection;
use UnexpectedValueException;
use function Kirki\Framework\collection;
class MultipartStream
{
    /**
     * The boundary string used to separate parts of the multipart stream.
     *
     * @var string
     *
     * @since 1.0.0
     */
    protected $boundary;
    /**
     * The stream of data to be sent.
     *
     * @var string
     *
     * @since 1.0.0
     */
    protected $stream;
    /**
     * Create a new MultipartStream instance.
     *
     * @param array $data The data to be sent.
     * @param string|null $boundary The boundary string to use.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function __construct(array $data, ?string $boundary = null)
    {
        $this->boundary = $boundary ?? "MultipartBoundary" . wp_generate_password(20, \false);
        $this->stream = $this->create_stream($data);
    }
    /**
     * Get the boundary string used to separate parts of the multipart stream.
     *
     * @return string The boundary string.
     *
     * @since 1.0.0
     */
    public function boundary()
    {
        return $this->boundary;
    }
    /**
     * Get the stream of data to be sent.
     *
     * @return string The stream of data.
     *
     * @since 1.0.0
     */
    public function payload()
    {
        return $this->stream;
    }
    /**
     * Create the stream of data to be sent.
     *
     * @param array $data The data to be sent.
     *
     * @return string The stream of data.
     *
     * @throws \UnexpectedValueException
     *
     * @since 1.0.0
     */
    protected function create_stream(array $data)
    {
        $stream = collection();
        foreach ($data as $item) {
            if (!\is_array($item)) {
                throw new UnexpectedValueException('Invalid data format');
            }
            $stream->push($this->create_stream_item($item));
        }
        $stream->push("--{$this->boundary}--\r\n");
        return $stream->join("");
    }
    /**
     * Create a stream item from the given data.
     *
     * @param array $item The data to be sent.
     *
     * @return string The stream item.
     *
     * @throws \UnexpectedValueException
     *
     * @since 1.0.0
     */
    protected function create_stream_item(array $item)
    {
        foreach (['contents', 'name'] as $key) {
            if (!\array_key_exists($key, $item)) {
                throw new UnexpectedValueException("Missing {$key} in item");
            }
        }
        $name = $item['name'];
        $contents = $item['contents'];
        $filename = $item['filename'] ?? null;
        $headers = $item['headers'] ?? [];
        return $this->create_element($name, $contents, $filename, $headers);
    }
    /**
     * Create a stream element from the given data.
     *
     * @param string $name The name of the element.
     * @param string $contents The contents of the element.
     * @param string|null $filename The filename of the element.
     * @param array $headers The headers of the element.
     *
     * @return string The stream element.
     *
     * @since 1.0.0
     */
    protected function create_element($name, $contents, ?string $filename = null, array $headers = [])
    {
        if (!empty($filename)) {
            return $this->create_file_stream($name, $contents, $filename, $headers);
        }
        return $this->create_field_stream($name, $contents, $headers);
    }
    /**
     * Create a file stream from the given data.
     *
     * @param string $name The name of the element.
     * @param string $contents The contents of the element.
     * @param string $filename The filename of the element.
     * @param array $headers The headers of the element.
     *
     * @return string The file stream.
     *
     * @since 1.0.0
     */
    protected function create_file_stream($name, $contents, $filename, array $headers = [])
    {
        $headers['Content-Type'] ??= $this->get_mimetype($filename);
        $headers['Content-Length'] ??= \strlen($contents);
        $payload = '';
        $payload .= "--{$this->boundary}\r\n";
        $payload .= "Content-Disposition: form-data; name=\"{$name}\"; filename=\"{$filename}\"\r\n";
        $payload .= $this->create_headers($headers);
        $payload .= $contents . "\r\n";
        return $payload;
    }
    /**
     * Get the mimetype of the file.
     *
     * @param string $filename The filename of the file.
     *
     * @return string The mimetype of the file.
     *
     * @since 1.0.0
     */
    protected function get_mimetype(string $filename)
    {
        return wp_check_filetype($filename)['type'] ?? 'application/octet-stream';
    }
    /**
     * Create a field stream from the given data.
     *
     * @param string $name The name of the element.
     * @param string $contents The contents of the element.
     * @param array $headers The headers of the element.
     *
     * @return string The field stream.
     *
     * @since 1.0.0
     */
    protected function create_field_stream($name, $contents, array $headers = [])
    {
        $payload = '';
        $payload .= "--{$this->boundary}\r\n";
        $payload .= "Content-Disposition: form-data; name=\"{$name}\"\r\n";
        if (!empty($headers)) {
            $payload .= $this->create_headers($headers);
        } else {
            $payload .= "\r\n";
        }
        $payload .= $contents . "\r\n";
        return $payload;
    }
    /**
     * Create headers for the stream.
     *
     * @param array $headers The headers to create.
     *
     * @return string The headers.
     *
     * @since 1.0.0
     */
    protected function create_headers(array $headers)
    {
        $payload = '';
        foreach ($headers as $name => $header) {
            $payload .= "{$name}: {$header}\r\n";
        }
        return \trim($payload) . "\r\n\r\n";
    }
}
