<?php
/**
 * Credits Backend Client (Week 26).
 *
 * Talks to the backend service for:
 * - site registration
 * - credits status
 * - credits consumption (idempotent)
 *
 * @package BoostBot
 */

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

class BoostBot_Credits_Backend_Client {

    /**
     * Get configured backend base URL (no trailing slash).
     */
    public function get_base_url(): string {
        $base = (string) BoostBot_Settings::get( 'backend_base_url', '' );
        $base = trim( $base );
        return rtrim( $base, '/' );
    }

    /**
     * Ensure a site_id exists for this WP site (register if needed).
     *
     * @return string|WP_Error
     */
    public function ensure_site_id() {
        $site_id  = trim( (string) BoostBot_Settings::get( 'backend_site_id', '' ) );
        $site_key = trim( (string) BoostBot_Settings::get( 'backend_site_api_key', '' ) );

        // Fast path: both site_id and site_api_key exist — fully configured.
        if ( '' !== $site_id && '' !== $site_key ) {
            return $site_id;
        }

        // ── Broken state detection: site_id exists but key is missing ──
        // This can happen if the initial registration succeeded but the key
        // wasn't persisted (disk full, DB issue, plugin re-install, etc.).
        // Log it clearly so it can be diagnosed from server logs.
        if ( '' !== $site_id && '' === $site_key ) {
            BoostBot_Logger::warning( 'WARNING: backend_site_id is set (' . $site_id . ') but backend_site_api_key is missing. Attempting re-registration.' );
        }

        $home_url = home_url( '/' );
        $host     = wp_parse_url( $home_url, PHP_URL_HOST );
        if ( empty( $host ) ) {
            return new WP_Error( 'boostbot_backend_invalid_domain', 'Could not determine site domain.' );
        }

        // Generate a management key so the backend can call our REST API for remote support.
        $management_key = $this->ensure_management_key();

        $resp = $this->request(
            'POST',
            '/api/v1/sites/register',
            array(
                'domain'         => strtolower( $host ),
                'wp_site_url'    => $home_url,
                'management_key' => $management_key,
            )
        );

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

        if ( empty( $resp['site_id'] ) ) {
            return new WP_Error( 'boostbot_backend_register_failed', 'Registration failed (missing site_id).' );
        }

        BoostBot_Settings::set( 'backend_site_id', sanitize_text_field( $resp['site_id'] ) );

        // Backend hardening: if a site is already registered, the backend will NOT reissue the key
        // (it only stores a hash). In that case the WP install must recover the key via admin rotation.
        if ( empty( $resp['site_api_key'] ) ) {
            BoostBot_Logger::error( 'CRITICAL: Site ' . $resp['site_id'] . ' is already registered but no API key was returned. The key must be rotated from the backend admin.' );
            // Store a transient so we can show an admin notice.
            set_transient( 'boostbot_backend_key_missing', 1, DAY_IN_SECONDS );
            return new WP_Error(
                'boostbot_backend_missing_site_key',
                'Backend connection error: the site API key is missing and cannot be re-issued automatically. Please go to BoostBot Settings → Connection and click "Repair Connection", or contact support.'
            );
        }

        // Save the key and VERIFY it was persisted correctly.
        BoostBot_Settings::set( 'backend_site_api_key', sanitize_text_field( $resp['site_api_key'] ) );

        // Verification: read back the key to ensure encryption round-trip works.
        $verify = trim( (string) BoostBot_Settings::get( 'backend_site_api_key', '' ) );
        if ( '' === $verify ) {
            BoostBot_Logger::error( 'CRITICAL: backend_site_api_key was saved but could not be read back. Encryption round-trip may be broken.' );
            return new WP_Error(
                'boostbot_backend_key_save_failed',
                'Backend API key was received but could not be persisted. This may indicate a database or encryption issue.'
            );
        }

        // Auto-sync management key to backend after successful registration.
        $this->sync_management_key();

        return (string) $resp['site_id'];
    }

    /**
     * Ensure a management key exists for backend → WP remote support.
     *
     * Generates a random 32-byte hex key, stores its SHA-256 hash in WP options,
     * and returns the plaintext so it can be sent to the backend.
     * If a key already exists, returns the stored plaintext (kept in encrypted options).
     *
     * @since 1.18.0
     * @return string The plaintext management key.
     */
    public function ensure_management_key() {
        // Check if we already have a management key.
        $existing = BoostBot_Settings::get( 'management_key_plaintext', '' );
        if ( ! empty( $existing ) ) {
            return $existing;
        }

        // Generate new key.
        $key  = bin2hex( random_bytes( 32 ) );
        $hash = hash( 'sha256', $key );

        // Store both: hash for verification, plaintext for re-sync with backend.
        update_option( 'boostbot_management_key_hash', $hash );
        BoostBot_Settings::set( 'management_key_plaintext', $key );

        BoostBot_Logger::info( 'Management key generated for remote support.' );

        return $key;
    }

    /**
     * Sync the management key to the backend (call after initial registration or key rotation).
     *
     * @since 1.18.0
     * @return bool|WP_Error True on success.
     */
    public function sync_management_key() {
        $site_id = BoostBot_Settings::get( 'backend_site_id', '' );
        if ( empty( $site_id ) ) {
            return new WP_Error( 'no_site_id', 'Site not registered with backend yet.' );
        }

        $key = $this->ensure_management_key();

        $resp = $this->request(
            'POST',
            '/api/v1/sites/' . $site_id . '/management-key',
            array( 'management_key' => $key )
        );

        return is_wp_error( $resp ) ? $resp : true;
    }

    /**
     * Fetch credits status.
     *
     * @param string $site_id
     * @return array|WP_Error
     */
    public function get_status( string $site_id ) {
        return $this->request( 'GET', '/api/v1/sites/' . rawurlencode( $site_id ) . '/credits/status' );
    }

    /**
     * Consume 1 credit for a conversation start (idempotent).
     *
     * @param string $site_id
     * @param string $conversation_id
     * @return array|WP_Error
     */
    public function consume( string $site_id, string $conversation_id ) {
        $conversation_id = trim( (string) $conversation_id );
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/credits/consume',
            array(
                'conversation_id' => $conversation_id,
                'idempotency_key' => 'consume:' . $conversation_id,
            )
        );
    }

    /**
     * Fetch credit usage history (daily consumption).
     *
     * @param string $site_id
     * @param int    $days Number of days to look back (default 30).
     * @return array|WP_Error
     */
    public function get_usage_history( string $site_id, int $days = 30 ) {
        return $this->request( 'GET', '/api/v1/sites/' . rawurlencode( $site_id ) . '/credits/usage-history?days=' . absint( $days ) );
    }

    /**
     * Get pricing catalog from backend (Week 26).
     *
     * @return array|WP_Error
     */
    public function get_catalog() {
        return $this->request( 'GET', '/api/v1/pricing/catalog' );
    }

    /**
     * Create a PayPal subscription in backend (backend-owned subscriptions, Week 26).
     *
     * @param string $site_id
     * @param string $package_id
     * @param string $return_url
     * @param string $cancel_url
     * @return array|WP_Error
     */
    public function create_paypal_subscription( string $site_id, string $package_id, string $return_url, string $cancel_url ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/subscriptions/paypal/create',
            array(
                'package_id' => $package_id,
                'return_url' => $return_url,
                'cancel_url' => $cancel_url,
            )
        );
    }

    /**
     * Fetch subscription status from backend (Week 26).
     *
     * @param string $site_id
     * @return array|WP_Error
     */
    public function get_subscription_status( string $site_id ) {
        return $this->request( 'GET', '/api/v1/sites/' . rawurlencode( $site_id ) . '/subscription/status' );
    }

    /**
     * Create a PayPal order in backend (backend-first flow, Week 26.7).
     *
     * @param string $site_id
     * @param string $package_id
     * @param string $return_url
     * @param string $cancel_url
     * @return array|WP_Error
     */
    public function create_paypal_order( string $site_id, string $package_id, string $return_url, string $cancel_url ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/purchases/paypal/create-order',
            array(
                'package_id' => $package_id,
                'return_url' => $return_url,
                'cancel_url' => $cancel_url,
            )
        );
    }

    /**
     * Capture a PayPal order after the user approves it (Week 26.7).
     *
     * Credits are granted via webhook after capture completes.
     *
     * @param string $site_id
     * @param string $order_id
     * @return array|WP_Error
     */
    public function capture_paypal_order( string $site_id, string $order_id ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/purchases/paypal/capture-order',
            array(
                'order_id' => $order_id,
            )
        );
    }

    /**
     * Refund a PayPal capture (admin/testing helper).
     *
     * Refund enforcement (disable site) is applied via backend webhook.
     *
     * @param string $site_id
     * @param string $capture_id
     * @return array|WP_Error
     */
    public function refund_paypal_capture( string $site_id, string $capture_id ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/purchases/paypal/refund-capture',
            array(
                'capture_id' => $capture_id,
            )
        );
    }

    /**
     * Grant Week 26 migration goodwill (100 purchased credits) once per site.
     *
     * @param string $site_id
     * @return array|WP_Error
     */
    public function grant_goodwill( string $site_id ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/migration/goodwill',
            array()
        );
    }

    /**
     * Proxy chat generation through the backend (Week 26).
     *
     * @param string $site_id
     * @param array  $payload { messages: [...], system?:string, max_tokens?:int, temperature?:float }
     * @return array|WP_Error
     */
    public function chat( string $site_id, array $payload ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/chat',
            $payload,
            60
        );
    }

    /**
     * Week 28: RAG rerank helper (Gemini) hosted on backend.
     *
     * @param string $site_id
     * @param string $query
     * @param array  $candidates [{id,text,title?,url?,score?}]
     * @return array|WP_Error
     */
    public function rag_rerank( string $site_id, string $query, array $candidates ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/rag/rerank',
            array(
                'query'      => $query,
                'candidates' => $candidates,
            )
        );
    }

    /**
     * Week 27.5: Gemini query understanding / rewrite helper.
     *
     * @param string $site_id
     * @param string $query
     * @param string|null $language_hint 'he'|'en'
     * @return array|WP_Error
     */
    public function rag_prepare_query( string $site_id, string $query, ?string $language_hint = null, ?array $context_messages = null ) {
        $body = array( 'query' => $query );
        if ( ! empty( $language_hint ) ) {
            $body['language_hint'] = $language_hint;
        }
        if ( is_array( $context_messages ) && ! empty( $context_messages ) ) {
            $body['context_messages'] = $context_messages;
        }

        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/rag/prepare-query',
            $body
        );
    }

    /**
     * Week 27.5: Gemini evidence compression helper (optional but recommended).
     *
     * @param string $site_id
     * @param string $query
     * @param array  $candidates [{id,text,title?,url?}]
     * @return array|WP_Error
     */
    public function rag_compress( string $site_id, string $query, array $candidates ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/rag/compress',
            array(
                'query'      => $query,
                'candidates' => $candidates,
            )
        );
    }

    /**
     * Week 27.8-2: Gemini tool selection helper (contacts + widgets) hosted on backend.
     *
     * @param string $site_id
     * @param array  $payload {
     *   query: string,
     *   language_hint?: 'he'|'en',
     *   intents?: string[],
     *   entities?: string[],
     *   must_include?: string[],
     *   available_contacts: array[],
     *   available_widgets: array[]
     * }
     * @return array|WP_Error
     */
    public function rag_select_tools( string $site_id, array $payload ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/rag/select-tools',
            $payload
        );
    }

    /**
     * Week 31: Unified Router (Gemini) - consolidates all routing decisions.
     *
     * Replaces fragmented approach (prepare-query + PHP keywords + select-tools)
     * with a single intelligent routing decision.
     *
     * @param string $site_id
     * @param array  $payload {
     *   query: string,
     *   language_hint?: 'he'|'en',
     *   context_messages?: array[{role,content}],
     *   available_contacts?: array[{id,name,title?,department?,specialties?,description?}],
     *   available_widgets?: array[{id,type,name,description?}],
     *   brand_name?: string,
     *   page_title?: string,
     *   page_url?: string
     * }
     * @return array|WP_Error {
     *   rewritten_query: string,
     *   language: 'he'|'en',
     *   intents: string[],
     *   entities: string[],
     *   must_include: string[],
     *   routing: {
     *     needs_retrieval: bool,
     *     retrieval_strategy: 'colbert'|'raptor'|'both',
     *     needs_contacts: bool,
     *     needs_widgets: bool,
     *     needs_brand_info: bool,
     *     needs_page_context: bool,
     *     confidence: float
     *   },
     *   selected_contact_ids: int[],
     *   selected_widget_ids: int[],
     *   picker_widget_ids: int[],
     *   needs_clarification: bool,
     *   clarification_question: string|null,
     *   reasoning: string,
     *   fallback_used: bool,
     *   provider: string|null,
     *   model: string|null,
     *   usage: object|null
     * }
     */
    public function rag_unified_router( string $site_id, array $payload ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/rag/unified-router',
            $payload,
            20 // Slightly longer timeout for comprehensive routing
        );
    }

    /**
     * Week 27.6: BrandCard generation helper (Gemini) hosted on backend.
     *
     * @param string $site_id
     * @param array  $payload {about_source:{title?,url?,text}, language_hint?, existing?}
     * @return array|WP_Error
     */
    public function rag_brand_card_generate( string $site_id, array $payload ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/rag/brand-card/generate',
            $payload
        );
    }

    /**
     * Week 27.7: BrandCard + ServicesList autofill helper (Gemini) hosted on backend.
     *
     * @param string $site_id
     * @param array  $payload {about_source:{title?,url?,text}, pages:[{title?,url?,text}], language_hint?, existing?}
     * @return array|WP_Error
     */
    public function rag_brand_card_autofill( string $site_id, array $payload ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/rag/brand-card/autofill',
            $payload,
            30
        );
    }

    /**
     * Phase 1 of two-phase autofill: Gemini selects which pages to read in full.
     *
     * @param string $site_id
     * @param array  $payload {pages:[{post_id,title,url?,excerpt?}], language_hint?}
     * @return array|WP_Error  {selected_page_ids:[int], fallback_used:bool}
     */
    public function rag_brand_card_autofill_plan( string $site_id, array $payload ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/rag/brand-card/autofill/plan',
            $payload,
            15
        );
    }

    /**
     * Quick LLM call for simple tasks like widget analysis.
     * Uses Gemini Flash for fast, cheap inference.
     *
     * @since 1.16.0
     * @param string $site_id  Site ID.
     * @param array  $payload  {model, prompt, max_tokens?, purpose?}
     * @return array|WP_Error Response with 'response' key.
     */
    public function quick_llm_call( string $site_id, array $payload ) {
        return $this->request(
            'POST',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/llm/quick',
            $payload,
            10 // 10 second timeout for quick calls
        );
    }

    /**
     * Fetch the current JWT signing secret from the backend.
     *
     * Used by the JWT sync mechanism to keep wp-config.php in sync
     * with the backend's signing secret.
     *
     * @param string $site_id Site ID.
     * @return array|WP_Error Response with 'jwt_secret' key.
     */
    public function fetch_jwt_secret( string $site_id ) {
        return $this->request(
            'GET',
            '/api/v1/sites/' . rawurlencode( $site_id ) . '/jwt-secret',
            null,
            10
        );
    }

    /**
     * Low-level HTTP request helper.
     *
     * @param string $method
     * @param string $path
     * @param array|null $body
     * @return array|WP_Error
     */
    private function request( string $method, string $path, ?array $body = null, int $timeout_seconds = 15 ) {
        $t0 = microtime( true );
        $base = $this->get_base_url();
        if ( empty( $base ) ) {
            return new WP_Error( 'boostbot_backend_not_configured', 'Credits backend not configured (missing backend_base_url).' );
        }

        $url = $base . $path;

        $raw_body = '';
        if ( null !== $body ) {
            $raw_body = wp_json_encode( $body );
            if ( false === $raw_body ) {
                return new WP_Error( 'boostbot_backend_invalid_json', 'Failed to encode request body.' );
            }
        }

        $headers = array(
            'Accept'       => 'application/json',
            'Content-Type' => 'application/json',
        );

        // Per-site backend API key (Authorization: Bearer <site_api_key>)
        $site_key = (string) BoostBot_Settings::get( 'backend_site_api_key', '' );
        $site_key = trim( $site_key );
        // Add bearer auth for all requests except the initial register (which doesn't have a key yet).
        if ( ! empty( $site_key ) && '/api/v1/sites/register' !== $path ) {
            $headers['Authorization'] = 'Bearer ' . $site_key;
        }

        // Timeout guardrails:
        // - Most backend helpers should be fast (default 15s).
        // - Final chat can legitimately take longer, so callers may override.
        $timeout_seconds = absint( $timeout_seconds );
        if ( $timeout_seconds < 5 ) {
            $timeout_seconds = 5;
        }
        if ( $timeout_seconds > 120 ) {
            $timeout_seconds = 120;
        }

        $args = array(
            'method'  => $method,
            'timeout' => $timeout_seconds,
            'headers' => $headers,
        );
        if ( null !== $body ) {
            $args['body'] = $raw_body;
        }

        $res = wp_remote_request( $url, $args );
        $elapsed_ms = round( ( microtime( true ) - $t0 ) * 1000 );
        BoostBot_Logger::info( sprintf( '[TIMING] backend_request %s %s: %dms', $method, $path, $elapsed_ms ) );
        if ( is_wp_error( $res ) ) {
            return $res;
        }

        $status = (int) wp_remote_retrieve_response_code( $res );
        $text   = (string) wp_remote_retrieve_body( $res );

        $json = json_decode( $text, true );
        if ( ! is_array( $json ) ) {
            return new WP_Error( 'boostbot_backend_bad_response', 'Invalid backend response.' );
        }

        // Backend standard envelope: { success: true, data: ... } | { success:false, error:{...} }
        if ( isset( $json['success'] ) && false === $json['success'] ) {
            $msg = isset( $json['error']['message'] ) ? (string) $json['error']['message'] : 'Backend error';
            return new WP_Error( 'boostbot_backend_error', $msg, array( 'http_status' => $status, 'raw' => $json ) );
        }

        if ( isset( $json['data'] ) && is_array( $json['data'] ) ) {
            return $json['data'];
        }

        // Fallback: return whole object if not wrapped
        return $json;
    }
}


