<?php
/**
 * Truebe provider: the one place that talks to an AI provider (Claude or ChatGPT).
 *
 * Generic on purpose. Callers hand it a prompt and, optionally, a base64 PDF to attach.
 * It reads the owner's provider choice and key from settings, builds the right request
 * for that provider, and returns the model's raw text (or a WP_Error). It knows nothing
 * about seeds or questions, so both the documents pipeline and the query path can reuse it.
 *
 * The call always runs from the owner's own server to the owner's own provider on the
 * owner's own key. Truebe is never in the data path.
 *
 * On WordPress 7.0+, if no Truebe key has been entered but a provider is already
 * connected via Settings > Connectors, the call routes through WordPress's own
 * wp_ai_client_prompt() instead. If the owner has entered a Truebe key, that always
 * takes priority regardless of Connectors state.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'Truebe_Provider' ) ) {
	return;
}

class Truebe_Provider {

	/**
	 * Send a prompt (and optional PDF) to the configured provider, return raw text.
	 *
	 * @param string      $prompt  The instruction for the model.
	 * @param string|null $pdf_b64 Base64-encoded PDF to attach, or null for text-only.
	 * @param int         $max_tokens Response ceiling (Anthropic requires it; OpenAI ignores).
	 * @return string|WP_Error Model text, or WP_Error on any failure.
	 */
	public static function complete( $prompt, $pdf_b64 = null, $max_tokens = 32000 ) {
		$has_own_key = ( class_exists( 'Truebe_Setup' ) && '' !== Truebe_Setup::get_secret( 'truebe_ai_key' ) );
		$wp_provider = ( ! $has_own_key && class_exists( 'Truebe_Setup' ) ) ? Truebe_Setup::detect_wp_ai_provider() : null;

		if ( $wp_provider ) {
			return self::complete_via_wp_ai_client( $prompt, $pdf_b64, $wp_provider );
		}

		$provider = get_option( 'truebe_ai_provider', 'anthropic' );
		$key      = class_exists( 'Truebe_Setup' ) ? Truebe_Setup::get_secret( 'truebe_ai_key' ) : '';

		if ( '' === $key ) {
			return new WP_Error( 'no_key', __( 'No AI key is configured in Truebe settings.', 'truebe-fact-based-ai-chatbot' ) );
		}

		$model = self::model_for( $provider );

		$response = ( 'openai' === $provider )
			? self::call_openai( $key, $model, $prompt, $pdf_b64 )
			: self::call_anthropic( $key, $model, $prompt, $pdf_b64, $max_tokens );

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

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = wp_remote_retrieve_body( $response );

		if ( 200 !== $code ) {
			return new WP_Error( 'provider_http', 'Provider returned HTTP ' . $code . ': ' . substr( $body, 0, 300 ) );
		}

		$data = json_decode( $body, true );
		if ( ! is_array( $data ) ) {
			return new WP_Error( 'provider_parse', 'Provider response was not valid JSON.' );
		}

		$text = ( 'openai' === $provider )
			? ( isset( $data['choices'][0]['message']['content'] ) ? $data['choices'][0]['message']['content'] : '' )
			: ( isset( $data['content'][0]['text'] ) ? $data['content'][0]['text'] : '' );

		if ( '' === $text ) {
			return new WP_Error( 'provider_empty', 'Provider returned no content.' );
		}

		return $text;
	}

	/**
	 * Send a prompt through WordPress 7.0's native AI Client instead of a Truebe-stored key.
	 *
	 * @param string      $prompt      The instruction for the model.
	 * @param string|null $pdf_b64     Base64-encoded PDF to attach, or null for text-only.
	 * @param string      $provider_id The connector id detected as live ('anthropic' or 'openai').
	 * @return string|WP_Error Model text, or WP_Error on any failure.
	 */
	private static function complete_via_wp_ai_client( $prompt, $pdf_b64, $provider_id ) {
		if ( ! function_exists( 'wp_ai_client_prompt' ) ) {
			return new WP_Error( 'wp_ai_unavailable', __( 'The WordPress AI Client is not available on this site.', 'truebe-fact-based-ai-chatbot' ) );
		}

		$builder = wp_ai_client_prompt()->with_text( $prompt )->using_provider( $provider_id );

		if ( class_exists( '\WordPress\AiClient\Providers\Http\DTO\RequestOptions' ) ) {
			$builder = $builder->using_request_options(
				\WordPress\AiClient\Providers\Http\DTO\RequestOptions::fromArray(
					array( \WordPress\AiClient\Providers\Http\DTO\RequestOptions::KEY_TIMEOUT => 300.0 )
				)
			);
		}

		$tmp_file = null;

		if ( $pdf_b64 ) {
			$bytes = base64_decode( $pdf_b64 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- decoding our own base64-encoded PDF bytes back to a temp file, since with_file() takes a path; not obfuscation.
			if ( false !== $bytes && '' !== $bytes ) {
				if ( ! function_exists( 'wp_tempnam' ) ) {
					require_once ABSPATH . 'wp-admin/includes/file.php';
				}
				$tmp_file = wp_tempnam( 'truebe-wpai' );
				if ( $tmp_file ) {
					file_put_contents( $tmp_file, $bytes ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- writing provider-bound bytes to a private temp file so with_file() can read them; deleted immediately after the call.
					$builder = $builder->with_file( $tmp_file, 'application/pdf' );
				}
			}
		}

		$text = $builder->generate_text();

		if ( $tmp_file && file_exists( $tmp_file ) ) {
			wp_delete_file( $tmp_file );
		}

		if ( is_wp_error( $text ) ) {
			return $text;
		}
		if ( '' === (string) $text ) {
			return new WP_Error( 'provider_empty', 'Provider returned no content.' );
		}

		return $text;
	}

	/**
	 * Build and send the OpenAI chat-completions request.
	 *
	 * @return array|WP_Error wp_remote_post result.
	 */
	/**
	 * Direct OpenAI integration by design: this is Truebe's Path A fallback for
	 * sites on WordPress < 7.0, or 7.0+ sites with no provider connected via
	 * Settings > Connectors. See complete() above, which routes to
	 * complete_via_wp_ai_client() first whenever a WP AI Client provider is
	 * actually usable. This direct call only runs when that path is unavailable.
	 */
	private static function call_openai( $key, $model, $prompt, $pdf_b64 ) {		$content = array( array( 'type' => 'text', 'text' => $prompt ) );
		if ( $pdf_b64 ) {
			$content[] = array(
				'type' => 'file',
				'file' => array(
					'filename'  => 'document.pdf',
					'file_data' => 'data:application/pdf;base64,' . $pdf_b64,
				),
			);
		}

		return wp_remote_post(
			'https://api.openai.com/v1/chat/completions',
			array(
				'timeout' => 300,
				'headers' => array(
					'Authorization' => 'Bearer ' . $key,
					'Content-Type'  => 'application/json',
				),
				'body'    => wp_json_encode(
					array(
						'model'           => $model,
						'response_format' => array( 'type' => 'json_object' ),
						'messages'        => array(
							array( 'role' => 'user', 'content' => $content ),
						),
					)
				),
			)
		);
	}

	/**
	 * Build and send the Anthropic messages request.
	 *
	 * @return array|WP_Error wp_remote_post result.
	 */
	/**
	 * Direct Anthropic integration by design: same Path A fallback rationale as
	 * call_openai() above. Intentional, not a candidate for wp_ai_client_prompt().
	 */
	private static function call_anthropic( $key, $model, $prompt, $pdf_b64, $max_tokens ) {
		$content = array();
		if ( $pdf_b64 ) {
			$content[] = array(
				'type'   => 'document',
				'source' => array(
					'type'       => 'base64',
					'media_type' => 'application/pdf',
					'data'       => $pdf_b64,
				),
			);
		}
		$content[] = array( 'type' => 'text', 'text' => $prompt );

		return wp_remote_post(
			'https://api.anthropic.com/v1/messages',
			array(
				'timeout' => 300,
				'headers' => array(
					'x-api-key'         => $key,
					'anthropic-version' => '2023-06-01',
					'Content-Type'      => 'application/json',
				),
				'body'    => wp_json_encode(
					array(
						'model'      => $model,
						'max_tokens' => (int) $max_tokens,
						'messages'   => array(
							array( 'role' => 'user', 'content' => $content ),
						),
					)
				),
			)
		);
	}

	/**
	 * Default model per provider, filterable so the owner can match their key's access.
	 *
	 * @param string $provider
	 * @return string
	 */
	private static function model_for( $provider ) {
		// Cheap, capable defaults for document extraction. Power users override via truebe_model.
		$default = ( 'openai' === $provider ) ? 'gpt-4o-mini' : 'claude-haiku-4-5';
		return (string) apply_filters( 'truebe_model', $default, $provider );
	}
}