<?php

namespace Antimanual;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * AI Provider Factory - Single source of truth for AI provider selection.
 *
 * Manages all five supported providers (OpenAI, Gemini, Anthropic, xAI, DeepSeek)
 * through a slug-keyed registry. Providers without native embeddings (Anthropic,
 * xAI, DeepSeek) automatically fall back to OpenAI or Gemini for embedding
 * operations, whichever has a configured API key.
 *
 * @package Antimanual
 * @since 1.5.0
 */
class AIProvider {

	/**
	 * Registry of all supported providers.
	 *
	 * Keys: slug used in option storage, frontend, and REST routes.
	 * Values: class name, display label, option keys, and capability flags.
	 *
	 * @var array<string, array{class:string,label:string,key:string,model:string,embeddings:bool,images:bool}>
	 */
	private const PROVIDERS = [
		'openai'    => [
			'class'      => OpenAI::class,
			'label'      => 'OpenAI',
			'key'        => 'openai_api_key',
			'model'      => 'openai_response_model',
			'embeddings' => true,
			'images'     => true,
		],
		'gemini'    => [
			'class'      => Gemini::class,
			'label'      => 'Gemini',
			'key'        => 'gemini_api_key',
			'model'      => 'gemini_response_model',
			'embeddings' => true,
			'images'     => true,
		],
		'anthropic' => [
			'class'      => Anthropic::class,
			'label'      => 'Anthropic',
			'key'        => 'anthropic_api_key',
			'model'      => 'anthropic_response_model',
			'embeddings' => false,
			'images'     => false,
		],
		'xai'       => [
			'class'      => Xai::class,
			'label'      => 'xAI Grok',
			'key'        => 'xai_api_key',
			'model'      => 'xai_response_model',
			'embeddings' => false,
			'images'     => true,
		],
		'deepseek'  => [
			'class'      => DeepSeek::class,
			'label'      => 'DeepSeek',
			'key'        => 'deepseek_api_key',
			'model'      => 'deepseek_response_model',
			'embeddings' => false,
			'images'     => false,
		],
		'opencodezen' => [
			'class'      => OpenCodeZen::class,
			'label'      => 'OpenCode Zen',
			'key'        => 'opencodezen_api_key',
			'model'      => 'opencodezen_response_model',
			'embeddings' => false,
			'images'     => false,
		],
	];

	/**
	 * Singleton instance.
	 *
	 * @var AIProvider|null
	 */
	private static $instance = null;

	/**
	 * Current active provider slug.
	 *
	 * @var string
	 */
	private $provider_name = 'openai';

	/**
	 * Current active provider instance.
	 *
	 * @var OpenAI|Gemini|Anthropic|Xai|DeepSeek
	 */
	private $provider = null;

	/**
	 * Cached embedding-fallback provider instance.
	 *
	 * Used when the active provider has no native embeddings support.
	 *
	 * @var OpenAI|Gemini|null
	 */
	private $embedding_provider = null;

	/**
	 * Slug of the resolved embedding-fallback provider.
	 *
	 * @var string
	 */
	private $embedding_provider_slug = '';

	/**
	 * Private constructor to enforce singleton pattern.
	 */
	private function __construct() {
		$saved = (string) atml_option( 'last_active_provider' );
		$this->provider_name = isset( self::PROVIDERS[ $saved ] ) ? $saved : 'openai';
		$this->provider      = $this->create_provider();
	}

	/**
	 * Get the singleton instance.
	 *
	 * @return AIProvider
	 */
	public static function instance(): AIProvider {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Get the current AI provider instance.
	 *
	 * @return OpenAI|Gemini|Anthropic|Xai|DeepSeek
	 */
	public static function get() {
		return self::instance()->provider;
	}

	/**
	 * Get the current provider slug.
	 *
	 * @return string e.g. 'openai', 'gemini', 'anthropic', 'xai', 'deepseek'.
	 */
	public static function get_name(): string {
		return self::instance()->provider_name;
	}

	/**
	 * Return the complete PROVIDERS registry.
	 *
	 * @return array
	 */
	public static function get_providers(): array {
		return self::PROVIDERS;
	}

	// ─── Per-provider boolean helpers ──────────────────────────────────────────

	/** @return bool */
	public static function is_openai(): bool      { return 'openai'      === self::get_name(); }
	/** @return bool */
	public static function is_gemini(): bool       { return 'gemini'      === self::get_name(); }
	/** @return bool */
	public static function is_anthropic(): bool     { return 'anthropic'   === self::get_name(); }
	/** @return bool */
	public static function is_xai(): bool          { return 'xai'         === self::get_name(); }
	/** @return bool */
	public static function is_deepseek(): bool     { return 'deepseek'    === self::get_name(); }
	/** @return bool */
	public static function is_opencodezen(): bool  { return 'opencodezen' === self::get_name(); }

	/**
	 * Check if the current provider has a valid API key configured.
	 *
	 * @return bool
	 */
	public static function has_api_key(): bool {
		$slug = self::get_name();
		$key  = self::PROVIDERS[ $slug ]['key'] ?? '';
		return ! empty( atml_option( $key ) );
	}

	/**
	 * Get the API key for the current provider.
	 *
	 * @return string
	 */
	public static function get_api_key(): string {
		$slug = self::get_name();
		$key  = self::PROVIDERS[ $slug ]['key'] ?? '';
		return (string) ( atml_option( $key ) ?: '' );
	}

	/**
	 * Get the display name for the current provider.
	 *
	 * @return string Human-readable provider label.
	 */
	public static function get_display_name(): string {
		$slug = self::get_name();
		return self::PROVIDERS[ $slug ]['label'] ?? $slug;
	}

	// ─── Embedding provider resolution ─────────────────────────────────────────

	/**
	 * Resolve the provider instance responsible for generating embeddings.
	 *
	 * When the active provider supports embeddings natively, returns the active
	 * instance. Otherwise falls back to OpenAI, then Gemini, then WP_Error.
	 *
	 * @return OpenAI|Gemini|\WP_Error
	 */
	private function resolve_embedding_provider() {
		$slug = $this->provider_name;

		if ( self::PROVIDERS[ $slug ]['embeddings'] ) {
			$this->embedding_provider_slug = $slug;
			return $this->embedding_provider = $this->provider;
		}

		if ( $this->embedding_provider ) {
			return $this->embedding_provider;
		}

		if ( ! empty( atml_option( 'openai_api_key' ) ) ) {
			$this->embedding_provider_slug = 'openai';
			return $this->embedding_provider = new OpenAI();
		}

		if ( ! empty( atml_option( 'gemini_api_key' ) ) ) {
			$this->embedding_provider_slug = 'gemini';
			return $this->embedding_provider = new Gemini();
		}

		return new \WP_Error(
			'embeddings_unavailable',
			__( 'Embeddings require an OpenAI or Gemini API key. Add one in Antimanual → AI Provider.', 'antimanual' )
		);
	}

	/**
	 * Get the slug of the provider that will actually handle embeddings.
	 *
	 * Use this — not get_name() — when tagging KB rows with a provider field,
	 * so that similarity search uses the correct vector space.
	 *
	 * @return string
	 */
	public static function get_embedding_provider_name(): string {
		$self = self::instance();
		$slug = $self->provider_name;

		if ( self::PROVIDERS[ $slug ]['embeddings'] ) {
			return $slug;
		}

		// Resolve fallback to populate the slug.
		$result = $self->resolve_embedding_provider();

		if ( is_wp_error( $result ) ) {
			return $slug;
		}

		return $self->embedding_provider_slug;
	}

	// ─── Factory methods ───────────────────────────────────────────────────────

	/**
	 * Create the provider instance based on the saved preference.
	 *
	 * @return OpenAI|Gemini|Anthropic|Xai|DeepSeek
	 */
	private function create_provider() {
		$class = self::PROVIDERS[ $this->provider_name ]['class'] ?? OpenAI::class;
		return new $class();
	}

	/**
	 * Force refresh of the provider instance.
	 *
	 * Useful when settings change and the provider needs to be reloaded.
	 *
	 * @return AIProvider
	 */
	public static function refresh(): AIProvider {
		self::$instance = null;
		return self::instance();
	}

	// ─── Delegating public API ─────────────────────────────────────────────────

	/**
	 * Get a reply from the AI provider.
	 *
	 * Wraps the provider's get_reply() and normalises successful responses to
	 * plain text for non-conversational features. Use the provider directly
	 * when the conversation ID is needed.
	 *
	 * @param array  $input             Array of message objects or structured input.
	 * @param string $conversation_id   Optional conversation ID.
	 * @param string $instructions      Optional system instructions.
	 * @param int    $max_output_tokens Optional maximum output tokens.
	 * @return string|array The reply string, or an error array.
	 */
	public static function get_reply( $input, string $conversation_id = '', string $instructions = '', int $max_output_tokens = 0 ) {
		if ( ! self::has_api_key() ) {
			return [
				'error' => sprintf(
					/* translators: %s: AI provider display name */
					__( '%s API key is not configured.', 'antimanual' ),
					self::get_display_name()
				),
			];
		}

		$response = self::get()->get_reply( $input, $conversation_id, $instructions, $max_output_tokens );

		if ( is_array( $response ) && ! isset( $response['error'] ) && isset( $response['reply'] ) ) {
			return (string) $response['reply'];
		}

		return $response;
	}

	/**
	 * Generate an image using the current provider.
	 *
	 * @param string $prompt The image prompt.
	 * @param string $size   Image size. Default '1024x1024'.
	 * @return string|\WP_Error URL of the generated image or WP_Error.
	 */
	public static function generate_image( string $prompt, string $size = '1024x1024' ) {
		if ( ! self::has_api_key() ) {
			return new \WP_Error( 'api_key_missing', __( 'AI API key is not configured.', 'antimanual' ) );
		}

		$slug = self::get_name();
		if ( ! self::PROVIDERS[ $slug ]['images'] ) {
			return new \WP_Error( 'not_supported', __( 'Image generation is not supported by the current provider.', 'antimanual' ) );
		}

		return self::get()->generate_image( $prompt, $size );
	}

	/**
	 * Generate an embedding using the current provider (or a fallback).
	 *
	 * @param string $content The content to generate an embedding for.
	 * @return array|\WP_Error The embedding array or WP_Error on failure.
	 */
	public static function generate_embedding( string $content ) {
		$self     = self::instance();
		$provider = $self->resolve_embedding_provider();

		if ( is_wp_error( $provider ) ) {
			return $provider;
		}

		return $provider->generate_embedding( $content );
	}

	/**
	 * Get the embedding model name used by the resolved embedding provider.
	 *
	 * @return string
	 */
	public static function get_embedding_model(): string {
		$self     = self::instance();
		$provider = $self->resolve_embedding_provider();

		if ( is_wp_error( $provider ) ) {
			return 'text-embedding-3-small';
		}

		if ( method_exists( $provider, 'get_embedding_model' ) ) {
			return $provider->get_embedding_model();
		}

		return 'text-embedding-3-small';
	}

	/**
	 * Create a new conversation.
	 *
	 * @param array $items Initial conversation items.
	 * @return string|array The conversation ID or error array.
	 */
	public static function create_conversation( array $items = [] ) {
		if ( ! self::has_api_key() ) {
			return [
				'error' => sprintf(
					/* translators: %s: AI provider display name */
					__( '%s API key is not configured.', 'antimanual' ),
					self::get_display_name()
				),
			];
		}

		return self::get()->create_conversation( $items );
	}
}
