<?php
/**
 * Claude API Client
 *
 * Implements AIClientInterface for Anthropic's Claude API.
 *
 * @package AIPT\Api\Claude
 * @since 2.4.0
 */

namespace AIPT\Api\Claude;

use AIPT\Api\AIClientInterface;
use AIPT\Api\Chatbot\Jobs\IdempotencyKeyContext;
use AIPT\Api\Helpers\VisionImageEncoder;
use AIPT\Core\ErrorHandler;
use AIPT\Utils\PromptFormatter;

/**
 * Claude API client implementation.
 */
class ClaudeClient implements AIClientInterface {

    /**
     * Claude API base URL.
     *
     * @var string
     */
    private $api_url = 'https://api.anthropic.com/v1/messages';

    /**
     * API key for authentication.
     *
     * @var string
     */
    private $api_key;

    /**
     * Available Claude models with configurations.
     *
     * @var array
     */
    private $models = [];

    /**
     * Anthropic API version header.
     *
     * @var string
     */
    private $anthropic_version = '2023-06-01';

    private ?IdempotencyKeyContext $idempotency_context;

    /**
     * Constructor.
     *
     * @param string $api_key Claude API key.
     */
    public function __construct( string $api_key, ?IdempotencyKeyContext $idempotency_context = null ) {
        $this->api_key = $api_key;
        $this->idempotency_context = $idempotency_context;
        $this->initializeModels();
    }

    /**
     * Initialize available Claude models with their configurations.
     *
     * @return void
     */
    private function initializeModels(): void {
        $this->models = [

            'claude-opus-4-8' => [
                'name'                 => 'Claude Opus 4.8',
                'max_tokens'           => 8192,
                'temperature'          => 0.7,
                'has_temperature'      => false,
                'supports_system'      => true,
            ],

            'claude-opus-4-7' => [
                'name'                 => 'Claude Opus 4.7',
                'max_tokens'           => 8192,
                'temperature'          => 0.7,
                'has_temperature'      => false,
                'supports_system'      => true,
            ],

            'claude-opus-4-6' => [
                'name'                 => 'Claude Opus 4.6',
                'max_tokens'           => 8192,
                'temperature'          => 0.7,
                'has_temperature'      => true,
                'supports_system'      => true,
            ],
            'claude-sonnet-4-6' => [
                'name'                 => 'Claude Sonnet 4.6',
                'max_tokens'           => 8192,
                'temperature'          => 0.7,
                'has_temperature'      => true,
                'supports_system'      => true,
            ],

            'claude-opus-4-5-20251101' => [
                'name'                 => 'Claude Opus 4.5',
                'max_tokens'           => 8192,
                'temperature'          => 0.7,
                'has_temperature'      => true,
                'supports_system'      => true,
            ],
            'claude-sonnet-4-5-20250929' => [
                'name'                 => 'Claude Sonnet 4.5',
                'max_tokens'           => 8192,
                'temperature'          => 0.7,
                'has_temperature'      => true,
                'supports_system'      => true,
            ],
            'claude-haiku-4-5-20251001' => [
                'name'                 => 'Claude Haiku 4.5',
                'max_tokens'           => 8192,
                'temperature'          => 0.7,
                'has_temperature'      => true,
                'supports_system'      => true,
            ],

            'claude-opus-4-1-20250805' => [
                'name'                 => 'Claude Opus 4.1',
                'max_tokens'           => 8192,
                'temperature'          => 0.7,
                'has_temperature'      => true,
                'supports_system'      => true,
            ],

        ];
    }

    /**
     * Set parameters for all models.
     *
     * @param array $parameters Parameters to set.
     * @return void
     */
    public function setParameters( array $parameters ): void {
        if ( empty( $parameters ) ) {
            return;
        }

        try {

            foreach ( $this->models as $model_id => &$model_config ) {

                if ( isset( $parameters['temperature'] ) && is_numeric( $parameters['temperature'] ) ) {
                    $temperature = floatval( $parameters['temperature'] );

                    if ( $temperature >= 0.0 && $temperature <= 1.0 ) {
                        $model_config['temperature'] = $temperature;
                    }
                }

                if ( isset( $parameters['top_p'] ) && is_numeric( $parameters['top_p'] ) ) {
                    $top_p = floatval( $parameters['top_p'] );

                    if ( $top_p >= 0.0 && $top_p <= 1.0 ) {
                        $model_config['top_p'] = $top_p;
                    }
                }
            }

            unset( $model_config );
        } catch ( \Exception $e ) {

        } catch ( \Error $e ) {

        }
    }

    /**
     * Get model configuration.
     *
     * @param string $model Model identifier.
     * @return array Model configuration or default.
     */
    private function getModelConfig( string $model ): array {
        return $this->models[ $model ] ?? [
            'name'            => $model,
            'max_tokens'      => 4096,
            'temperature'     => 0.7,
            'has_temperature' => true,
            'supports_system' => true,
        ];
    }

    /**
     * Sanitize prompt content.
     *
     * @param string $content Content to sanitize.
     * @return string Sanitized content.
     */
    private function sanitize_prompt_content( string $content ): string {
        return PromptFormatter::cleanContent( $content );
    }

    /**
     * Generate description content using Claude API.
     *
     * @param string   $system_prompt   System instructions.
     * @param string   $user_prompt     User message/prompt.
     * @param string   $model           Model to use.
     * @param int|null $maximum_length  Maximum response length.
     * @return string Generated content or error message.
     */
    public function generate_description( string $system_prompt, string $user_prompt, string $model = '', ?int $maximum_length = null ): string {
        if ( empty( $model ) ) {
            $model = 'claude-sonnet-4-6';
        }

        $model_config = $this->getModelConfig( $model );

        $body = [
            'model'      => $model,
            'max_tokens' => $maximum_length ?? $model_config['max_tokens'],
            'messages'   => [
                [
                    'role'    => 'user',
                    'content' => $this->sanitize_prompt_content( $user_prompt ),
                ],
            ],
        ];

        if ( ! empty( $system_prompt ) && $model_config['supports_system'] ) {
            $body['system'] = $this->sanitize_prompt_content( $system_prompt );
        }

        if ( ! empty( $model_config['has_temperature'] ) ) {
            $body['temperature'] = $model_config['temperature'];
        }

        if ( ! empty( $model_config['has_temperature'] ) && isset( $model_config['top_p'] ) && is_numeric( $model_config['top_p'] ) ) {
            $body['top_p'] = (float) $model_config['top_p'];
        }

        $response = wp_remote_post(
            $this->api_url,
            [
                'headers' => $this->withIdempotencyHeader( [
                    'Content-Type'      => 'application/json',
                    'x-api-key'         => $this->api_key,
                    'anthropic-version' => $this->anthropic_version,
                ] ),
                'body'    => $this->encodeBody( $body ),
                'timeout' => 120,
            ]
        );

        return $this->handle_response( $response, 'description', $model );
    }

    /**
     * Generate FAQ content using Claude API.
     *
     * @param string      $system_prompt System instructions.
     * @param string      $user_prompt   User message/prompt.
     * @param string|null $model         Model to use.
     * @return string Generated content or error message.
     */
    public function generate_from_vision( string $system_prompt, string $user_prompt, string $image_url, ?string $model = null ): string {
        $resolved_model = $model ?: 'claude-sonnet-4-6';

        $ai_gate = \AIPT\Core\Services\AiSupportGate::guard();
        if ( $ai_gate instanceof \WP_Error ) {
            throw new \Exception( esc_html( $ai_gate->get_error_message() ) );
        }

        try {
            $encoded_image = VisionImageEncoder::fetchAndEncode( $image_url );
        } catch ( \RuntimeException $e ) {
            $detailed_error = ErrorHandler::format_api_response_error(
                [
                    'code'    => 'aipt_vision_fetch_failed',
                    'message' => esc_html( $e->getMessage() ),
                    'type'    => 'vision_image_fetch_failed',
                ],
                'Claude',
                0
            );

            throw new \Exception(
                $detailed_error
            );

        }

        $model_config = $this->getModelConfig( $resolved_model );

        $body = [
            'model'      => $resolved_model,
            'max_tokens' => min( 150, (int) $model_config['max_tokens'] ),
            'messages'   => [
                [
                    'role'    => 'user',
                    'content' => [
                        [
                            'type'   => 'image',
                            'source' => [
                                'type'       => 'base64',
                                'media_type' => $encoded_image['mime_type'],
                                'data'       => $encoded_image['data'],
                            ],
                        ],
                        [
                            'type' => 'text',
                            'text' => $this->sanitize_prompt_content( $user_prompt ),
                        ],
                    ],
                ],
            ],
        ];

        if ( ! empty( $system_prompt ) && $model_config['supports_system'] ) {
            $body['system'] = $this->sanitize_prompt_content( $system_prompt );
        }

        if ( ! empty( $model_config['has_temperature'] ) ) {
            $body['temperature'] = (float) $model_config['temperature'];
        }

        if ( ! empty( $model_config['has_temperature'] ) && isset( $model_config['top_p'] ) && is_numeric( $model_config['top_p'] ) ) {
            $body['top_p'] = (float) $model_config['top_p'];
        }

        $response = wp_remote_post(
            $this->api_url,
            [
                'headers' => $this->withIdempotencyHeader( [
                    'Content-Type'      => 'application/json',
                    'x-api-key'         => $this->api_key,
                    'anthropic-version' => $this->anthropic_version,
                ] ),
                'body'    => $this->encodeBody( $body ),
                'timeout' => 60,
            ]
        );

        return $this->handle_response( $response, 'vision', $resolved_model );
    }

    public function generate_faq_content( string $system_prompt, string $user_prompt, ?string $model = null ): string {
        if ( empty( $model ) ) {
            $model = 'claude-sonnet-4-6';
        }

        $model_config = $this->getModelConfig( $model );

        $body = [
            'model'      => $model,
            'max_tokens' => $model_config['max_tokens'],
            'messages'   => [
                [
                    'role'    => 'user',
                    'content' => $this->sanitize_prompt_content( $user_prompt ),
                ],
            ],
        ];

        if ( ! empty( $system_prompt ) && $model_config['supports_system'] ) {
            $body['system'] = $this->sanitize_prompt_content( $system_prompt );
        }

        if ( ! empty( $model_config['has_temperature'] ) ) {
            $body['temperature'] = $model_config['temperature'];
        }

        if ( ! empty( $model_config['has_temperature'] ) && isset( $model_config['top_p'] ) && is_numeric( $model_config['top_p'] ) ) {
            $body['top_p'] = (float) $model_config['top_p'];
        }

        $response = wp_remote_post(
            $this->api_url,
            [
                'headers' => $this->withIdempotencyHeader( [
                    'Content-Type'      => 'application/json',
                    'x-api-key'         => $this->api_key,
                    'anthropic-version' => $this->anthropic_version,
                ] ),
                'body'    => $this->encodeBody( $body ),
                'timeout' => 120,
            ]
        );

        return $this->handle_response( $response, 'faq', $model );
    }

    /**
     * Encode a request body to JSON, failing loudly on invalid UTF-8.
     *
     * Mirrors the OpenRouter/OpenAI clients: wp_json_encode() returns false when
     * the payload contains invalid byte sequences. Passing that false straight
     * to wp_remote_post() would silently dispatch an empty body and surface an
     * opaque provider error instead of a clear local one.
     *
     * @param array $body Request body.
     * @return string Encoded JSON.
     * @throws \Exception When encoding fails.
     */
    private function encodeBody( array $body ): string {
        $json = wp_json_encode( $body );
        if ( false === $json ) {
            throw new \Exception(
                esc_html__( 'Failed to encode Claude request body as JSON. The product data may contain invalid characters.', 'ai-product-tools' )
            );
        }
        return $json;
    }

    /**
     * Handle API response.
     *
     * @param array|\WP_Error $response     API response.
     * @param string          $content_type Type of content being generated.
     * @param string          $model        Model identifier (for usage tracking).
     * @return string Extracted content or error message.
     */
    private function handle_response( $response, string $content_type, string $model = '' ): string {
        if ( is_wp_error( $response ) ) {

            throw new \Exception(
                ErrorHandler::format_api_response_error(
                    $response->get_error_message(),
                    'claude',
                    0
                )
            );

        }

        $response_code = (int) wp_remote_retrieve_response_code( $response );
        $body          = json_decode( (string) wp_remote_retrieve_body( $response ), true );

        if ( $response_code !== 200 ) {

            if ( isset( $body['request_id'] ) && is_string( $body['request_id'] ) ) {
                ErrorHandler::log_error(
                    sprintf( 'Claude request_id=%s status=%d', sanitize_text_field( $body['request_id'] ), $response_code ),
                    'debug'
                );
            }

            $error_payload = $this->parse_error_response( $body, $response_code );

            throw new \Exception(
                ErrorHandler::format_api_response_error(
                    $error_payload,
                    'claude',
                    $response_code
                )
            );

        }

        if ( isset( $body['content'][0]['text'] ) ) {
            \AIPT\Core\Services\UsageTracker::trackApiCall(
                'claude',
                (string) ( $body['model'] ?? $model ),
                (int) ( $body['usage']['input_tokens'] ?? 0 ),
                (int) ( $body['usage']['output_tokens'] ?? 0 )
            );
            return $body['content'][0]['text'];
        }

        throw new \Exception(
            ErrorHandler::format_api_response_error(
                __( 'Unexpected response format from Claude API', 'ai-product-tools' ),
                'claude',
                0
            )
        );

    }

    /**
     * Parse Claude error response into a structured payload.
     *
     * When Claude returns a structured error body, the full error array is returned
     * so downstream ErrorHandler can surface both type and message. Falls back to a
     * hardcoded HTTP-status-to-message map when no structured error is present.
     *
     * @param array|null $body          Response body.
     * @param int        $response_code HTTP response code.
     * @return array|string Structured error array when available, mapped string otherwise.
     */
    private function parse_error_response( ?array $body, int $response_code ) {

        if ( is_array( $body ) && isset( $body['error'] ) && is_array( $body['error'] ) && isset( $body['error']['message'] ) ) {

            $body['error']['message'] = ErrorHandler::sanitize_error_for_display( $body['error']['message'] );
            return $body['error'];
        }

        $error_messages = [
            400 => __( 'Invalid request to Claude API', 'ai-product-tools' ),
            401 => __( 'Invalid Claude API key', 'ai-product-tools' ),
            403 => __( 'Permission denied for Claude API', 'ai-product-tools' ),
            404 => __( 'Claude API endpoint not found', 'ai-product-tools' ),
            429 => __( 'Claude API rate limit exceeded. Please try again later.', 'ai-product-tools' ),
            500 => __( 'Claude API internal error', 'ai-product-tools' ),
            529 => __( 'Claude API is temporarily overloaded. Please try again later.', 'ai-product-tools' ),
        ];

        return $error_messages[ $response_code ] 
            ?? sprintf(
                /* translators: %d: HTTP status code */
                __( 'Claude API error (HTTP %d)', 'ai-product-tools' ),
                $response_code
            );
    }

    private function withIdempotencyHeader( array $headers ): array {
        if ( null === $this->idempotency_context || ! $this->isIdempotencyAllowed() ) {
            return $headers;
        }

        $key = IdempotencyKeyContext::getIdempotencyKey();

        if ( null !== $key ) {
            $headers['Idempotency-Key'] = $key;
        }

        return $headers;
    }

    private function isIdempotencyAllowed(): bool {
        $providers = \apply_filters( 'aipt_chatbot_idempotency_providers', [ 'openai', 'anthropic' ] );

        if ( ! is_array( $providers ) ) {
            return false;
        }

        $providers = array_map( '\sanitize_key', $providers );

        return in_array( 'anthropic', $providers, true );
    }

    /**
     * Validate Claude API key.
	 *
	 * @param string $api_key API key to validate.
	 * @return bool True if valid.
	 * @throws \Exception If API key is invalid or validation fails.
	 */
	public static function validate_api_key( string $api_key ): bool {
		if ( empty( $api_key ) ) {

			throw new \Exception( __( 'Claude API key cannot be empty.', 'ai-product-tools' ) );
		}

		if ( strpos( $api_key, 'sk-ant-' ) !== 0 ) {

			throw new \Exception( __( 'Invalid Claude API key format. Key should start with "sk-ant-".', 'ai-product-tools' ) );
		}

		$response = wp_remote_get(
			'https://api.anthropic.com/v1/models',
			[
				'headers' => [
					'x-api-key'         => $api_key,
					'anthropic-version' => '2023-06-01',
				],
				'timeout' => 20,
			]
		);

		if ( is_wp_error( $response ) ) {
			/** @var \WP_Error $response */

			throw new \Exception(
				sprintf(
					/* translators: %s: Error message from API request */
					__( 'Claude API request failed: %s', 'ai-product-tools' ),
					$response->get_error_message()
				)
			);

		}

		$response_code = wp_remote_retrieve_response_code( $response );
		$body_str      = (string) wp_remote_retrieve_body( $response );
		$body          = json_decode( $body_str, true );

		if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $body ) ) {

			throw new \Exception(
				sprintf(
					/* translators: %s: JSON error message */
					__( 'Invalid response from Claude API: %s', 'ai-product-tools' ),
					json_last_error_msg()
				)
			);

		}

		if ( $response_code === 200 ) {
			return true;
		}

		if ( $response_code === 401 ) {
			$error_msg = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'Invalid Claude API key', 'ai-product-tools' );

			throw new \Exception( $error_msg );
		}

		if ( $response_code === 403 ) {

			throw new \Exception( __( 'Permission denied for Claude API. Please check your API key permissions.', 'ai-product-tools' ) );
		}

		if ( $response_code === 429 ) {

			throw new \Exception( __( 'Claude API rate limit exceeded. Please try again later.', 'ai-product-tools' ) );
		}

		$error_msg = isset( $body['error']['message'] ) ? $body['error']['message'] : sprintf(
			/* translators: %d: HTTP status code */
			__( 'Claude API error (HTTP %d)', 'ai-product-tools' ),
			$response_code
		);

		throw new \Exception( $error_msg );
	}

    /**
     * Get available Claude models.
     *
     * @return array List of available models with their display names.
     */
    public function get_available_models(): array {
        $available_models = [];

        foreach ( $this->models as $model_id => $config ) {
            $available_models[ $model_id ] = $config['name'];
        }

        return $available_models;
    }
}
