<?php

declare(strict_types=1);

namespace KimaAI\Ai\Providers;

use KimaAI\Ai\Providers\Contracts\ProviderClientInterface;
use KimaAI\Ai\Providers\Http\CurlHttpClient;
use KimaAI\Ai\Providers\Http\HttpClientInterface;
use KimaAI\Ai\Providers\Http\HttpRequest;
use KimaAI\Ai\Providers\Support\ProviderMetadata;
use RuntimeException;


abstract class AbstractClient implements ProviderClientInterface {

  /**
   * Provider API key.
   *
   * @var string
   */
  public string $api_key;
  /**
   * Provider API base URL without trailing slash.
   *
   * @var string
   */
  public string $base_url;
  /**
   * Request timeout in seconds.
   *
   * @var int
   */
  public int $timeout = 300;
  /**
   * Whether deterministic test responses are enabled.
   *
   * @var bool
   */
  public bool $test_mode = false;
  /**
   * HTTP transport implementation.
   *
   * @var HttpClientInterface
   */
  protected HttpClientInterface $http_client;

  public function __construct(
    string $api_key,
    string $base_url,
    int $timeout = 300,
    ?HttpClientInterface $http_client = null
  ) {
    $this->api_key = $api_key;
    $this->base_url = rtrim($base_url, '/');
    $this->timeout = $timeout;
    $this->test_mode = \defined('\KIMAAI_API_TEST_MODE') && \KIMAAI_API_TEST_MODE;
    $this->http_client = $http_client ?? new CurlHttpClient();
  }

  abstract protected function buildHeaders(bool $stream): array;

  abstract public function metadata(): ProviderMetadata;

  public function supports(string $capability): bool {
    return $this->metadata()->supports($capability);
  }

  public function isTestMode(): bool {
    return $this->test_mode;
  }

  public function withHttpClient(HttpClientInterface $http_client): static {
    $this->http_client = $http_client;
    return $this;
  }

  /**
   * Perform an HTTP request.
   *
   * @param string $method HTTP method
   * @param string $path Relative path
   * @param array $payload JSON payload or query
   * @param bool $stream Whether to stream
   * @param array $extra_headers Optional provider-specific headers
   * @param callable|null $on_chunk Optional chunk handler for streaming mode: function(string $chunk): void
   * @return array
   *
   * @throws RuntimeException When the provider returns an error response.
   */
  public function request(
    string $method,
    string $path,
    array $payload = [],
    bool $stream = false,
    array $extra_headers = [],
    ?callable $on_chunk = null
  ): array {
    $url = str_starts_with($path, 'http') ? $path : $this->base_url . $path;
    $this->debug('Requesting ' . $method . ' ' . $url . ' with payload: ' . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

    $headers = array_merge(
      $this->buildHeaders($stream),
      $extra_headers
    );

    if (!$stream) {
      $response = $this->http_client->send(new HttpRequest(
        strtoupper($method),
        $url,
        $headers,
        $payload,
        $this->timeout
      ));

      $decoded = $response->json();

      if ($response->status >= 400) {
        $msg = $decoded['error'] ?? ('HTTP ' . $response->status);
        $error_message = 'API error: ' . (is_string($msg) ? $msg : json_encode($msg));
        $this->error($error_message);
        throw new RuntimeException($error_message);
      }

      $this->debug('Response from ' . $method . ' ' . $url . ': ' . json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
      return $decoded;
    }

    $ch = curl_init($url);

    curl_setopt_array($ch, [
      CURLOPT_CUSTOMREQUEST => strtoupper($method),
      CURLOPT_RETURNTRANSFER => !$stream,
      CURLOPT_HTTPHEADER => $headers,
      CURLOPT_TIMEOUT => $this->timeout,
      CURLOPT_CAINFO => ABSPATH . WPINC . '/certificates/ca-bundle.crt',
    ]);

    if ($method !== 'GET' && !empty($payload)) {
      $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
    }
    elseif ($method === 'GET' && !empty($payload)) {
      $query = http_build_query($payload);
      curl_setopt($ch, CURLOPT_URL, $url . '?' . $query);
    }

    if ($stream) {
      // Capture the streamed body so we can extract provider error details if status >= 400
      $stream_buffer = '';

      curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, string $data) use ($on_chunk, &$stream_buffer) {
        $stream_buffer .= $data;

        if (is_callable($on_chunk)) {
          $on_chunk($data);
        }
        else {
          echo $data;
          if (function_exists('flush')) {
            @flush();
          }
        }

        return strlen($data);
      });

      $ok = curl_exec($ch);
      if ($ok === false) {
        $error_message = 'cURL error: ' . curl_error($ch);
        $this->error($error_message);
        curl_close($ch);
        throw new RuntimeException($error_message);
      }

      $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
      curl_close($ch);

      if ($status >= 400) {
        // Try to parse OpenAI's SSE error body (or JSON error) so logs show the real issue
        $detail = '';

        // If SSE, pull the last "data:" JSON chunk
        $normalized = str_replace("\r\n", "\n", $stream_buffer);
        if (strpos($normalized, 'data:') !== false) {
          $parts = preg_split("/\n\n/", $normalized) ?: [];
          $last_json = '';
          foreach (array_reverse($parts) as $frame) {
            $lines = preg_split("/\n/", trim($frame)) ?: [];
            $data_lines = [];
            foreach ($lines as $line) {
              $line = trim($line);
              if (str_starts_with($line, 'data:')) {
                $data_lines[] = ltrim(substr($line, 5));
              }
            }
            $candidate = trim(implode("\n", $data_lines));
            if ($candidate !== '' && $candidate !== '[DONE]') {
              $last_json = $candidate;
              break;
            }
          }

          if ($last_json !== '') {
            $decoded = json_decode($last_json, true);
            if (is_array($decoded)) {
              // Common OpenAI error placements:
              $detail =
              (string) ($decoded['error']['message'] ?? '') ?:
              (string) ($decoded['message'] ?? '') ?:
              wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            }
            else {
              $detail = $last_json;
            }
          }
        }
        else {
          // Maybe not SSE: try plain JSON
          $decoded = json_decode($stream_buffer, true);
          if (is_array($decoded)) {
            $detail =
            (string) ($decoded['error']['message'] ?? '') ?:
            (string) ($decoded['message'] ?? '') ?:
            wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
          }
          else {
            $detail = trim($stream_buffer);
          }
        }

        $error_message = 'API error: HTTP ' . $status . ($detail ? ' - ' . $detail : '');
        $this->error($error_message);
        throw new RuntimeException($error_message);
      }

      return ['status' => $status];
    }

    return [];
  }

  protected function debug(string $message): void {
    if (class_exists('\KimaAI\Logger', false)) {
      \KimaAI\Logger::debug($message);
    }
  }

  protected function error(string $message): void {
    if (class_exists('\KimaAI\Logger', false)) {
      \KimaAI\Logger::error($message);
    }
  }
}
