<?php
/**
 * This file is part of the Wisewand WordPress plugin.
 *
 * @package Wisewand
 * @since 1.0.0
 * @license GPL-2.0-or-later
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

/**
 * OpenAI API Client
 *
 * Wrapper for OpenAI API with error handling and rate limiting
 *
 * @package Wisewand
 * @subpackage Utils
 * @version 1.0.0
 */

namespace Wisewand\Utils;

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


use Wisewand\Logging\Logger;

// phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_error_log -- error_log() is only used within WP_DEBUG conditionals for debugging

class OpenAI_Client {

	private $api_key;
	private $logger;
	private $model    = 'gpt-4o-mini'; // Latest OpenAI model (as of 2025-01)
	private $base_url = 'https://api.openai.com/v1';

	public function __construct( $api_key, Logger $logger ) {
		$this->api_key = $api_key;
		$this->logger  = $logger;
	}

	/**
	 * Get stored OpenAI API key
	 *
	 * @return string|null API key or null if not configured
	 */
	public static function get_stored_api_key() {
		return get_option( 'wisewand_openai_api_key', '' );
	}

	/**
	 * Check if OpenAI is configured
	 *
	 * @return bool True if API key is set
	 */
	public static function is_configured() {
		$api_key = self::get_stored_api_key();
		return ! empty( $api_key ) && strlen( $api_key ) > 20;
	}

	/**
	 * Send chat completion request to OpenAI
	 *
	 * @param array $messages Array of message objects
	 * @param array $options Additional options (temperature, max_tokens, etc.)
	 * @return array|WP_Error Response data or error
	 */
	public function chat_completion( $messages, $options = array() ) {
		$defaults = array(
			'model'       => $this->model,
			'temperature' => 0.3, // Low temperature for consistent, focused responses
			'max_tokens'  => 2000,
		);

		// If no custom response_format provided, use strict JSON mode with structured output
		if ( ! isset( $options['response_format'] ) ) {
			// Use structured output to enforce AI instruction fields
			// This prevents missing 'matches' key and other field omissions
			$defaults['response_format'] = array(
				'type'        => 'json_schema',
				'json_schema' => array(
					'name'   => 'ai_instructions',
					'strict' => true,
					'schema' => array(
						'type'                 => 'object',
						'properties'           => array(
							'search_intention'        => array( 'type' => 'string' ),
							'additional_instructions' => array( 'type' => 'string' ),
							'avoid_information'       => array( 'type' => 'string' ),
							'confidence'              => array( 'type' => 'number' ),
							'matches'                 => array(
								'type'  => 'array',
								'items' => array( 'type' => 'string' ),
							),
						),
						'required'             => array( 'search_intention', 'additional_instructions', 'avoid_information', 'confidence', 'matches' ),
						'additionalProperties' => false,
					),
				),
			);
		}

		$params = array_merge(
			$defaults,
			$options,
			array(
				'messages' => $messages,
			)
		);

		$response = $this->make_request( 'chat/completions', $params );

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

		// Extract content from response
		if ( isset( $response['choices'][0]['message']['content'] ) ) {
			$content = $response['choices'][0]['message']['content'];

			$this->logger->debug(
				'OpenAI response content received',
				array(
					'content_length'  => strlen( $content ),
					'content_preview' => substr( $content, 0, 200 ),
				)
			);

			// Parse JSON response
			$decoded = json_decode( $content, true );

			if ( json_last_error() !== JSON_ERROR_NONE ) {
				$this->logger->error(
					'OpenAI returned invalid JSON',
					array(
						'content' => substr( $content, 0, 500 ),
						'error'   => json_last_error_msg(),
					)
				);

				return new \WP_Error( 'invalid_json', 'OpenAI returned invalid JSON: ' . json_last_error_msg() );
			}

			// Validate and ensure all expected fields exist
			// This prevents downstream errors when code expects specific keys
			$required_fields = array( 'search_intention', 'additional_instructions', 'avoid_information', 'confidence', 'matches' );
			$missing_fields  = array();

			foreach ( $required_fields as $field ) {
				if ( ! isset( $decoded[ $field ] ) ) {
					$missing_fields[] = $field;

					// Provide safe defaults for missing fields
					switch ( $field ) {
						case 'matches':
							$decoded[ $field ] = array(); // Empty array for matches
							break;
						case 'confidence':
							$decoded[ $field ] = 0.5; // Neutral confidence
							break;
						default:
							$decoded[ $field ] = ''; // Empty string for text fields
							break;
					}
				}
			}

			// Log missing fields warning
			if ( ! empty( $missing_fields ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log(
					sprintf(
						'OpenAI response missing fields (defaults applied): %s',
						implode( ', ', $missing_fields )
					)
				);
			}

			$this->logger->debug(
				'OpenAI JSON parsed successfully',
				array(
					'decoded_keys'   => array_keys( $decoded ),
					'missing_fields' => $missing_fields,
					'structure'      => json_encode( $decoded, JSON_PRETTY_PRINT ),
				)
			);

			// Direct error_log to show structure
			if ( WP_DEBUG ) {
				error_log( 'OpenAI Parsed Response Keys: ' . implode( ', ', array_keys( $decoded ) ) );
				if ( isset( $decoded['matches'] ) ) {
					error_log( 'OpenAI Matches Count: ' . count( $decoded['matches'] ) );
				}
			}

			return $decoded;
		}

		if ( WP_DEBUG ) {
			error_log( 'OpenAI Response has NO CONTENT in choices[0].message.content' );
		}

		$this->logger->error(
			'No content in OpenAI response',
			array(
				'response_structure' => array_keys( $response ),
				'choices_count'      => count( $response['choices'] ?? array() ),
			)
		);

		return new \WP_Error( 'no_content', 'No content in OpenAI response' );
	}

	/**
	 * Make HTTP request to OpenAI API
	 *
	 * @param string $endpoint API endpoint (e.g., 'chat/completions')
	 * @param array  $data Request data
	 * @return array|WP_Error Response or error
	 */
	private function make_request( $endpoint, $data ) {
		$url = $this->base_url . '/' . $endpoint;

		$args = array(
			'method'  => 'POST',
			'timeout' => 60,
			'headers' => array(
				'Authorization' => 'Bearer ' . $this->api_key,
				'Content-Type'  => 'application/json',
			),
			'body'    => wp_json_encode( $data ),
		);

		$this->logger->debug(
			'OpenAI API Request',
			array(
				'endpoint'       => $endpoint,
				'model'          => $data['model'] ?? 'unknown',
				'messages_count' => count( $data['messages'] ?? array() ),
			)
		);

		// Direct error_log for debugging
		if ( WP_DEBUG ) {
			$temp_log = isset( $data['temperature'] ) ? $data['temperature'] : 'N/A';
			error_log( "OpenAI API Call: {$endpoint}, Model: {$data['model']}, Temperature: {$temp_log}" );
		}

		$response = wp_remote_post( $url, $args );

		if ( is_wp_error( $response ) ) {
			$this->logger->error(
				'OpenAI API request failed',
				array(
					'error' => $response->get_error_message(),
				)
			);
			return $response;
		}

		$status_code = wp_remote_retrieve_response_code( $response );
		$body        = wp_remote_retrieve_body( $response );

		// Direct error_log for debugging
		if ( WP_DEBUG ) {
			error_log( "OpenAI Response Status: {$status_code}, Body length: " . strlen( $body ) );
		}

		if ( $status_code !== 200 ) {
			$error_data    = json_decode( $body, true );
			$error_message = $error_data['error']['message'] ?? 'Unknown OpenAI error';

			// Direct error_log
			if ( WP_DEBUG ) {
				error_log( "OpenAI API ERROR: Status {$status_code}, Message: {$error_message}" );
			}

			$this->logger->error(
				'OpenAI API error',
				array(
					'status'   => $status_code,
					'error'    => $error_message,
					'response' => $body,
				)
			);

			return new \WP_Error( 'openai_error', $error_message, array( 'status' => $status_code ) );
		}

		$decoded = json_decode( $body, true );

		if ( json_last_error() !== JSON_ERROR_NONE ) {
			$this->logger->error(
				'Failed to decode OpenAI response',
				array(
					'error' => json_last_error_msg(),
					'body'  => substr( $body, 0, 500 ),
				)
			);

			return new \WP_Error( 'decode_error', 'Failed to decode OpenAI response' );
		}

		// Log usage
		if ( isset( $decoded['usage'] ) ) {
			$this->logger->info( 'OpenAI API usage', $decoded['usage'] );
		}

		return $decoded;
	}

	/**
	 * Validate API key by making a test request
	 *
	 * @param string $api_key API key to validate
	 * @return bool|WP_Error True if valid, WP_Error if invalid
	 */
	public static function validate_api_key( $api_key ) {
		$temp_client = new self( $api_key, new Logger() );

		$test_response = $temp_client->chat_completion(
			array(
				array(
					'role'    => 'user',
					'content' => 'Test connection. Respond with JSON: {"status": "ok"}',
				),
			),
			array(
				'max_tokens' => 20,
			)
		);

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

		return true;
	}

	/**
	 * Estimate cost for a request
	 *
	 * @param int $input_tokens Estimated input tokens
	 * @param int $output_tokens Estimated output tokens
	 * @return float Estimated cost in USD
	 */
	public static function estimate_cost( $input_tokens, $output_tokens ) {
		// GPT-4o-mini pricing (as of 2025)
		$input_cost_per_1m  = 0.15;  // $0.150 per 1M input tokens
		$output_cost_per_1m = 0.60; // $0.600 per 1M output tokens

		$input_cost  = ( $input_tokens / 1000000 ) * $input_cost_per_1m;
		$output_cost = ( $output_tokens / 1000000 ) * $output_cost_per_1m;

		return round( $input_cost + $output_cost, 6 );
	}

	/**
	 * Generate image metadata (alt text, caption, description) using AI
	 *
	 * Uses article context to generate SEO-friendly, accessible image metadata.
	 * Essential for accessibility (screen readers) and SEO (image search).
	 *
	 * @param string $article_title Article title for context
	 * @param string $article_content Full article content for context
	 * @param string $image_url Image URL (optional, for context)
	 * @param string $image_position Position in article: 'featured', 'inline', 'gallery'
	 * @param int    $image_index For inline images: which number (1, 2, 3...)
	 * @return array|WP_Error Array with alt, title, caption, description or error
	 */
	public function generate_image_metadata( $article_title, $article_content, $image_url = '', $image_position = 'featured', $image_index = 1 ) {
		// Truncate content to avoid excessive token usage (use first 2000 chars)
		$content_excerpt = substr( wp_strip_all_tags( $article_content ), 0, 2000 );

		// Build context-aware prompt
		$prompt = $this->build_image_metadata_prompt(
			$article_title,
			$content_excerpt,
			$image_url,
			$image_position,
			$image_index
		);

		if ( WP_DEBUG ) {
			error_log( 'OpenAI Image Metadata: Generating for ' . $image_position . ' image #' . $image_index );
		}

		$messages = array(
			array(
				'role'    => 'system',
				'content' => 'You are an SEO and accessibility expert specializing in image optimization for WordPress websites. Generate concise, descriptive, keyword-rich metadata that helps both search engines and screen readers understand image content.',
			),
			array(
				'role'    => 'user',
				'content' => $prompt,
			),
		);

		$response = $this->chat_completion(
			$messages,
			array(
				'max_tokens'  => 300, // Metadata is concise
				'temperature' => 0.3, // Consistent, factual descriptions
			)
		);

		if ( is_wp_error( $response ) ) {
			$this->logger->error(
				'Failed to generate image metadata',
				array(
					'error'    => $response->get_error_message(),
					'position' => $image_position,
				)
			);
			return $response;
		}

		// Validate response structure
		if ( ! isset( $response['alt'] ) || empty( $response['alt'] ) ) {
			if ( WP_DEBUG ) {
				error_log( 'OpenAI Image Metadata: Response missing alt text, using fallback' );
			}
			return new \WP_Error( 'missing_alt', 'OpenAI response missing alt text' );
		}

		$metadata = array(
			'alt'         => sanitize_text_field( $response['alt'] ),
			'title'       => sanitize_text_field( $response['title'] ?? $response['alt'] ),
			'caption'     => sanitize_textarea_field( $response['caption'] ?? '' ),
			'description' => sanitize_textarea_field( $response['description'] ?? '' ),
		);

		if ( WP_DEBUG ) {
			error_log( 'OpenAI Image Metadata Generated:' );
			error_log( '  Alt: ' . $metadata['alt'] );
			error_log( '  Title: ' . $metadata['title'] );
			error_log( '  Caption: ' . substr( $metadata['caption'], 0, 100 ) );
		}

		$this->logger->info(
			'Generated image metadata via OpenAI',
			array(
				'position'    => $image_position,
				'alt_length'  => strlen( $metadata['alt'] ),
				'has_caption' => ! empty( $metadata['caption'] ),
			)
		);

		return $metadata;
	}

	/**
	 * Build prompt for image metadata generation
	 *
	 * @param string $article_title Article title
	 * @param string $content_excerpt Content excerpt for context
	 * @param string $image_url Image URL
	 * @param string $image_position Position: featured, inline, gallery
	 * @param int    $image_index Image number
	 * @return string Formatted prompt
	 */
	private function build_image_metadata_prompt( $article_title, $content_excerpt, $image_url, $image_position, $image_index ) {
		$position_context = array(
			'featured' => 'This is the featured image (main header image) for the article.',
			'inline'   => "This is inline image #{$image_index} within the article content.",
			'gallery'  => "This is image #{$image_index} in the article's image gallery.",
		);

		$context = $position_context[ $image_position ] ?? 'This is an image associated with the article.';

		$prompt = 'Generate SEO-optimized and accessibility-friendly metadata for an image in a WordPress article.' . "\n\n" .
			'ARTICLE CONTEXT:' . "\n" .
			'Title: ' . $article_title . "\n\n" .
			'Content Preview:' . "\n" .
			$content_excerpt . "\n\n" .
			'IMAGE CONTEXT:' . "\n" .
			$context . "\n\n" .
			'REQUIREMENTS:' . "\n" .
			'1. Alt Text: 50-125 characters, descriptive for screen readers, includes relevant keywords naturally' . "\n" .
			'2. Title: 30-60 characters, concise image title for hover tooltips' . "\n" .
			'3. Caption: 100-200 characters, engaging text displayed below image (optional but recommended)' . "\n" .
			'4. Description: 150-300 characters, detailed image description for media library' . "\n\n" .
			'RESPOND WITH JSON:' . "\n" .
			'{' . "\n" .
			'    "alt": "descriptive alt text here",' . "\n" .
			'    "title": "short image title",' . "\n" .
			'    "caption": "engaging caption for readers",' . "\n" .
			'    "description": "detailed description for SEO and media library"' . "\n" .
			'}' . "\n\n" .
			'Focus on: Accessibility, SEO value, natural keyword usage, conciseness.';

		return $prompt;
	}
	/**
	 * Generate embeddings for text using OpenAI
	 *
	 * @param string|array $input Text or array of texts to embed
	 * @return array|WP_Error Array of embeddings or error
	 */
	public function get_embeddings( $input ) {
		// Ensure input is array for consistent handling
		$is_batch = is_array( $input );
		$texts    = $is_batch ? $input : array( $input );

		// Clean and truncate inputs
		$cleaned_texts = array_map(
			function ( $text ) {
				// Remove newlines and excessive whitespace which can affect embeddings
				$text = str_replace( array( "\r", "\n" ), ' ', $text );
				$text = preg_replace( '/\s+/', ' ', $text );
				return trim( substr( $text, 0, 8000 ) ); // Truncate to safe length
			},
			$texts
		);

		$params = array(
			'model'           => 'text-embedding-3-small', // Fast, cheap, high performance
			'input'           => $is_batch ? $cleaned_texts : $cleaned_texts[0],
			'encoding_format' => 'float',
			'dimensions'      => 256, // Optimization: Reduce vector size for performance (10k keywords = 10MB RAM instead of 60MB)
		);

		$response = $this->make_request( 'embeddings', $params );

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

		if ( ! isset( $response['data'] ) || empty( $response['data'] ) ) {
			return new \WP_Error( 'no_embeddings', 'No embeddings returned from OpenAI' );
		}

		// Extract embeddings
		$embeddings = array();
		foreach ( $response['data'] as $item ) {
			$index                = $item['index'];
			$embeddings[ $index ] = $item['embedding'];
		}

		// Sort by index to maintain order
		ksort( $embeddings );

		// Return single embedding if input was string, otherwise array
		return $is_batch ? $embeddings : $embeddings[0];
	}
}
