<?php

namespace Datalumo\PhpSdk;

use Datalumo\PhpSdk\DataObjects\StreamEvent;
use Datalumo\PhpSdk\DataObjects\StreamResponse;
use Datalumo\PhpSdk\Exceptions\AuthenticationException;
use Datalumo\PhpSdk\Exceptions\DatalumoException;
use Datalumo\PhpSdk\Exceptions\NotFoundException;
use Datalumo\PhpSdk\Exceptions\QuotaExceededException;
use Datalumo\PhpSdk\Exceptions\ValidationException;
use Generator;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;

class HttpClient
{
    private Client $client;

    public function __construct(
        private readonly string $baseUrl,
        private readonly string $token,
    ) {
        $this->client = new Client([
            'base_uri' => rtrim($baseUrl, '/').'/api/v1/',
            'headers' => [
                'Authorization' => 'Bearer '.$this->token,
                'Accept' => 'application/json',
                'Content-Type' => 'application/json',
            ],
        ]);
    }

    public function get(string $uri, array $query = []): array
    {
        return $this->request('GET', $uri, ['query' => $query]);
    }

    public function post(string $uri, array $data = []): array
    {
        return $this->request('POST', $uri, ['json' => $data]);
    }

    public function patch(string $uri, array $data = []): array
    {
        return $this->request('PATCH', $uri, ['json' => $data]);
    }

    public function put(string $uri, array $data = []): array
    {
        return $this->request('PUT', $uri, ['json' => $data]);
    }

    public function delete(string $uri): void
    {
        $this->request('DELETE', $uri);
    }

    public function getStream(string $uri, array $query = []): StreamResponse
    {
        return $this->streamRequest('GET', $uri, ['query' => $query]);
    }

    public function postStream(string $uri, array $data = []): StreamResponse
    {
        return $this->streamRequest('POST', $uri, ['json' => $data]);
    }

    private function request(string $method, string $uri, array $options = []): array
    {
        try {
            $response = $this->client->request($method, $uri, $options);

            if ($response->getStatusCode() === 204) {
                return [];
            }

            return json_decode($response->getBody()->getContents(), true);
        } catch (ClientException $e) {
            $this->handleClientException($e);
        } catch (GuzzleException $e) {
            throw new DatalumoException($e->getMessage(), $e->getCode(), previous: $e);
        }
    }

    private function streamRequest(string $method, string $uri, array $options = []): StreamResponse
    {
        try {
            $response = $this->client->request($method, $uri, $options);
        } catch (ClientException $e) {
            $this->handleClientException($e);
        } catch (GuzzleException $e) {
            throw new DatalumoException($e->getMessage(), $e->getCode(), previous: $e);
        }

        $headers = $response->getHeaders();
        $body = $response->getBody();

        $events = (function () use ($body): Generator {
            $buffer = '';

            while (! $body->eof()) {
                $chunk = $body->read(1024);
                $buffer .= $chunk;

                while (($pos = strpos($buffer, "\n\n")) !== false) {
                    $line = substr($buffer, 0, $pos);
                    $buffer = substr($buffer, $pos + 2);

                    if (! str_starts_with($line, 'data: ')) {
                        continue;
                    }

                    $data = substr($line, 6);

                    if ($data === '[DONE]') {
                        return;
                    }

                    $decoded = json_decode($data, true);

                    if (is_array($decoded)) {
                        yield StreamEvent::fromArray($decoded);
                    }
                }
            }
        })();

        return new StreamResponse($events, $headers);
    }

    private function handleClientException(ClientException $e): never
    {
        $response = $e->getResponse();
        $status = $response->getStatusCode();
        $body = json_decode($response->getBody()->getContents(), true) ?? [];
        $message = $body['message'] ?? $e->getMessage();

        throw match ($status) {
            401, 403 => new AuthenticationException($message, $status, $body, $e),
            402 => new QuotaExceededException($message, $status, $body, $e),
            404 => new NotFoundException($message, $status, $body, $e),
            422 => new ValidationException($message, $status, $body, $e),
            default => new DatalumoException($message, $status, $body, $e),
        };
    }
}
