<?php
/**
 * Chatbot provider error normalization.
 *
 * @package KimaAI\ChatBot
 */

declare(strict_types=1);

namespace KimaAI\ChatBot;

defined('ABSPATH') || exit;

final class ChatbotProviderErrorNormalizer {

  /**
   * Normalize a raw provider error into a structured error array.
   *
   * @param mixed $error Raw error (Throwable, array, or string).
   * @param string $provider Active provider slug.
   * @param int $fallback_status HTTP fallback status code.
   * @return array
   */
  public function normalize(mixed $error, string $provider = 'openai', int $fallback_status = 500): array {
    $raw_message = '';

    if ($error instanceof \Throwable) {
      $raw_message = $error->getMessage();
    } elseif (is_string($error)) {
      $raw_message = $error;
    } elseif (is_array($error)) {
      $raw_message = wp_json_encode($error, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '';
    }

    $payload = $this->extractErrorPayload($error);

    $message = trim((string) ($payload['message'] ?? ''));
    $type = trim((string) ($payload['type'] ?? ''));
    $code = trim((string) ($payload['code'] ?? ''));
    $param = isset($payload['param']) ? (string) $payload['param'] : null;
    $status = isset($payload['status']) ? (int) $payload['status'] : $fallback_status;

    if ($status <= 0) {
      $status = $fallback_status;
    }

    if ($message === '' && $raw_message !== '') {
      $message = $raw_message;
    }

    return [
      'provider'    => $provider,
      'userMessage' => $this->buildProviderUserMessage($provider, $message, $type, $code, $status),
      'message'     => $message,
      'type'        => $type,
      'code'        => $code,
      'param'       => $param,
      'status'      => $status,
      'retryable'   => in_array($status, [408, 409, 429], true) || $status >= 500,
      'rawMessage'  => $raw_message,
    ];
  }

  /**
   * Extract a structured error payload from a raw error value.
   *
   * @param mixed $error Raw error (Throwable, array, or string).
   * @return array
   */
  private function extractErrorPayload(mixed $error): array {
    if (is_array($error)) {
      if (isset($error['response']['error']) && is_array($error['response']['error'])) {
        return $error['response']['error'];
      }

      if (isset($error['error']) && is_array($error['error'])) {
        return $error['error'];
      }

      return $error;
    }

    if ($error instanceof \Throwable) {
      return $this->extractErrorPayloadFromString($error->getMessage());
    }

    if (is_string($error)) {
      return $this->extractErrorPayloadFromString($error);
    }

    return [];
  }

  /**
   * Parse a raw error string into a structured error array, extracting HTTP status when present.
   *
   * @param string $message Raw error message.
   * @return array
   */
  private function extractErrorPayloadFromString(string $message): array {
    $message = trim($message);
    if ($message === '') {
      return [];
    }

    $status = null;
    if (preg_match('/\bHTTP\s+(\d{3})\b/i', $message, $matches) === 1) {
      $status = (int) ($matches[1] ?? 0);
    }

    $decoded = json_decode($message, true);
    if (is_array($decoded)) {
      if (isset($decoded['error']) && is_array($decoded['error'])) {
        if ($status && !isset($decoded['error']['status'])) {
          $decoded['error']['status'] = $status;
        }
        return $decoded['error'];
      }
      if ($status && !isset($decoded['status'])) {
        $decoded['status'] = $status;
      }
      return $decoded;
    }

    $start = strpos($message, '{');
    $end = strrpos($message, '}');

    if ($start !== false && $end !== false && $end > $start) {
      $json_part = substr($message, $start, $end - $start + 1);
      $decoded = json_decode($json_part, true);

      if (is_array($decoded)) {
        if (isset($decoded['error']) && is_array($decoded['error'])) {
          if ($status && !isset($decoded['error']['status'])) {
            $decoded['error']['status'] = $status;
          }
          return $decoded['error'];
        }
        if ($status && !isset($decoded['status'])) {
          $decoded['status'] = $status;
        }
        return $decoded;
      }
    }

    $payload = ['message' => $message];
    if ($status) {
      $payload['status'] = $status;
    }

    return $payload;
  }

  /**
   * Build a human-readable error message for the given provider and error details.
   *
   * @param string $provider Active provider slug.
   * @param string $message Raw error message.
   * @param string $type Error type.
   * @param string $code Error code.
   * @param int $status HTTP status code.
   * @return string
   */
  private function buildProviderUserMessage(string $provider, string $message, string $type, string $code, int $status): string {
    $key = strtolower($code !== '' ? $code : $type);
    $message_lc = strtolower($message);

    if ($provider === 'openai' || $provider === 'openrouter') {
      $provider_label = $provider === 'openrouter' ? __('OpenRouter', 'kimaai') : __('OpenAI', 'kimaai');

      return match (true) {
        $key === 'insufficient_quota' =>
        /* translators: %s: AI provider name (e.g. OpenAI, OpenRouter). */
        sprintf(__('%s API quota exceeded. Please check the configured plan and billing details.', 'kimaai'), $provider_label),

        $provider === 'openrouter' &&
        ($status === 402 ||
        str_contains($message_lc, 'requires more credits') ||
        str_contains($message_lc, 'can only afford') ||
        str_contains($message_lc, 'fewer max_tokens')) =>
        __('OpenRouter does not have enough credits for this request. Reduce Max Tokens or add more credits to the OpenRouter account.', 'kimaai'),

        $key === 'invalid_api_key',
        $key === 'authentication_error',
        $status === 401 =>
        /* translators: %s: AI provider name (e.g. OpenAI, OpenRouter). */
        sprintf(__('%s authentication failed. Please check the configured API key.', 'kimaai'), $provider_label),

        $key === 'rate_limit_exceeded',
        $status === 429 =>
        /* translators: %s: AI provider name (e.g. OpenAI, OpenRouter). */
        sprintf(__('%s rate limit reached. Please try again in a moment.', 'kimaai'), $provider_label),

        $key === 'model_not_found' =>
        /* translators: %s: AI provider name (e.g. OpenAI, OpenRouter). */
        sprintf(__('The selected %s model is not available. Please check the chatbot model setting.', 'kimaai'), $provider_label),

        str_contains($message_lc, 'not a valid model id'),
        str_contains($message_lc, 'invalid model'),
        str_contains($message_lc, 'model not found') =>
        /* translators: %s: AI provider name (e.g. OpenAI, OpenRouter). */
        sprintf(__('The selected %s model is not available. Please check the chatbot model setting.', 'kimaai'), $provider_label),

        $key === 'context_length_exceeded' =>
        sprintf(
          /* translators: %s: AI provider name (e.g. OpenAI, OpenRouter). */
          __('The request is too large for the selected %s model. Please shorten the message or reduce the context.', 'kimaai'),
          $provider_label
        ),

        $status >= 500 =>
        /* translators: %s: AI provider name (e.g. OpenAI, OpenRouter). */
        sprintf(__('%s is currently unavailable. Please try again in a moment.', 'kimaai'), $provider_label),

        $message !== '' =>
        /* translators: %1$s: AI provider name (e.g. OpenAI). %2$s: raw error message from the provider. */
        sprintf(__('%1$s API error: %2$s', 'kimaai'), $provider_label, $message),

        default =>
        /* translators: %s: AI provider name (e.g. OpenAI, OpenRouter). */
        sprintf(__('A %s API error occurred.', 'kimaai'), $provider_label),
      };
    }

    if ($message !== '') {
      return $message;
    }

    return __('Something went wrong while processing the AI request.', 'kimaai');
  }
}
