<?php

declare(strict_types=1);

namespace App\Framework\HttpClient\Curl;

use App\Framework\HttpClient\Curl\Exception\MultiHandleException;
use CurlMultiHandle;

/**
 * Object-oriented wrapper for cURL multi handle
 *
 * Provides parallel request execution with type-safe API.
 * Based on PHP RFC: Object-oriented curl API v2
 *
 * Features:
 * - Parallel request execution
 * - Exception-based error handling
 * - Automatic resource cleanup
 * - Status monitoring
 * - Event callbacks (onComplete, onError, onProgress)
 *
 * @link https://wiki.php.net/rfc/curl_oop_v2
 */
final class MultiHandle
{
    /**
     * Underlying cURL multi handle resource
     */
    private readonly CurlMultiHandle $handle;

    /**
     * Attached handles for tracking
     *
     * @var array<int, Handle>
     */
    private array $handles = [];

    /**
     * Callback to execute when a handle completes
     */
    private ?\Closure $onCompleteCallback = null;

    /**
     * Callback to execute when a handle errors
     */
    private ?\Closure $onErrorCallback = null;

    /**
     * Callback to execute on progress updates
     */
    private ?\Closure $onProgressCallback = null;

    /**
     * Equivalent to curl_multi_init()
     *
     * @param Handle ...$handles Optional handles to add immediately
     * @throws MultiHandleException If multi handle initialization fails
     */
    public function __construct(Handle ...$handles)
    {
        $handle = curl_multi_init();

        if ($handle === false) {
            throw MultiHandleException::initializationFailed();
        }

        $this->handle = $handle;

        // Add all provided handles
        foreach ($handles as $curlHandle) {
            $this->addHandle($curlHandle);
        }
    }

    /**
     * Add a handle to the multi handle
     *
     * Equivalent to curl_multi_add_handle()
     *
     * @param Handle $handle Handle to add
     * @return self For method chaining
     * @throws MultiHandleException If adding handle fails
     */
    public function addHandle(Handle $handle): self
    {
        $result = curl_multi_add_handle($this->handle, $handle->getResource());

        if ($result !== CURLM_OK) {
            throw MultiHandleException::addHandleFailed($result);
        }

        $handleId = (int) $handle->getResource();
        $this->handles[$handleId] = $handle;

        return $this;
    }

    /**
     * Remove a handle from the multi handle
     *
     * Equivalent to curl_multi_remove_handle()
     *
     * @param Handle $handle Handle to remove
     * @return self For method chaining
     * @throws MultiHandleException If removing handle fails
     */
    public function removeHandle(Handle $handle): self
    {
        $result = curl_multi_remove_handle($this->handle, $handle->getResource());

        if ($result !== CURLM_OK) {
            throw MultiHandleException::removeHandleFailed($result);
        }

        $handleId = (int) $handle->getResource();
        unset($this->handles[$handleId]);

        return $this;
    }

    /**
     * Execute all handles
     *
     * Equivalent to curl_multi_exec()
     *
     * @param int &$stillRunning Number of handles still running (output parameter)
     * @return self For method chaining
     * @throws MultiHandleException If execution fails
     */
    public function execute(int &$stillRunning): self
    {
        $result = curl_multi_exec($this->handle, $stillRunning);

        if ($result !== CURLM_OK) {
            throw MultiHandleException::executionFailed($result);
        }

        return $this;
    }

    /**
     * Wait for activity on any handle
     *
     * Equivalent to curl_multi_select()
     *
     * @param float $timeout Maximum time to wait (in seconds)
     * @return int Number of handles with activity
     * @throws MultiHandleException If select operation fails
     */
    public function select(float $timeout = 1.0): int
    {
        $result = curl_multi_select($this->handle, $timeout);

        if ($result === -1) {
            throw MultiHandleException::selectFailed();
        }

        return $result;
    }

    /**
     * Get information about completed transfers
     *
     * Equivalent to curl_multi_info_read()
     *
     * @param int|null $messagesInQueue Number of messages still in queue (output parameter)
     * @return array|false Transfer information or false if no messages
     */
    public function getInfo(?int &$messagesInQueue = null): array|false
    {
        $queue = 0;
        $result = curl_multi_info_read($this->handle, $queue);
        $messagesInQueue = $queue;
        return $result;
    }

    /**
     * Get all attached handles
     *
     * @return array<Handle>
     */
    public function getHandles(): array
    {
        return array_values($this->handles);
    }

    /**
     * Get number of attached handles
     *
     * @return int
     */
    public function count(): int
    {
        return count($this->handles);
    }

    /**
     * Execute all handles and wait for completion
     *
     * High-level convenience method that combines execute() and select()
     *
     * @return array<Handle> Completed handles
     */
    public function executeAll(): array
    {
        $stillRunning = 0;
        $completed = [];
        $totalHandles = count($this->handles);
        $processedCount = 0;

        do {
            // Execute all handles
            $this->execute($stillRunning);

            // Wait for activity
            if ($stillRunning > 0) {
                $this->select();
            }

            // Check for completed transfers
            while ($info = $this->getInfo($messagesInQueue)) {
                $handleResource = $info['handle'];
                $handleId = (int) $handleResource;

                if (isset($this->handles[$handleId])) {
                    $handle = $this->handles[$handleId];
                    $completed[] = $handle;
                    $processedCount++;

                    // Check for errors and trigger callbacks
                    if ($info['result'] !== CURLE_OK) {
                        $this->triggerErrorCallback($handle, $info['result']);
                    } else {
                        $this->triggerCompleteCallback($handle);
                    }

                    // Trigger progress callback
                    $this->triggerProgressCallback($processedCount, $totalHandles);
                }
            }
        } while ($stillRunning > 0);

        return $completed;
    }

    /**
     * Register callback for when a handle completes successfully
     *
     * Callback signature: function(Handle $handle): void
     *
     * @param \Closure $callback Callback to execute
     * @return self For method chaining
     */
    public function onComplete(\Closure $callback): self
    {
        $this->onCompleteCallback = $callback;
        return $this;
    }

    /**
     * Register callback for when a handle encounters an error
     *
     * Callback signature: function(Handle $handle, int $errorCode): void
     *
     * @param \Closure $callback Callback to execute
     * @return self For method chaining
     */
    public function onError(\Closure $callback): self
    {
        $this->onErrorCallback = $callback;
        return $this;
    }

    /**
     * Register callback for progress updates
     *
     * Callback signature: function(int $completed, int $total): void
     *
     * @param \Closure $callback Callback to execute
     * @return self For method chaining
     */
    public function onProgress(\Closure $callback): self
    {
        $this->onProgressCallback = $callback;
        return $this;
    }

    /**
     * Trigger complete callback if registered
     */
    private function triggerCompleteCallback(Handle $handle): void
    {
        if ($this->onCompleteCallback !== null) {
            ($this->onCompleteCallback)($handle);
        }
    }

    /**
     * Trigger error callback if registered
     */
    private function triggerErrorCallback(Handle $handle, int $errorCode): void
    {
        if ($this->onErrorCallback !== null) {
            ($this->onErrorCallback)($handle, $errorCode);
        }
    }

    /**
     * Trigger progress callback if registered
     */
    private function triggerProgressCallback(int $completed, int $total): void
    {
        if ($this->onProgressCallback !== null) {
            ($this->onProgressCallback)($completed, $total);
        }
    }

    /**
     * Set multi handle option
     *
     * Equivalent to curl_multi_setopt()
     *
     * @param int $option Option constant (CURLMOPT_*)
     * @param mixed $value Option value
     * @return self For method chaining
     * @throws MultiHandleException If setting option fails
     */
    public function setOption(int $option, mixed $value): self
    {
        $result = curl_multi_setopt($this->handle, $option, $value);

        // curl_multi_setopt can return bool true on success or CURLM_OK (0)
        if ($result !== true && $result !== CURLM_OK) {
            // Ensure $result is an int for the exception
            $errorCode = is_int($result) ? $result : -1;
            throw MultiHandleException::setOptionFailed($option, $value, $errorCode);
        }

        return $this;
    }

    /**
     * Get underlying CurlMultiHandle resource
     *
     * For compatibility with existing code that needs the resource
     *
     * @return CurlMultiHandle
     */
    public function getResource(): CurlMultiHandle
    {
        return $this->handle;
    }

    /**
     * Cleanup: close multi handle and remove all handles
     */
    public function __destruct()
    {
        // Remove all handles before closing
        foreach ($this->handles as $handle) {
            curl_multi_remove_handle($this->handle, $handle->getResource());
        }

        curl_multi_close($this->handle);
    }
}
