<?php
/**
 * REST API endpoints.
 *
 * @package Inqyra
 */

// If this file is called directly, abort.
if (!defined('WPINC')) {
    die;
}

/**
 * REST API class.
 */
class Inqyra_REST_API {

    /**
     * API namespace.
     */
    const API_NAMESPACE = 'inqyra/v1';

    /**
     * Maximum requests per IP per minute.
     */
    const IP_RATE_LIMIT_PER_MINUTE = 20;

    /**
     * Maximum requests per IP per hour.
     */
    const IP_RATE_LIMIT_PER_HOUR = 200;

    /**
     * Maximum message length in characters.
     */
    const MAX_MESSAGE_LENGTH = 10000;

    /**
     * Constructor.
     */
    public function __construct() {
        // Clean output buffer before serving REST responses to prevent PHP notices from breaking JSON
        add_filter('rest_pre_serve_request', [$this, 'clean_output_buffer'], 0, 4);
    }

    /**
     * Clean output buffer before REST response.
     *
     * This prevents PHP notices/warnings from other plugins from breaking our JSON responses.
     *
     * @param bool             $served  Whether the request has been served.
     * @param WP_REST_Response $result  Response object.
     * @param WP_REST_Request  $request Request object.
     * @param WP_REST_Server   $server  Server instance.
     * @return bool
     */
    public function clean_output_buffer($served, $result, $request, $server) {
        // Only clean for our namespace
        $route = $request->get_route();
        if (strpos($route, '/' . self::API_NAMESPACE) === 0) {
            while (ob_get_level() > 0) {
                ob_end_clean();
            }
        }
        return $served;
    }

    /**
     * Register REST API routes.
     */
    public function register_routes() {
        // Chat endpoint (public)
        register_rest_route(self::API_NAMESPACE, '/chat', [
            'methods' => 'POST',
            'callback' => [$this, 'handle_chat'],
            'permission_callback' => '__return_true',
            'args' => [
                'agent_id' => [
                    'required' => true,
                    'type' => 'integer',
                    'sanitize_callback' => 'absint',
                ],
                'message' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_textarea_field',
                ],
                'session_id' => [
                    'required' => false,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                ],
                'query_embedding' => [
                    'required' => false,
                    'type' => 'array',
                    'validate_callback' => function ( $param ) {
                        if ( ! is_array( $param ) || count( $param ) !== 384 ) {
                            return false;
                        }
                        foreach ( $param as $v ) {
                            if ( ! is_numeric( $v ) ) {
                                return false;
                            }
                        }
                        return true;
                    },
                    'sanitize_callback' => function ( $param ) {
                        return array_map( 'floatval', $param );
                    },
                ],
            ],
        ]);

        // Get conversation history (for session restore)
        register_rest_route(self::API_NAMESPACE, '/conversation/(?P<session_id>[a-zA-Z0-9_-]+)', [
            'methods' => 'GET',
            'callback' => [$this, 'get_conversation'],
            'permission_callback' => '__return_true',
            'args' => [
                'session_id' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                ],
                'token' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                ],
            ],
        ]);

        // Health check endpoint
        register_rest_route(self::API_NAMESPACE, '/health', [
            'methods' => 'GET',
            'callback' => [$this, 'health_check'],
            'permission_callback' => '__return_true',
        ]);
    }

    /**
     * Handle chat request.
     *
     * @param WP_REST_Request $request The request object.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function handle_chat(WP_REST_Request $request) {
        // Block chat if premium license has expired
        if (function_exists('inqyra_is_license_expired') && inqyra_is_license_expired()) {
            return new WP_Error(
                'license_expired',
                __('This chatbot is currently unavailable.', 'inqyra'),
                ['status' => 403]
            );
        }

        // Check per-IP rate limit first (before any processing)
        $rate_limit_error = $this->check_ip_rate_limit();
        if ($rate_limit_error !== null) {
            return $rate_limit_error;
        }

        $agent_id = (int) $request->get_param('agent_id');
        $message = $request->get_param('message');
        $session_id = $request->get_param('session_id');
        $query_embedding = $request->get_param('query_embedding');

        // Validate message
        if (empty(trim($message))) {
            return new WP_Error(
                'empty_message',
                __('Message cannot be empty.', 'inqyra'),
                ['status' => 400]
            );
        }

        // Validate message length
        if (mb_strlen($message) > self::MAX_MESSAGE_LENGTH) {
            return new WP_Error(
                'message_too_long',
                sprintf(
                    /* translators: %d: Maximum message length */
                    __('Message exceeds maximum length of %d characters.', 'inqyra'),
                    self::MAX_MESSAGE_LENGTH
                ),
                ['status' => 400]
            );
        }

        // Generate session ID if not provided
        if (empty($session_id)) {
            $session_id = $this->generate_session_id();
        }

        // Validate query embedding if provided
        if (!empty($query_embedding) && is_array($query_embedding)) {
            $query_embedding = array_map('floatval', $query_embedding);
            // Should be 384 dimensions for MiniLM
            if (count($query_embedding) !== 384) {
                $query_embedding = null;
            }
        } else {
            $query_embedding = null;
        }

        try {
            $chat_service = new Inqyra_Chat_Service();
            $response = $chat_service->process_message(
                $agent_id,
                $session_id,
                $message,
                ['query_embedding' => $query_embedding]
            );

            $rest_data = [
                'success' => true,
                'response' => $response['response'],
                'session_id' => $session_id,
                'session_token' => $this->generate_session_token($session_id),
                'conversation_id' => $response['conversation_id'],
            ];

            // Include search debug info only for authenticated admins
            if (!empty($response['search_debug']) && current_user_can('manage_options')) {
                $rest_data['search_debug'] = $response['search_debug'];
            }

            return new WP_REST_Response($rest_data, 200);

        } catch (\Throwable $e) {
            $error_message = $e->getMessage();

            // Check for rate limit errors
            if (strpos($error_message, 'limit') !== false) {
                return new WP_Error(
                    'rate_limit',
                    $error_message,
                    ['status' => 429]
                );
            }

            // Log the error with file and line info.
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated behind WP_DEBUG.
                error_log(sprintf(
                    'Inqyra API error: %s in %s:%d',
                    $error_message,
                    $e->getFile(),
                    $e->getLine()
                ));
            }

            $error_data = ['status' => 500];

            // Include debug info for admins only in debug mode
            if ( current_user_can( 'manage_options' ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                $error_data['debug'] = sprintf( '%s in %s:%d', $error_message, $e->getFile(), $e->getLine() );
            }

            return new WP_Error(
                'chat_error',
                __('An error occurred while processing your message.', 'inqyra'),
                $error_data
            );
        }
    }

    /**
     * Get conversation history.
     *
     * @param WP_REST_Request $request The request object.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_conversation(WP_REST_Request $request) {
        $session_id = $request->get_param('session_id');
        $token = $request->get_param('token');

        // Verify session token to prevent unauthorized access
        $expected_token = $this->generate_session_token($session_id);
        if (!hash_equals($expected_token, $token ?? '')) {
            return new WP_Error(
                'invalid_token',
                __('Invalid session token.', 'inqyra'),
                ['status' => 403]
            );
        }

        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
        $conversation = $wpdb->get_row($wpdb->prepare(
            'SELECT * FROM %i WHERE session_id = %s',
            $wpdb->prefix . 'inqyra_conversations',
            $session_id
        ));

        if (!$conversation) {
            return new WP_REST_Response([
                'success' => true,
                'messages' => [],
            ], 200);
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
        $messages = $wpdb->get_results($wpdb->prepare(
            'SELECT role, content, created_at FROM %i
             WHERE conversation_id = %d
             ORDER BY created_at ASC
             LIMIT 50',
            $wpdb->prefix . 'inqyra_messages',
            $conversation->id
        ), ARRAY_A);

        return new WP_REST_Response([
            'success' => true,
            'conversation_id' => $conversation->id,
            'messages' => $messages,
        ], 200);
    }

    /**
     * Health check endpoint.
     *
     * @return WP_REST_Response Response object.
     */
    public function health_check() {
        $status = [
            'status' => 'ok',
        ];

        // Only expose detailed info to authenticated admins
        if ( current_user_can( 'manage_options' ) ) {
            $status['version']              = INQYRA_VERSION;
            $status['php_version']          = PHP_VERSION;
            $status['wp_version']           = get_bloginfo( 'version' );
            $status['providers_configured'] = Inqyra_AI_Provider_Factory::has_any_configured();
            $status['license_valid']        = function_exists( 'inqyra_fs' ) && inqyra_fs()->is__premium_only() && inqyra_fs()->is_paying();
        }

        return new WP_REST_Response( $status, 200 );
    }

    /**
     * Generate a unique session ID.
     *
     * @return string Session ID.
     */
    private function generate_session_id(): string {
        return 'sess_' . bin2hex(random_bytes(16));
    }

    /**
     * Generate a session token for conversation history access.
     *
     * Uses HMAC so the token is deterministically derived from the session ID
     * and a server-side secret. No need to store the token in the database.
     *
     * @param string $session_id The session ID.
     * @return string The session token (hex-encoded HMAC).
     */
    private function generate_session_token(string $session_id): string {
        return hash_hmac('sha256', $session_id, wp_salt('auth') . 'inqyra_session_token');
    }

    /**
     * Check per-IP rate limit.
     *
     * Uses atomic database operations to prevent race conditions under concurrency.
     *
     * @return WP_Error|null Returns WP_Error if rate limited, null if OK.
     */
    private function check_ip_rate_limit() {
        $ip = $this->get_client_ip();
        if (empty($ip)) {
            return null; // Can't rate limit without IP
        }

        // Create a safe cache key (hash the IP to avoid key length issues)
        $ip_hash = md5($ip);

        // Check per-minute limit
        if (!$this->increment_rate_counter('inqyra_rate_' . $ip_hash . '_min', 60, self::IP_RATE_LIMIT_PER_MINUTE)) {
            return new WP_Error(
                'rate_limit',
                __('Too many requests. Please wait a moment before trying again.', 'inqyra'),
                ['status' => 429]
            );
        }

        // Check per-hour limit
        if (!$this->increment_rate_counter('inqyra_rate_' . $ip_hash . '_hour', 3600, self::IP_RATE_LIMIT_PER_HOUR)) {
            return new WP_Error(
                'rate_limit',
                __('Hourly request limit exceeded. Please try again later.', 'inqyra'),
                ['status' => 429]
            );
        }

        return null;
    }

    /**
     * Atomically increment a rate limit counter.
     *
     * Uses a single UPDATE query so the read+check+increment is atomic in MySQL,
     * preventing concurrent requests from bypassing the limit.
     *
     * @param string $key   Transient key name.
     * @param int    $ttl   Time-to-live in seconds.
     * @param int    $limit Maximum allowed count.
     * @return bool True if under limit (incremented), false if at/over limit.
     */
    private function increment_rate_counter(string $key, int $ttl, int $limit): bool {
        global $wpdb;

        $transient_option = '_transient_' . $key;
        $timeout_option = '_transient_timeout_' . $key;

        // Check if the counter has expired and clean up if so
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
        $timeout = $wpdb->get_var($wpdb->prepare(
            'SELECT option_value FROM %i WHERE option_name = %s',
            $wpdb->options,
            $timeout_option
        ));

        if ($timeout !== null && (int) $timeout < time()) {
            // Expired — clean up and reset
            delete_transient($key);
            $timeout = null;
        }

        if ($timeout === null) {
            // Counter doesn't exist — create it (small race here on first request only)
            set_transient($key, 1, $ttl);
            return true;
        }

        // Counter exists and not expired — atomic increment with limit check.
        // MySQL handles read+check+write in a single statement, preventing
        // concurrent requests from reading the same stale count.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
        $rows_updated = $wpdb->query($wpdb->prepare(
            'UPDATE %i SET option_value = option_value + 0 + 1 WHERE option_name = %s AND option_value + 0 < %d',
            $wpdb->options,
            $transient_option,
            $limit
        ));

        // rows_updated > 0: incremented successfully (was under limit)
        // rows_updated = 0: at or over limit (UPDATE matched no rows)
        return $rows_updated > 0;
    }

    /**
     * Get the client IP address.
     *
     * Proxy headers (X-Forwarded-For, etc.) are only trusted when the request
     * arrives from a local/private IP (indicating a reverse proxy like Nginx or
     * Apache is in front). When REMOTE_ADDR is a public IP, the client is
     * connecting directly and proxy headers can be spoofed.
     *
     * @return string Client IP address.
     */
    private function get_client_ip(): string {
        // CloudFlare sets this header at their edge — always safe to trust
        if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
            $ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']));
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
                return $ip;
            }
        }

        // Auto-detect proxy: if REMOTE_ADDR is a private/reserved IP, the request
        // is coming through a local reverse proxy (Nginx, Apache, load balancer),
        // so proxy headers are trustworthy. If REMOTE_ADDR is public, the client
        // is connecting directly and could spoof these headers.
        $remote_addr = !empty($_SERVER['REMOTE_ADDR'])
            ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']))
            : '';
        $behind_proxy = !empty($remote_addr) && !filter_var(
            $remote_addr,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        );

        if ($behind_proxy) {
            $proxy_headers = [
                'HTTP_X_FORWARDED_FOR',
                'HTTP_X_REAL_IP',
                'HTTP_CLIENT_IP',
            ];

            foreach ($proxy_headers as $header) {
                if (!empty($_SERVER[$header])) {
                    // X-Forwarded-For can contain multiple IPs, take the first public one
                    $ip_list = sanitize_text_field(wp_unslash($_SERVER[$header]));
                    $ips = array_map('trim', explode(',', $ip_list));
                    foreach ($ips as $ip) {
                        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                            return $ip;
                        }
                    }
                }
            }
        }

        // Use REMOTE_ADDR directly (either public IP or no proxy headers found)
        if (!empty($remote_addr) && filter_var($remote_addr, FILTER_VALIDATE_IP)) {
            return $remote_addr;
        }

        return '';
    }

}
