<?php

namespace WPDRMS\ASP\AI;

use WPDRMS\ASP\AI\ORM\AIInteraction;
use WPDRMS\ASP\Misc\PluginLicense;
use WPDRMS\PluginCore\Traits\SingletonTrait;

if ( !defined( 'ABSPATH' ) ) {
	die( '-1' );
}

class AIService {
	use SingletonTrait;

	/**
	 * Sends a one-shot prompt to the gateway /generate endpoint.
	 * Checks the WordPress transient cache before hitting the network.
	 * Writes an AIInteraction log row on every call.
	 *
	 * @param string $feature   Slug identifying the feature (e.g. 'synonym_discover').
	 * @param string $agent_id  Agent identifier in the gateway.
	 * @param string $prompt    Full prompt text.
	 * @param int    $ttl       Transient TTL in seconds (default 1 hour).
	 * @param bool   $use_cache Whether this method owns the result cache. Pass false when the
	 *                          caller already caches the shaped payload (e.g.
	 *                          AbstractCachedAIFeatureService::runCached) to avoid a redundant
	 *                          second transient layer and a misleading "cached" interaction log.
	 * @return array{result: string, tokens_used: int, cached: bool}
	 * @throws \RuntimeException When no license key is available or the gateway returns an error.
	 */
	public function generate( string $feature, string $agent_id, string $prompt, int $ttl = 3600, bool $use_cache = true ): array {
		$license_key = $this->requireLicenseKey();

		$cache_key = '';
		if ( $use_cache ) {
			$gen        = (int) get_option( 'asp_ai_cache_gen', 1 );
			$cache_key  = 'asp_ai_' . $gen . '_' . md5( $agent_id . $prompt );
			$cached_val = get_transient( $cache_key );
			if ( $cached_val !== false && is_array( $cached_val ) ) {
				$this->logInteraction( $feature, $prompt, 0, true );
				return array_merge( $cached_val, array( 'cached' => true ) );
			}
		}

		$data = $this->post(
			'/generate',
			array(
				'license_key' => $license_key,
				'agent_id'    => $agent_id,
				'prompt'      => $prompt,
			),
			30
		);

		if ( $use_cache ) {
			set_transient( $cache_key, array( 'result' => $data['result'] ), $ttl );
		}
		$this->logInteraction( $feature, $prompt, (int) ( $data['tokens_used'] ?? 0 ), false );

		return array(
			'result'      => $data['result'],
			'tokens_used' => (int) ( $data['tokens_used'] ?? 0 ),
			'cached'      => false,
		);
	}

	/**
	 * Opens a new multi-turn conversation with the gateway.
	 *
	 * @param string $agent_id
	 * @return string The conversation_id returned by the gateway.
	 * @throws \RuntimeException
	 */
	public function startConversation( string $agent_id ): string {
		$data = $this->post(
			'/conversation',
			array(
				'license_key' => $this->requireLicenseKey(),
				'agent_id'    => $agent_id,
			),
			15
		);
		return (string) ( $data['conversation_id'] ?? '' );
	}

	/**
	 * Sends a message to an existing conversation and returns the AI reply.
	 *
	 * @param string $feature         Feature slug for the interaction log.
	 * @param string $conversation_id Conversation ID returned by startConversation().
	 * @param string $message         User message.
	 * @return array{reply: string, tokens_used: int}
	 * @throws \RuntimeException
	 */
	public function sendMessage( string $feature, string $conversation_id, string $message ): array {
		$data = $this->post(
			'/conversation/' . rawurlencode( $conversation_id ) . '/message',
			array(
				'license_key' => $this->requireLicenseKey(),
				'message'     => $message,
			),
			30
		);
		$this->logInteraction( $feature, $message, (int) ( $data['tokens_used'] ?? 0 ), false );

		return array(
			'reply'       => (string) ( $data['reply'] ?? '' ),
			'tokens_used' => (int) ( $data['tokens_used'] ?? 0 ),
		);
	}

	/**
	 * Returns current token usage for the active license key from the gateway.
	 *
	 * @return array<string, mixed>
	 * @throws \RuntimeException
	 */
	public function getUsage(): array {
		$response = wp_remote_get(
			ASP_AI_GATEWAY_URL . '/usage/' . rawurlencode( $this->requireLicenseKey() ),
			array( 'timeout' => 15 )
		);

		return $this->parseResponse( $response );
	}

	/**
	 * Extracts the JSON payload from a gateway response, stripping a leading/trailing
	 * markdown code fence (```json … ```) when present. The gateway sometimes wraps
	 * JSON in fences; this is the single source of truth for unwrapping it.
	 *
	 * @param string $raw Raw AI result text.
	 * @return string Unwrapped, trimmed payload ready for json_decode().
	 */
	public static function extractJsonPayload( string $raw ): string {
		if ( preg_match( '/```(?:json)?\s*([\s\S]+?)\s*```/s', $raw, $m ) ) {
			return trim( (string) $m[1] );
		}
		return trim( $raw );
	}

	/**
	 * Returns the active license key or throws when none is available.
	 *
	 * @return string
	 * @throws \RuntimeException When no license key is available.
	 */
	private function requireLicenseKey(): string {
		$license_key = $this->getLicenseKey();
		if ( $license_key === false ) {
			throw new \RuntimeException( 'No active license key found.' );
		}
		return $license_key;
	}

	/**
	 * POSTs a JSON payload to a gateway endpoint and returns the parsed response.
	 *
	 * @param string               $endpoint Path appended to ASP_AI_GATEWAY_URL (e.g. '/generate').
	 * @param array<string, mixed> $payload  Request body; must include 'license_key'.
	 * @param int                  $timeout  Request timeout in seconds.
	 * @return array<string, mixed>
	 * @throws \RuntimeException When the gateway returns an error.
	 */
	private function post( string $endpoint, array $payload, int $timeout = 30 ): array {
		$response = wp_remote_post(
			ASP_AI_GATEWAY_URL . $endpoint,
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( $payload ),
				'timeout' => $timeout,
			)
		);

		return $this->parseResponse( $response );
	}

	/**
	 * @return string|false
	 */
	private function getLicenseKey() {
		return PluginLicense::isActivated();
	}

	/**
	 * @param array<string, mixed>|\WP_Error $response
	 * @return array<string, mixed>
	 * @throws \RuntimeException
	 */
	private function parseResponse( $response ): array {
		if ( is_wp_error( $response ) ) {
			$this->logError( 'Gateway request failed: ' . $response->get_error_message() );
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new \RuntimeException( __( 'Could not reach the AI service. Please try again.', 'ajax-search-pro' ) );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( !is_array( $data ) ) {
			$this->logError( "Gateway returned invalid JSON (HTTP $code): $body" );
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new \RuntimeException( __( 'The AI service returned an unexpected response. Please try again.', 'ajax-search-pro' ) );
		}

		if ( $code >= 400 ) {
			$gateway_msg = (string) ( $data['error'] ?? '' );
			$this->logError( "Gateway error (HTTP $code): " . ( $gateway_msg !== '' ? $gateway_msg : 'unknown' ) );

			// 4xx responses from our own gateway carry a controlled, user-safe message
			// (e.g. "Monthly token limit reached") that is worth showing. 5xx and any
			// detail-free responses are masked to avoid leaking internals into the UI.
			if ( $code < 500 && $gateway_msg !== '' ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
				throw new \RuntimeException( $gateway_msg );
			}
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new \RuntimeException( __( 'The AI service is temporarily unavailable. Please try again later.', 'ajax-search-pro' ) );
		}

		return $data;
	}

	/**
	 * Logs an internal AI error server-side (only when WP_DEBUG is on) so gateway
	 * internals never surface in the admin UI but remain diagnosable in dev/logs.
	 *
	 * @param string $message Detailed internal error message.
	 */
	private function logError( string $message ): void {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log( '[ASP AI] ' . $message );
		}
	}

	private function logInteraction( string $feature, string $prompt, int $tokens_used, bool $cached ): void {
		$row                 = new AIInteraction();
		$row->feature        = substr( $feature, 0, 50 );
		$row->prompt_summary = substr( $prompt, 0, 120 );
		$row->tokens_used    = $tokens_used;
		$row->cached         = $cached ? 1 : 0;
		$row->save();
	}
}
