<?php
/**
 * AI Client — OpenAI, Gemini, Claude, Grok, Kimi
 * Fixes: #10 nonce (caller validates), #12 sanitization, #13 rate limiting,
 *        #14 error logging, #15 timeout filter, #16 escaping
 *
 * @package EPWF\Includes
 */
namespace EPWF\Includes;
defined( 'ABSPATH' ) || exit;

class AI_Client {

    /** @var Settings */
    private $s;

    public function __construct( Settings $s ) {
        $this->s = $s;
    }

    // ── REST API: permission_callback (ready for future REST routes) ──────────

    /**
     * Standard permission_callback for any REST route added later.
     * Usage: 'permission_callback' => [ $this->ai_client, 'rest_permission' ]
     *
     * @param \WP_REST_Request $request
     * @return bool|\WP_Error
     */
    public function rest_permission( $request ) {
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            return new \WP_Error(
                'epwf_forbidden',
                __( 'You do not have permission to perform this action.', 'exertpro-faq-ai' ),
                [ 'status' => 403 ]
            );
        }
        if ( ! isset( $request['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( $request['_wpnonce'] ), 'wp_rest' ) ) {
            return new \WP_Error(
                'epwf_nonce',
                __( 'Nonce verification failed.', 'exertpro-faq-ai' ),
                [ 'status' => 403 ]
            );
        }
        return true;
    }

    // ── Public API ─────────────────────────────────────────────────────────────

    /**
     * Generate FAQs. Rate-limiting is checked before any HTTP call.
     *
     * @param string $title
     * @param string $desc
     * @param int    $count
     * @return array|\WP_Error
     */
    public function generate( $title, $desc, $count = 5 ) {
        if ( ! $this->s->has_key() ) {
            return new \WP_Error(
                'epwf_no_key',
                __( 'No API key configured for the selected model. Go to WooCommerce → ProductFAQ AI.', 'exertpro-faq-ai' )
            );
        }

        // Fix #13: Rate limiting — max EPWF_RATE_LIMIT requests/hour per user
        $rate_error = $this->check_rate_limit();
        if ( is_wp_error( $rate_error ) ) {
            return $rate_error;
        }

        $prompt   = $this->build_prompt( $title, $desc, absint( $count ) );
        $provider = $this->s->get_provider();
        $model    = $this->s->get_model();

        switch ( $provider ) {
            case 'gemini':    $raw = $this->call_gemini( $prompt, $model );    break;
            case 'anthropic': $raw = $this->call_claude( $prompt, $model );    break;
            case 'grok':      $raw = $this->call_openai_compat(
                                    'https://api.x.ai/v1/chat/completions',
                                    $this->s->get_grok_key(), $prompt, $model, 'grok' ); break;
            case 'kimi':      $raw = $this->call_openai_compat(
                                    'https://api.moonshot.cn/v1/chat/completions',
                                    $this->s->get_kimi_key(), $prompt, $model, 'kimi' ); break;
            default:          $raw = $this->call_openai_compat(
                                    'https://api.openai.com/v1/chat/completions',
                                    $this->s->get_openai_key(), $prompt, $model, 'openai' ); break;
        }

        if ( is_wp_error( $raw ) ) {
            $this->log_error( $raw->get_error_message(), $provider, $model );
            return $raw;
        }

        return $this->parse_response( $raw );
    }

    // ── Prompt ─────────────────────────────────────────────────────────────────

    private function build_prompt( $title, $desc, $count ) {
        // Sanitize inputs before sending to AI
        $title = sanitize_text_field( $title );
        // Fix #8: Use wp_trim_words() for token-cost control (char substr replaced)
        // epwf_trim_description() strips HTML and limits to EPWF_DESC_WORD_LIMIT words (filterable)
        $desc = function_exists( 'epwf_trim_description' )
            ? epwf_trim_description( $desc )
            : wp_trim_words( wp_strip_all_tags( $desc ), 300, '' );

        // Fix: Prompt injection protection — strip common attack phrases
        $injection_patterns = [
            '/ignore\s+(all\s+)?previous\s+instructions?/i',
            '/disregard\s+(all\s+)?previous\s+instructions?/i',
            '/forget\s+(all\s+)?previous\s+instructions?/i',
            '/you\s+are\s+now\s+/i',
            '/system\s*prompt/i',
            '/\\\\n\s*system\s*:/i',
            '/```[\s\S]*?```/i',       // strip markdown code blocks
            '/<\/?[a-z][\s\S]*?>/i',  // strip any HTML tags
            '/\[INST\]|\[\/INST\]/i', // LLaMA injection tokens
            '/<\|im_start\|>|<\|im_end\|>/i', // ChatML injection tokens
        ];
        foreach ( $injection_patterns as $pattern ) {
            $title = preg_replace( $pattern, '', $title );
            $desc  = preg_replace( $pattern, '', $desc );
        }
        $title = trim( $title );
        $desc  = trim( $desc );

        return sprintf(
            "Generate exactly %d FAQ items for the WooCommerce product below.\n"
            . "Return ONLY a raw JSON array — no markdown fences, no explanation.\n"
            . "Start with [ and end with ].\n\n"
            . "Product: %s\nDescription: %s\n\n"
            . 'Each item must be: {"question":"...","answer":"..."}'
            . "\nCover: usage, materials, shipping, returns, warranty, compatibility.\n"
            . "Output the JSON array now:",
            $count,
            $title,
            $desc ?: 'Not provided.'
        );
    }

    // ── OpenAI-compatible (OpenAI, Grok, Kimi) ────────────────────────────────

    private function call_openai_compat( $endpoint, $api_key, $prompt, $model, $provider_id ) {
        if ( empty( $api_key ) ) {
            return new \WP_Error( 'epwf_no_key', sprintf(
                /* translators: %s: provider name */
                __( 'API key for %s is empty.', 'exertpro-faq-ai' ), $provider_id
            ) );
        }

        $body = wp_json_encode( [
            'model'       => sanitize_text_field( $model ),
            'temperature' => 0.7,
            'max_tokens'  => 2000,
            'messages'    => [
                [ 'role' => 'system', 'content' => 'You are a WooCommerce FAQ generator. Output ONLY raw JSON arrays. No markdown, no explanation.' ],
                [ 'role' => 'user',   'content' => $prompt ],
            ],
        ] );

        if ( false === $body ) {
            return new \WP_Error( 'epwf_encode', __( 'Failed to encode request body.', 'exertpro-faq-ai' ) );
        }

        $response = wp_remote_post( esc_url_raw( $endpoint ), [
            'timeout'    => epwf_api_timeout(),
            'sslverify'  => (bool) apply_filters( 'epwf_sslverify', true ),
            'user-agent' => 'WooFAQ-AI/' . EPWF_VERSION . '; ' . home_url(),
            'headers'    => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ],
            'body'       => $body,
        ] );

        return $this->handle_oa_response( $response, $provider_id );
    }

    private function handle_oa_response( $response, $provider_id ) {
        if ( is_wp_error( $response ) ) {
            $err_msg = $response->get_error_message();
            // Classify curl timeout vs other HTTP errors
            $err_code = ( false !== stripos( $err_msg, 'timed out' ) || false !== stripos( $err_msg, 'timeout' ) )
                ? 'epwf_timeout' : 'epwf_http';
            if ( 'epwf_timeout' === $err_code ) {
                $err_msg = sprintf(
                    /* translators: 1: provider name (e.g. OPENAI), 2: timeout seconds */
                    __( '%1$s request timed out after %2$d seconds. Try increasing the timeout or check your server.', 'exertpro-faq-ai' ),
                    strtoupper( $provider_id ), epwf_api_timeout()
                );
            }
            return new \WP_Error( $err_code, $err_msg );
        }

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

        if ( $code !== 200 ) {
            $api_msg = isset( $decoded['error']['message'] )
                ? sanitize_text_field( $decoded['error']['message'] ) : '';
            // Classify into specific error codes for better UI feedback
            if ( in_array( $code, [ 401, 403 ], true ) ) {
                $err_code = 'epwf_invalid_key';
                /* translators: %s: provider name (e.g. OPENAI) */
                $msg = $api_msg ?: sprintf( __( 'Invalid API key for %s. Please check your key in Settings.', 'exertpro-faq-ai' ), strtoupper( $provider_id ) );
            } elseif ( 429 === $code ) {
                $err_code = 'epwf_quota_exceeded';
                /* translators: %s: provider name (e.g. OPENAI) */
                $msg = $api_msg ?: sprintf( __( '%s quota exceeded. Check your billing or wait before retrying.', 'exertpro-faq-ai' ), strtoupper( $provider_id ) );
            } elseif ( $code >= 500 ) {
                $err_code = 'epwf_provider_error';
                /* translators: 1: provider name (e.g. OPENAI), 2: HTTP status code */
                $msg = $api_msg ?: sprintf( __( '%1$s server error (HTTP %2$d). Please try again later.', 'exertpro-faq-ai' ), strtoupper( $provider_id ), $code );
            } else {
                $err_code = 'epwf_' . $provider_id . '_err';
                /* translators: 1: provider name (e.g. OPENAI), 2: HTTP status code */
                $msg = $api_msg ?: sprintf( __( '%1$s API returned HTTP %2$d.', 'exertpro-faq-ai' ), strtoupper( $provider_id ), $code );
            }
            return new \WP_Error( $err_code, $msg );
        }

        $content = isset( $decoded['choices'][0]['message']['content'] )
            ? trim( $decoded['choices'][0]['message']['content'] )
            : '';

        if ( '' === $content ) {
            return new \WP_Error( 'epwf_empty', sprintf(
                /* translators: %s: provider name */
                __( '%s returned an empty response.', 'exertpro-faq-ai' ), strtoupper( $provider_id )
            ) );
        }

        return $content;
    }

    // ── Google Gemini ──────────────────────────────────────────────────────────

    private function call_gemini( $prompt, $model ) {
        $key = $this->s->get_gemini_key();
        if ( empty( $key ) ) {
            return new \WP_Error( 'epwf_no_key', __( 'Gemini API key is empty.', 'exertpro-faq-ai' ) );
        }

        $url = add_query_arg(
            'key', $key,
            'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode( $model ) . ':generateContent'
        );

        $body = wp_json_encode( [
            'contents'         => [ [ 'parts' => [ [ 'text' => $prompt ] ] ] ],
            'generationConfig' => [ 'temperature' => 0.7, 'maxOutputTokens' => 2000 ],
        ] );

        $response = wp_remote_post( $url, [
            'timeout'    => epwf_api_timeout(),
            'sslverify'  => (bool) apply_filters( 'epwf_sslverify', true ),
            'user-agent' => 'WooFAQ-AI/' . EPWF_VERSION . '; ' . home_url(),
            'headers'    => [ 'Content-Type' => 'application/json' ],
            'body'       => $body,
        ] );

        if ( is_wp_error( $response ) ) return new \WP_Error( 'epwf_http', $response->get_error_message() );

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

        if ( 200 !== $code ) {
            $msg = isset( $decoded['error']['message'] )
                ? sanitize_text_field( $decoded['error']['message'] )
                /* translators: %d: HTTP status code */
                : sprintf( __( 'Gemini API returned HTTP %d.', 'exertpro-faq-ai' ), $code );
            return new \WP_Error( 'epwf_gemini_err', $msg );
        }

        $content = isset( $decoded['candidates'][0]['content']['parts'][0]['text'] )
            ? trim( $decoded['candidates'][0]['content']['parts'][0]['text'] )
            : '';

        return $content ?: new \WP_Error( 'epwf_gemini_empty', __( 'Gemini returned an empty response.', 'exertpro-faq-ai' ) );
    }

    // ── Anthropic Claude ───────────────────────────────────────────────────────

    private function call_claude( $prompt, $model ) {
        $key = $this->s->get_anthropic_key();
        if ( empty( $key ) ) {
            return new \WP_Error( 'epwf_no_key', __( 'Anthropic API key is empty.', 'exertpro-faq-ai' ) );
        }

        $model_map = [
            'claude-3-5-sonnet' => 'claude-3-5-sonnet-20241022',
            'claude-3-haiku'    => 'claude-3-haiku-20240307',
        ];
        $api_model = isset( $model_map[ $model ] ) ? $model_map[ $model ] : 'claude-3-haiku-20240307';

        $body = wp_json_encode( [
            'model'      => $api_model,
            'max_tokens' => 2000,
            'system'     => 'You are a WooCommerce FAQ generator. Output ONLY raw JSON arrays. No markdown, no explanation.',
            'messages'   => [ [ 'role' => 'user', 'content' => $prompt ] ],
        ] );

        $response = wp_remote_post( 'https://api.anthropic.com/v1/messages', [
            'timeout'    => epwf_api_timeout(),
            'sslverify'  => (bool) apply_filters( 'epwf_sslverify', true ),
            'user-agent' => 'WooFAQ-AI/' . EPWF_VERSION . '; ' . home_url(),
            'headers'    => [
                'x-api-key'         => $key,
                'anthropic-version' => '2023-06-01',
                'Content-Type'      => 'application/json',
            ],
            'body' => $body,
        ] );

        if ( is_wp_error( $response ) ) return new \WP_Error( 'epwf_http', $response->get_error_message() );

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

        if ( 200 !== $code ) {
            $msg = isset( $decoded['error']['message'] )
                ? sanitize_text_field( $decoded['error']['message'] )
                /* translators: %d: HTTP status code */
                : sprintf( __( 'Claude API returned HTTP %d.', 'exertpro-faq-ai' ), $code );
            return new \WP_Error( 'epwf_claude_err', $msg );
        }

        $content = isset( $decoded['content'][0]['text'] )
            ? trim( $decoded['content'][0]['text'] )
            : '';

        return $content ?: new \WP_Error( 'epwf_claude_empty', __( 'Claude returned an empty response.', 'exertpro-faq-ai' ) );
    }

    // ── Fix #12: Strict AI Response Parser ────────────────────────────────────

    private function parse_response( $raw ) {
        // Strip markdown code fences if AI wrapped the JSON
        $clean = preg_replace( '/^```(?:json)?\s*/m', '', $raw );
        $clean = preg_replace( '/\s*```$/m', '', $clean );
        $clean = trim( $clean );

        // Must start with [ and end with ]
        if ( ! $clean || $clean[0] !== '[' ) {
            // Try to extract JSON array from the response
            if ( preg_match( '/\[[\s\S]+\]/', $clean, $m ) ) {
                $clean = $m[0];
            } else {
                $this->log_error( 'Response did not contain a JSON array. Raw: ' . substr( $raw, 0, 200 ), '', '' );
                return new \WP_Error( 'epwf_malformed', __( 'AI response was malformed — no valid JSON array found. Please try again.', 'exertpro-faq-ai' ) );
            }
        }

        $data = json_decode( $clean, true );

        if ( JSON_ERROR_NONE !== json_last_error() ) {
            $this->log_error( 'JSON parse error: ' . json_last_error_msg(), '', '' );
            return new \WP_Error( 'epwf_json',
                /* translators: %s: JSON error message */
                sprintf( __( 'Could not parse AI response (%s). Please try again.', 'exertpro-faq-ai' ), json_last_error_msg() )
            );
        }

        // Handle wrapped format {"faqs":[...]}
        if ( is_array( $data ) && isset( $data['faqs'] ) && is_array( $data['faqs'] ) ) {
            $data = $data['faqs'];
        }

        if ( ! is_array( $data ) || empty( $data ) ) {
            return new \WP_Error( 'epwf_empty_arr', __( 'AI returned an empty FAQ list.', 'exertpro-faq-ai' ) );
        }

        $faqs = [];
        foreach ( $data as $item ) {
            if ( ! is_array( $item ) ) continue;

            // Fix #12: strict sanitization of AI content
            $q = isset( $item['question'] ) ? $item['question'] : '';
            $a = isset( $item['answer'] )   ? $item['answer']   : '';

            // Strip any HTML the AI may have injected
            $q = sanitize_text_field( wp_strip_all_tags( (string) $q ) );
            // Allow limited safe HTML in answers (bold, italic, links)
            $a = wp_kses( (string) $a, [
                'strong' => [], 'em' => [], 'b' => [], 'i' => [],
                'a'      => [ 'href' => [], 'title' => [], 'rel' => [] ],
                'br'     => [],
            ] );
            $a = sanitize_textarea_field( wp_strip_all_tags( $a ) );

            if ( mb_strlen( $q ) < 5 || mb_strlen( $a ) < 5 ) continue;  // skip trivial items
            if ( mb_strlen( $q ) > 300 )  $q = mb_substr( $q, 0, 300 );  // hard cap
            if ( mb_strlen( $a ) > 2000 ) $a = mb_substr( $a, 0, 2000 );

            $faqs[] = [ 'question' => $q, 'answer' => $a ];
        }

        if ( empty( $faqs ) ) {
            return new \WP_Error( 'epwf_no_valid', __( 'AI returned no valid FAQ items after validation.', 'exertpro-faq-ai' ) );
        }

        return $faqs;
    }

    // ── Fix #13: Rate limiting ─────────────────────────────────────────────────

    private function check_rate_limit() {
        if ( ! is_user_logged_in() ) {
            return new \WP_Error( 'epwf_not_logged_in', __( 'You must be logged in to generate FAQs.', 'exertpro-faq-ai' ) );
        }

        $uid      = get_current_user_id();
        $meta_key = '_epwf_rate_' . gmdate( 'YmdH' ); // resets every hour
        $count    = (int) get_user_meta( $uid, $meta_key, true );
        $limit    = (int) apply_filters( 'epwf_rate_limit', EPWF_RATE_LIMIT );

        if ( $count >= $limit ) {
            return new \WP_Error( 'epwf_rate_limit',
                sprintf(
                    /* translators: %d: request limit per hour */
                    __( 'Rate limit reached: maximum %d AI requests per hour. Please wait and try again.', 'exertpro-faq-ai' ),
                    $limit
                )
            );
        }

        update_user_meta( $uid, $meta_key, $count + 1 );

        // Clean up old hour keys (best-effort, non-blocking)
        $prev_key = '_epwf_rate_' . gmdate( 'YmdH', time() - 3600 );
        delete_user_meta( $uid, $prev_key );

        return true;
    }

    // ── Fix #14: Error logging ─────────────────────────────────────────────────

    private function log_error( $message, $provider, $model ) {
        // Only log when BOTH WP_DEBUG and WP_DEBUG_LOG are true,
        // or when a developer explicitly opts in via filter.
        $debug_on = defined( 'WP_DEBUG' ) && WP_DEBUG
                 && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG;
        if ( ! apply_filters( 'epwf_enable_error_log', $debug_on ) ) {
            return;
        }
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
        error_log( sprintf(
            '[ProductFAQ AI] Provider: %s | Model: %s | Error: %s',
            sanitize_text_field( $provider ?: 'unknown' ),
            sanitize_text_field( $model    ?: 'unknown' ),
            sanitize_text_field( $message )
        ) );
    }
}
