<?php
/**
 * Abstract AI Provider.
 *
 * Base class for all AI providers with common functionality.
 *
 * @package Inqyra
 */

// If this file is called directly, abort.
if (!defined('WPINC')) {
    die;
}

/**
 * Abstract AI Provider class.
 */
abstract class Inqyra_AI_Provider_Abstract implements Inqyra_AI_Provider_Interface {

    /**
     * API key.
     *
     * @var string
     */
    protected $api_key;

    /**
     * Request timeout in seconds.
     *
     * @var int
     */
    protected $timeout = 60;

    /**
     * Constructor.
     *
     * @param string $api_key The API key.
     */
    public function __construct(string $api_key = '') {
        $this->api_key = $api_key;
    }

    /**
     * Check if the provider is configured.
     *
     * @return bool Whether the provider has an API key.
     */
    public function is_configured(): bool {
        return !empty($this->api_key);
    }

    /**
     * Make an HTTP POST request.
     *
     * @param string $url     The URL to request.
     * @param array  $data    The request body data.
     * @param array  $headers Additional headers.
     * @return array Response data.
     * @throws Exception On HTTP or API error.
     */
    protected function make_request(string $url, array $data, array $headers = []): array {
        $default_headers = [
            'Content-Type' => 'application/json',
        ];

        $response = wp_remote_post($url, [
            'headers' => array_merge($default_headers, $headers),
            'body' => wp_json_encode($data),
            'timeout' => $this->timeout,
            'data_format' => 'body',
        ]);

        if (is_wp_error($response)) {
            throw new Exception(esc_html($response->get_error_message()));
        }

        $status_code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);
        $decoded = json_decode($body, true);

        if ($status_code >= 400) {
            $error_message = $this->extract_error_message($decoded, $status_code);
            throw new Exception(esc_html($error_message));
        }

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new Exception(esc_html('Invalid JSON response from API'));
        }

        return $decoded;
    }

    /**
     * Extract error message from API response.
     *
     * @param array|null $response    The decoded response.
     * @param int        $status_code The HTTP status code.
     * @return string Error message.
     */
    protected function extract_error_message($response, int $status_code): string {
        if (is_array($response)) {
            // Common error formats
            if (isset($response['error']['message'])) {
                return $response['error']['message'];
            }
            if (isset($response['error'])) {
                return is_string($response['error']) ? $response['error'] : wp_json_encode($response['error']);
            }
            if (isset($response['message'])) {
                return $response['message'];
            }
        }

        return "API request failed with status code: {$status_code}";
    }

    /**
     * Calculate cost for token usage.
     *
     * @param int    $input_tokens  Number of input tokens.
     * @param int    $output_tokens Number of output tokens.
     * @param string $model         Model identifier.
     * @return float Cost in USD.
     */
    abstract public function calculate_cost(int $input_tokens, int $output_tokens, string $model): float;

    /**
     * Log an API call for debugging.
     *
     * @param string $provider Provider name.
     * @param string $model    Model used.
     * @param array  $tokens   Token usage.
     * @param float  $cost     Cost in USD.
     */
    protected function log_api_call(string $provider, string $model, array $tokens, float $cost): void {
        $settings = get_option('inqyra_settings', []);
        if (!empty($settings['debug_mode'])) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated behind debug_mode setting.
            error_log(sprintf(
                'Inqyra API Call: provider=%s, model=%s, input_tokens=%d, output_tokens=%d, cost=$%.6f',
                $provider,
                $model,
                $tokens['input'] ?? 0,
                $tokens['output'] ?? 0,
                $cost
            ));
        }
    }

    /**
     * Make an HTTP GET request.
     *
     * @param string $url     The URL to request.
     * @param array  $headers Additional headers.
     * @return array Response data.
     * @throws Exception On HTTP or API error.
     */
    protected function make_get_request(string $url, array $headers = []): array {
        $response = wp_remote_get($url, [
            'headers' => $headers,
            'timeout' => $this->timeout,
        ]);

        if (is_wp_error($response)) {
            throw new Exception(esc_html($response->get_error_message()));
        }

        $status_code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);
        $decoded = json_decode($body, true);

        if ($status_code >= 400) {
            $error_message = $this->extract_error_message($decoded, $status_code);
            throw new Exception(esc_html($error_message));
        }

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new Exception(esc_html('Invalid JSON response from API'));
        }

        return $decoded;
    }

    /**
     * Get the hardcoded models for this provider.
     * Override in subclasses to provide curated model lists.
     *
     * @return array Associative array of model_id => display_name.
     */
    public function get_hardcoded_models(): array {
        return [];
    }

    /**
     * Get available models, merging hardcoded list with cached API models.
     *
     * @return array Associative array of model_id => display_name.
     */
    public function get_models(): array {
        $models = $this->get_hardcoded_models();

        $cached = get_transient('inqyra_api_models_' . $this->get_id());
        if (!$cached) {
            return $models;
        }

        foreach ($cached as $model_id => $display_name) {
            if (!isset($models[$model_id])) {
                $pricing = static::PRICING[$model_id] ?? null;
                if ($pricing) {
                    $models[$model_id] = sprintf('%s ($%.2f/$%.2f per 1M)', $display_name, $pricing['input'], $pricing['output']);
                } else {
                    $models[$model_id] = $display_name . ' (' . __('price unknown', 'inqyra') . ')';
                }
            }
        }

        return $models;
    }

    /**
     * Fetch available models from the provider's API.
     * Default implementation returns empty array - override in subclasses.
     *
     * @return array Associative array of model_id => display_name.
     */
    public function fetch_available_models(): array {
        return [];
    }

    /**
     * Get models with pricing information.
     * Combines get_models() with pricing data.
     *
     * @return array Array of models with pricing.
     */
    public function get_models_with_pricing(): array {
        $models = $this->get_models();
        $result = [];

        foreach ($models as $id => $name) {
            $pricing = $this->get_model_pricing($id);
            $result[$id] = [
                'name' => $name,
                'input_price' => $pricing['input'] ?? 0,
                'output_price' => $pricing['output'] ?? 0,
            ];
        }

        return $result;
    }

    /**
     * Get pricing for a specific model.
     * Override in subclasses to provide actual pricing.
     *
     * @param string $model Model identifier.
     * @return array Pricing with 'input' and 'output' per 1M tokens.
     */
    protected function get_model_pricing(string $model): array {
        return ['input' => 0, 'output' => 0];
    }

    /**
     * Get cached models or fetch from API.
     *
     * @param string $cache_key Unique cache key for this provider.
     * @param int    $expiry    Cache expiry in seconds (default 24 hours).
     * @return array Cached or freshly fetched models.
     */
    protected function get_cached_models(string $cache_key, int $expiry = 86400): array {
        $cached = get_transient($cache_key);

        if ($cached !== false) {
            return $cached;
        }

        try {
            $models = $this->fetch_available_models();
            if (!empty($models)) {
                set_transient($cache_key, $models, $expiry);
                return $models;
            }
        } catch (Exception $e) {
            // Log error but don't fail - return empty array
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated behind WP_DEBUG.
                error_log('Inqyra: Failed to fetch models: ' . $e->getMessage());
            }
        }

        return [];
    }
}
