<?php

declare(strict_types=1);

namespace Tests\Support\Api;

use RuntimeException;

final class ApiClient
{
    private const DEFAULT_TIMEOUT_SECONDS = 30;

    public function __construct(private readonly string $baseUrl)
    {
    }

    public function get(string $path, array $headers = []): ApiResponse
    {
        return $this->request('GET', $path, null, $headers);
    }

    public function post(string $path, ?array $payload = null, array $headers = []): ApiResponse
    {
        return $this->request('POST', $path, $payload, $headers);
    }

    /**
     * @param array<string, scalar|null> $fields
     * @param array<string, string|array{path:string,mime?:string,name?:string}> $files
     * @param array<string, string> $headers
     */
    public function postMultipart(string $path, array $fields = [], array $files = [], array $headers = []): ApiResponse
    {
        $postFields = [];
        foreach ($fields as $name => $value) {
            $postFields[$name] = $value === null ? '' : (string)$value;
        }

        foreach ($files as $name => $file) {
            $filePath = is_array($file) ? (string)$file['path'] : (string)$file;
            $mime = is_array($file) ? (string)($file['mime'] ?? 'application/octet-stream') : 'application/octet-stream';
            $filename = is_array($file) ? (string)($file['name'] ?? basename($filePath)) : basename($filePath);
            $postFields[$name] = new \CURLFile($filePath, $mime, $filename);
        }

        return $this->requestMultipart('POST', $path, $postFields, $headers);
    }

    public function put(string $path, ?array $payload = null, array $headers = []): ApiResponse
    {
        return $this->request('PUT', $path, $payload, $headers);
    }

    public function delete(string $path, ?array $payload = null, array $headers = []): ApiResponse
    {
        return $this->request('DELETE', $path, $payload, $headers);
    }

    public function request(string $method, string $path, ?array $payload = null, array $headers = []): ApiResponse
    {
        $curl = curl_init();
        if ($curl === false) {
            throw new RuntimeException('Unable to initialize cURL for API tests.');
        }

        $timeoutSeconds = (int)(getenv('API_TEST_REQUEST_TIMEOUT') ?: self::DEFAULT_TIMEOUT_SECONDS);
        if ($timeoutSeconds <= 0) {
            $timeoutSeconds = self::DEFAULT_TIMEOUT_SECONDS;
        }

        $responseHeaders = [];
        $normalizedHeaders = [];
        foreach ($headers as $name => $value) {
            $normalizedHeaders[] = $name . ': ' . $value;
        }

        if ($payload !== null) {
            $normalizedHeaders[] = 'Content-Type: application/json';
        }

        curl_setopt_array($curl, [
            CURLOPT_URL => rtrim($this->baseUrl, '/') . $path,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_HTTPHEADER => $normalizedHeaders,
            CURLOPT_CONNECTTIMEOUT => $timeoutSeconds,
            CURLOPT_TIMEOUT => $timeoutSeconds,
            CURLOPT_HEADERFUNCTION => static function ($curlHandle, string $headerLine) use (&$responseHeaders): int {
                $length = strlen($headerLine);
                $parts = explode(':', $headerLine, 2);
                if (count($parts) === 2) {
                    $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
                }

                return $length;
            },
        ]);

        if ($payload !== null) {
            $encodedPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if (!is_string($encodedPayload)) {
                throw new RuntimeException('Unable to encode the API request payload.');
            }

            curl_setopt($curl, CURLOPT_POSTFIELDS, $encodedPayload);
        }

        $body = curl_exec($curl);
        if ($body === false) {
            $error = curl_error($curl);
            curl_close($curl);
            throw new RuntimeException('API request failed: ' . $error);
        }

        $status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        $decoded = json_decode($body, true);

        if (getenv('EMAIL_FAKE_MODE') === '1' && class_exists(\classes\email::class)) {
            \classes\email::syncFakeDeliveries();
        }

        return new ApiResponse($status, $responseHeaders, $decoded, (string)$body);
    }

    /**
     * @param array<string, string|\CURLFile> $postFields
     * @param array<string, string> $headers
     */
    private function requestMultipart(string $method, string $path, array $postFields, array $headers = []): ApiResponse
    {
        $curl = curl_init();
        if ($curl === false) {
            throw new RuntimeException('Unable to initialize cURL for API tests.');
        }

        $timeoutSeconds = (int)(getenv('API_TEST_REQUEST_TIMEOUT') ?: self::DEFAULT_TIMEOUT_SECONDS);
        if ($timeoutSeconds <= 0) {
            $timeoutSeconds = self::DEFAULT_TIMEOUT_SECONDS;
        }

        $responseHeaders = [];
        $normalizedHeaders = [];
        foreach ($headers as $name => $value) {
            $normalizedHeaders[] = $name . ': ' . $value;
        }

        curl_setopt_array($curl, [
            CURLOPT_URL => rtrim($this->baseUrl, '/') . $path,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_HTTPHEADER => $normalizedHeaders,
            CURLOPT_CONNECTTIMEOUT => $timeoutSeconds,
            CURLOPT_TIMEOUT => $timeoutSeconds,
            CURLOPT_POSTFIELDS => $postFields,
            CURLOPT_HEADERFUNCTION => static function ($curlHandle, string $headerLine) use (&$responseHeaders): int {
                $length = strlen($headerLine);
                $parts = explode(':', $headerLine, 2);
                if (count($parts) === 2) {
                    $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
                }

                return $length;
            },
        ]);

        $body = curl_exec($curl);
        if ($body === false) {
            $error = curl_error($curl);
            curl_close($curl);
            throw new RuntimeException('API multipart request failed: ' . $error);
        }

        $status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        $decoded = json_decode($body, true);

        return new ApiResponse($status, $responseHeaders, $decoded, (string)$body);
    }
}
