<?php

declare(strict_types=1);

namespace App\Framework\HttpClient\Curl;

use App\Framework\HttpClient\Curl\Exception\HandleException;
use CurlHandle;

/**
 * Object-oriented wrapper for cURL handle
 *
 * Provides a type-safe, exception-based API for cURL operations.
 * Based on PHP RFC: Object-oriented curl API v2
 *
 * Features:
 * - Type-safe option setting via HandleOption enum
 * - Exception-based error handling (no false returns)
 * - Fluent API for option chaining
 * - Automatic resource cleanup
 * - Asymmetric visibility for error properties
 *
 * @link https://wiki.php.net/rfc/curl_oop_v2
 */
final class Handle
{
    /**
     * Equivalent to curl_errno()
     * Read-only access, privately settable
     */
    public private(set) int $errorNumber = 0;

    /**
     * Equivalent to curl_error()
     * Read-only access, privately settable
     */
    public private(set) string $errorMessage = '';

    /**
     * Underlying cURL handle resource
     */
    private readonly CurlHandle $handle;

    /**
     * Equivalent to curl_init()
     *
     * @param string|null $uri Optional URL to initialize with
     * @throws HandleException If handle initialization fails
     */
    public function __construct(?string $uri = null)
    {
        $handle = curl_init($uri);

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

        $this->handle = $handle;
    }

    /**
     * Equivalent to curl_exec() with CURLOPT_RETURNTRANSFER
     *
     * Always returns the content as a string.
     * Throws exceptions on failure instead of returning false.
     *
     * @return string Response body
     * @throws HandleException If execution fails
     */
    public function fetch(): string
    {
        // Ensure CURLOPT_RETURNTRANSFER is set
        curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, true);

        $result = curl_exec($this->handle);

        if ($result === false) {
            $this->updateErrorState();
            throw HandleException::executionFailed($this->errorMessage, $this->errorNumber);
        }

        return $result;
    }

    /**
     * Equivalent to curl_exec() with output to resource or callable
     *
     * Writes response to specified stream resource or callback.
     * Throws exceptions on failure instead of returning false.
     *
     * Callable signature: callback(CurlHandle $ch, string $data): int
     * Must return the exact number of bytes written or transfer aborts.
     *
     * @param resource|callable $out Stream resource or write callback
     * @throws HandleException If execution fails
     */
    public function execute(mixed $out): void
    {
        if (is_resource($out)) {
            curl_setopt($this->handle, CURLOPT_FILE, $out);
        } elseif (is_callable($out)) {
            curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, $out);
        } else {
            throw HandleException::invalidOutputTarget($out);
        }

        $result = curl_exec($this->handle);

        if ($result === false) {
            $this->updateErrorState();
            throw HandleException::executionFailed($this->errorMessage, $this->errorNumber);
        }
    }

    /**
     * Equivalent to curl_setopt()
     *
     * Fluent API: returns $this for method chaining
     *
     * @param HandleOption $opt Option to set
     * @param mixed $value Option value
     * @return self For method chaining
     * @throws HandleException If setting option fails
     */
    public function setOption(HandleOption $opt, mixed $value): self
    {
        // Type validation based on option
        $this->validateOptionValue($opt, $value);

        $success = curl_setopt($this->handle, $opt->value, $value);

        if (!$success) {
            $this->updateErrorState();
            throw HandleException::setOptionFailed($opt, $value, $this->errorMessage);
        }

        return $this;
    }

    /**
     * Set multiple options at once
     *
     * @param array<HandleOption|int, mixed> $options Options to set (accepts both HandleOption enums and CURLOPT_* integers)
     * @return self For method chaining
     * @throws HandleException If any option fails
     */
    public function setOptions(array $options): self
    {
        foreach ($options as $opt => $value) {
            // If $opt is an integer (raw CURLOPT_*), convert to HandleOption enum
            if (is_int($opt)) {
                $opt = HandleOption::from($opt);
            }

            $this->setOption($opt, $value);
        }

        return $this;
    }

    /**
     * Equivalent to curl_getinfo()
     *
     * @param Info|null $option Specific info to retrieve, or null for all
     * @return mixed Info value or array of all info
     */
    public function getInfo(?Info $option = null): mixed
    {
        if ($option === null) {
            return curl_getinfo($this->handle);
        }

        return curl_getinfo($this->handle, $option->value);
    }

    /**
     * Equivalent to curl_pause()
     *
     * @param Pause $flag Pause flags
     * @throws HandleException If pause operation fails
     */
    public function pause(Pause $flag): void
    {
        $result = curl_pause($this->handle, $flag->value);

        if ($result !== CURLE_OK) {
            $this->updateErrorState();
            throw HandleException::pauseFailed($flag, $this->errorMessage);
        }
    }

    /**
     * Equivalent to curl_reset()
     *
     * Resets all options to default values
     */
    public function reset(): void
    {
        curl_reset($this->handle);
        $this->errorNumber = 0;
        $this->errorMessage = '';
    }

    /**
     * Equivalent to curl_escape()
     *
     * @param string $string String to URL-encode
     * @return string URL-encoded string
     * @throws HandleException If encoding fails
     */
    public function escapeUrl(string $string): string
    {
        $result = curl_escape($this->handle, $string);

        if ($result === false) {
            throw HandleException::escapeFailed($string);
        }

        return $result;
    }

    /**
     * Equivalent to curl_unescape()
     *
     * @param string $string String to URL-decode
     * @return string URL-decoded string
     * @throws HandleException If decoding fails
     */
    public function unescapeUrl(string $string): string
    {
        $result = curl_unescape($this->handle, $string);

        if ($result === false) {
            throw HandleException::unescapeFailed($string);
        }

        return $result;
    }

    /**
     * Equivalent to curl_upkeep()
     *
     * Performs connection upkeep checks
     *
     * @throws HandleException If upkeep fails
     */
    public function upkeep(): void
    {
        $result = curl_upkeep($this->handle);

        if ($result !== CURLE_OK) {
            $this->updateErrorState();
            throw HandleException::upkeepFailed($this->errorMessage);
        }
    }

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

    /**
     * Update error state from cURL handle
     */
    private function updateErrorState(): void
    {
        $this->errorNumber = curl_errno($this->handle);
        $this->errorMessage = curl_error($this->handle);
    }

    /**
     * Validate option value against expected type
     *
     * @throws HandleException If value type doesn't match expected type
     */
    private function validateOptionValue(HandleOption $opt, mixed $value): void
    {
        $typeValid = match (true) {
            $opt->expectsBoolean() => is_bool($value),
            $opt->expectsInteger() => is_int($value),
            $opt->expectsString() => is_string($value),
            $opt->expectsArray() => is_array($value),
            $opt->expectsCallable() => is_callable($value),
            $opt->expectsResource() => is_resource($value),
            default => true, // Unknown type, allow it
        };

        if (!$typeValid) {
            throw HandleException::invalidOptionType($opt, $value);
        }
    }

    /**
     * Cleanup: close cURL handle
     */
    public function __destruct()
    {
        curl_close($this->handle);
    }
}
