/**
 * Chat API Client
 * Handles communication with the WordPress REST API and direct backend streaming
 */

import type { 
  SendMessageRequest, 
  SendMessageResponse, 
  ConfigResponse,
  CommerceActionHint,
  CommerceContextLite,
  RespondMetaV2Optional,
  WooCart,
} from '@/types';
import { t } from '@/lib/i18n';
import { getCurrentLocale } from '@/utils/locale';
import { normalizeWooCart } from '@/utils/cart';

/**
 * Proactive/Page transition action response
 */
type ProactiveAction = 
  | { action: 'WAIT' }
  | { action: 'SHOW_WIDGET'; widget: { type: string; id: string | number; config?: Record<string, unknown> } }
  | { action: 'RESPOND'; gpt_payload: { context: string; instructions: string; max_response_chars: number } };

/**
 * Per-visitor message rate limit error (Week 33).
 * Thrown when the backend returns rate_limited=true from retrieval/chat endpoints.
 * Carries the bot-friendly reply so the UI can display it as a chat message.
 */
export class RateLimitError extends Error {
  public readonly reply: string;
  public readonly limitType: string;
  public readonly retryAfter: number;
  public readonly cooldownSeconds: number;
  public readonly remainingHour: number;
  public readonly remainingDay: number;

  constructor(data: {
    reply: string;
    limit_type?: string;
    retry_after?: number;
    cooldown_seconds?: number;
    remaining_hour?: number;
    remaining_day?: number;
  }) {
    super(data.reply || 'Rate limited');
    this.name = 'RateLimitError';
    this.reply = data.reply;
    this.limitType = data.limit_type || '';
    this.retryAfter = data.retry_after || 0;
    this.cooldownSeconds = data.cooldown_seconds || 0;
    this.remainingHour = data.remaining_hour ?? 0;
    this.remainingDay = data.remaining_day ?? 0;
  }
}

function normalizeApiErrorMessage(message: unknown): string | undefined {
  if (typeof message !== 'string' || !message.trim()) {
    return undefined;
  }

  if (message === 'Too many requests. Please wait a moment and try again.') {
    return t('Too many requests. Please wait a moment and try again.');
  }

  return message;
}

/**
 * Session token response from WordPress
 */
interface SessionTokenResponse {
  token: string;
  expires_at: number;
  stream_url: string;
  credits_ok: boolean;
}

interface NormalizedCartResponse {
  cart: WooCart | null;
  message?: string;
  cartCount?: number;
}

interface BundleAddToCartResponse extends NormalizedCartResponse {
  couponApplied?: boolean;
  couponCode?: string;
}

/**
 * Orchestrated retrieval response (two-tier chunks)
 */
interface OrchestratedRetrievalResponse {
  conversation_id: string;
  messages: Array<{ role: string; content: string }>;
  history: Array<{ role: string; content: string }>;
  small_chunks: string;
  large_chunks: string;
  page_content: string;
  bot_config: {
    name: string;
    personality: string;
    output_limit: number;
    enabled_widgets: string[];
    language: string;
    tone?: string;
    forbidden_topics?: string;
    brand?: {
      company_name?: string;
      one_liner?: string;
      about_summary?: string;
      phone?: string;
      whatsapp_number?: string;
      primary_cta_text?: string;
      primary_cta_url?: string;
    };
    behavior?: {
      proactive_engagement?: boolean;
      proactive_delay_seconds?: number;
    };
    widget_assignments?: Array<{
      type: string;
      name: string;
      id: number;
      description: string;
      keywords?: string[];
      instruction?: string;
      has_phone_field?: boolean;
    }>;
  };
  shown_widgets_in_conversation: string[];
  shown_widget_ids_in_conversation?: number[];
  routing: {
    needs_retrieval: boolean;
    needs_page_context: boolean;
  } | null;
  timing_ms: number;
}

/**
 * Metadata event from unified /chat/respond endpoint.
 * First SSE event before content chunks.
 */
interface UnifiedRespondMeta extends RespondMetaV2Optional {
  type: 'meta';
  action: 'RESPOND' | 'RESPOND_WITH_WIDGET' | 'WAIT_FOR_USER';
  widget: { type: string; id?: number; reason: string; prefill?: Record<string, string> } | null;
  widgets?: Array<{ type: string; id?: number; reason: string; prefill?: Record<string, string> }>;
  needs_followup: boolean;
  followup_reason: string | null;
  thinking_message: string | null;
}

interface WooCartEnvelope {
  success: boolean;
  data?: {
    cart?: WooCart | null;
    success?: boolean;
    message?: string;
  };
  message?: string;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null;
}

function isProtocolDebugEnabled(): boolean {
  if (typeof window === 'undefined') return false;

  try {
    if (window.sessionStorage?.getItem('boostbot_debug_protocol') === '1') {
      return true;
    }
  } catch {
    // Ignore sessionStorage access issues.
  }

  const hostname = window.location.hostname || '';
  return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('dev.');
}

function normalizeCommerceActions(raw: unknown): CommerceActionHint[] {
  if (!Array.isArray(raw)) return [];

  return raw
    .filter(isRecord)
    .filter((action): action is Record<string, unknown> & { type: string } => typeof action.type === 'string')
    .map((action) => ({
      type: action.type,
      widget_type: typeof action.widget_type === 'string' ? action.widget_type : undefined,
      widget_id: typeof action.widget_id === 'number' ? action.widget_id : undefined,
      product_id: typeof action.product_id === 'number' ? action.product_id : undefined,
      reason: typeof action.reason === 'string' ? action.reason : undefined,
    }));
}

function normalizeMetaEvent(raw: unknown): UnifiedRespondMeta {
  const payload = isRecord(raw) ? raw : {};
  const knownMetaKeys = new Set([
    'type',
    'action',
    'widget',
    'widgets',
    'needs_followup',
    'followup_reason',
    'thinking_message',
    'protocol_version',
    'commerce_actions',
    'context_hints',
    '_debug',
  ]);

  const widget = isRecord(payload.widget) && typeof payload.widget.type === 'string'
    ? payload.widget as UnifiedRespondMeta['widget']
    : null;
  const widgets = Array.isArray(payload.widgets)
    ? payload.widgets.filter(
        (item): item is NonNullable<UnifiedRespondMeta['widgets']>[number] => isRecord(item) && typeof item.type === 'string'
      )
    : undefined;
  const contextHints = isRecord(payload.context_hints)
    ? {
        has_cart_context: typeof payload.context_hints.has_cart_context === 'boolean'
          ? payload.context_hints.has_cart_context
          : undefined,
        cart_item_count: typeof payload.context_hints.cart_item_count === 'number'
          ? payload.context_hints.cart_item_count
          : undefined,
      }
    : undefined;

  const normalized: UnifiedRespondMeta = {
    type: 'meta',
    action: payload.action === 'RESPOND_WITH_WIDGET' || payload.action === 'WAIT_FOR_USER' ? payload.action : 'RESPOND',
    widget,
    widgets,
    needs_followup: payload.needs_followup === true,
    followup_reason: typeof payload.followup_reason === 'string' ? payload.followup_reason : null,
    thinking_message: typeof payload.thinking_message === 'string' ? payload.thinking_message : null,
    protocol_version: typeof payload.protocol_version === 'string' ? payload.protocol_version : undefined,
    commerce_actions: normalizeCommerceActions(payload.commerce_actions),
    context_hints: contextHints,
  };

  if (isProtocolDebugEnabled()) {
    const unknownMetaKeys = Object.keys(payload).filter((key) => !knownMetaKeys.has(key));
    if (unknownMetaKeys.length > 0) {
      console.log('[BoostBot][Protocol] Unknown meta fields:', unknownMetaKeys);
    }
    if (normalized.protocol_version || (normalized.commerce_actions?.length ?? 0) > 0 || normalized.context_hints) {
      console.log('[BoostBot][Protocol] Normalized meta event:', {
        protocol_version: normalized.protocol_version ?? 'legacy',
        commerce_actions: normalized.commerce_actions,
        context_hints: normalized.context_hints,
      });
    }
  }

  return normalized;
}

class ChatApiClient {
  private baseUrl: string;
  private nonce: string;
  
  // Session token cache
  private sessionToken: string | null = null;
  private sessionExpires: number = 0;
  private streamUrl: string = '';
  
  constructor() {
    const config = window.boostbot_config;
    this.baseUrl = config?.api_url || '/wp-json/boostbot/v1/';
    this.nonce = config?.nonce || '';
  }

  /**
   * Build standard request headers for WordPress REST API calls.
   * Only includes X-WP-Nonce when a real nonce value exists.
   * Sending an empty X-WP-Nonce causes WordPress to return 403
   * ("Cookie check failed") because rest_cookie_check_errors()
   * treats it as an invalid nonce rather than "no nonce".
   */
  private wpHeaders(extra?: Record<string, string>): Record<string, string> {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      ...extra,
    };
    if (this.nonce) {
      headers['X-WP-Nonce'] = this.nonce;
    }
    return headers;
  }

  private getLanguageParam(): string {
    return getCurrentLocale();
  }
  
  /**
   * Get widget configuration
   */
  async getConfig(): Promise<ConfigResponse> {
    // Build URL with page context parameters
    const pageConfig = window.boostbot_config;
    const postId = pageConfig?.current_page?.id || '';
    const pageUrl = pageConfig?.current_page?.url || window.location.href;
    
    let url = this.baseUrl + 'config';
    const params = new URLSearchParams();
    if (postId) params.append('post_id', String(postId));
    if (pageUrl) params.append('page_url', pageUrl);
    
    if (params.toString()) {
      url += '?' + params.toString();
    }
    
    const response = await fetch(url, {
      method: 'GET',
      headers: this.wpHeaders(),
    });
    
    const data = await response.json();
    
    if (!response.ok) {
      throw new Error(normalizeApiErrorMessage(data.message) || 'Failed to get config');
    }
    
    return data;
  }

  async getCart(): Promise<WooCart> {
    const params = new URLSearchParams();
    params.set('language', this.getLanguageParam());

    const response = await fetch(`${this.baseUrl}woocommerce/cart?${params.toString()}`, {
      method: 'GET',
      headers: this.wpHeaders(),
    });

    const data = await response.json() as WooCartEnvelope;
    if (!response.ok || !data.success || !data.data?.cart) {
      throw new Error(normalizeApiErrorMessage(data.message) || t('Failed to load cart'));
    }

    return normalizeWooCart(data.data.cart) ?? {
      count: 0,
      subtotal: '0',
      total: '0',
      items: [],
      checkout_url: '',
      cart_url: '',
      is_empty: true,
    };
  }

  async addToCart(params: {
    productId: number;
    quantity?: number;
    variationId?: number;
    variation?: Record<string, string>;
  }): Promise<{ cart: WooCart | null; message?: string }> {
    const response = await fetch(`${this.baseUrl}woocommerce/cart/add`, {
      method: 'POST',
      headers: this.wpHeaders(),
      body: JSON.stringify({
        product_id: params.productId,
        quantity: params.quantity ?? 1,
        variation_id: params.variationId ?? 0,
        variation: params.variation ?? {},
        language: this.getLanguageParam(),
      }),
    });

    const data = await response.json() as WooCartEnvelope;
    if (!response.ok || !data.success) {
      throw new Error(normalizeApiErrorMessage(data.message) || t('Failed to add item to cart'));
    }

    return {
      cart: normalizeWooCart(data.data?.cart ?? null),
      message: data.data?.message,
    };
  }

  async submitProductCard(payload: Record<string, unknown>): Promise<NormalizedCartResponse> {
    const response = await fetch(`${this.baseUrl}widget/product_card/submit`, {
      method: 'POST',
      headers: this.wpHeaders(),
      body: JSON.stringify(payload),
    });

    const data = await response.json() as WooCartEnvelope & {
      data?: {
        cart?: WooCart | null;
        message?: string;
        cart_count?: number;
      };
    };

    if (!response.ok || !data.success) {
      throw new Error(normalizeApiErrorMessage(data.message) || t('Failed to add item to cart'));
    }

    return {
      cart: normalizeWooCart(data.data?.cart ?? null),
      message: data.data?.message,
      cartCount: typeof data.data?.cart_count === 'number' ? data.data.cart_count : undefined,
    };
  }

  async updateCartItem(itemKey: string, quantity: number): Promise<{ cart: WooCart | null; message?: string }> {
    const response = await fetch(`${this.baseUrl}woocommerce/cart/update-item`, {
      method: 'POST',
      headers: this.wpHeaders(),
      body: JSON.stringify({
        item_key: itemKey,
        quantity,
        language: this.getLanguageParam(),
      }),
    });

    const data = await response.json() as WooCartEnvelope;
    if (!response.ok || !data.success) {
      throw new Error(normalizeApiErrorMessage(data.message) || t('Failed to update cart'));
    }

    return {
      cart: normalizeWooCart(data.data?.cart ?? null),
      message: data.data?.message,
    };
  }

  async removeCartItem(itemKey: string): Promise<{ cart: WooCart | null; message?: string }> {
    const response = await fetch(`${this.baseUrl}woocommerce/cart/remove-item`, {
      method: 'POST',
      headers: this.wpHeaders(),
      body: JSON.stringify({
        item_key: itemKey,
        language: this.getLanguageParam(),
      }),
    });

    const data = await response.json() as WooCartEnvelope;
    if (!response.ok || !data.success) {
      throw new Error(normalizeApiErrorMessage(data.message) || t('Failed to remove item from cart'));
    }

    return {
      cart: normalizeWooCart(data.data?.cart ?? null),
      message: data.data?.message,
    };
  }

  async applyCoupon(code: string): Promise<{ cart: WooCart | null; message?: string; discountDisplay?: string }> {
    const response = await fetch(`${this.baseUrl}woocommerce/cart/apply-coupon`, {
      method: 'POST',
      headers: this.wpHeaders(),
      body: JSON.stringify({
        code,
        language: this.getLanguageParam(),
      }),
    });

    const data = await response.json() as WooCartEnvelope & {
      data?: {
        cart?: WooCart | null;
        message?: string;
        discount_display?: string;
      };
    };

    if (!response.ok || !data.success) {
      throw new Error(normalizeApiErrorMessage(data.message) || t('Failed to apply coupon'));
    }

    return {
      cart: normalizeWooCart(data.data?.cart ?? null),
      message: data.data?.message,
      discountDisplay: data.data?.discount_display,
    };
  }

  async removeCoupon(code: string): Promise<{ cart: WooCart | null; message?: string }> {
    const response = await fetch(`${this.baseUrl}woocommerce/cart/remove-coupon`, {
      method: 'POST',
      headers: this.wpHeaders(),
      body: JSON.stringify({
        code,
        language: this.getLanguageParam(),
      }),
    });

    const data = await response.json() as WooCartEnvelope;
    if (!response.ok || !data.success) {
      throw new Error(normalizeApiErrorMessage(data.message) || t('Failed to remove coupon'));
    }

    return {
      cart: normalizeWooCart(data.data?.cart ?? null),
      message: data.data?.message,
    };
  }

  async addBundleToCart(payload: {
    bundle_id: number;
    bundle_items: unknown[];
    discount_percent?: number;
    coupon_code?: string;
    language: string;
  }): Promise<BundleAddToCartResponse> {
    const response = await fetch(`${this.baseUrl}widgets/bundle-builder/add-to-cart`, {
      method: 'POST',
      headers: this.wpHeaders(),
      body: JSON.stringify(payload),
    });

    const data = await response.json() as WooCartEnvelope & {
      data?: {
        cart?: WooCart | null;
        message?: string;
        cart_count?: number;
        coupon_applied?: boolean;
        coupon_code?: string;
      };
    };

    if (!response.ok || !data.success) {
      throw new Error(normalizeApiErrorMessage(data.message) || t('Failed to add to cart'));
    }

    const rawCart = data.data?.cart ?? null;
    const normalizedCart = normalizeWooCart(rawCart);
    console.info('[BoostBot][Cart] addBundleToCart raw extraction:', {
      hasDataData: !!data.data,
      hasRawCart: !!rawCart,
      rawCartType: rawCart === null ? 'null' : typeof rawCart,
      rawCartKeys: rawCart && typeof rawCart === 'object' ? Object.keys(rawCart) : 'N/A',
      normalizedCartExists: !!normalizedCart,
      normalizedItemCount: normalizedCart?.items?.length ?? 'NULL',
    });

    return {
      cart: normalizedCart,
      message: data.data?.message,
      cartCount: typeof data.data?.cart_count === 'number' ? data.data.cart_count : undefined,
      couponApplied: data.data?.coupon_applied === true,
      couponCode: data.data?.coupon_code,
    };
  }
  
  
  // Archived: submitLead(), submitFormWidget(), sendFeedback() — see _archive/unused-api-functions/
  // Archived: sendMessage(), sendMessageStreaming(), retrieveEvidence(),
  //           streamFromBackend(), analyzeForWidget(), sendMessageDirect() — see _archive/legacy-chat-api-functions/

  /**
   * Get or refresh session token for direct backend streaming.
   * Token is cached and auto-refreshed when expired.
   */
  async getSessionToken(visitorId: string): Promise<SessionTokenResponse> {
    // Return cached token if still valid (with 30s buffer)
    const now = Math.floor(Date.now() / 1000);
    if (this.sessionToken && this.sessionExpires > now + 30) {
      return {
        token: this.sessionToken,
        expires_at: this.sessionExpires,
        stream_url: this.streamUrl,
        credits_ok: true,
      };
    }
    
    const response = await fetch(this.baseUrl + 'session/token', {
      method: 'POST',
      headers: this.wpHeaders(),
      body: JSON.stringify({ visitor_id: visitorId })
    });
    
    const data = await response.json() as { success: boolean; data: SessionTokenResponse; message?: string };
    
    if (!response.ok || !data.success) {
      throw new Error(normalizeApiErrorMessage(data.message) || 'Failed to get session token');
    }
    
    // Cache the token
    this.sessionToken = data.data.token;
    this.sessionExpires = data.data.expires_at;
    this.streamUrl = data.data.stream_url;
    
    return data.data;
  }
  
  
  /**
   * Complete a conversation after streaming (save to WordPress).
   * Called asynchronously after streaming completes.
   */
  async completeConversation(data: {
    conversation_id: string;
    visitor_id: string;
    user_message: string;
    bot_response: string;
    bot_name?: string;
    bot_id?: number | null;
    context?: {
      id?: number;
      url?: string;
      title?: string;
    };
  }): Promise<{ saved: boolean; message_count: number }> {
    const MAX_RETRIES = 2;
    let lastError: unknown;

    for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
      try {
        if (attempt > 0) {
          // Exponential backoff: 500ms, 1000ms
          await new Promise(r => setTimeout(r, 500 * attempt));
          console.debug(`[BoostBot] Retrying completeConversation (attempt ${attempt + 1}/${MAX_RETRIES + 1})`);
        }

        const response = await fetch(this.baseUrl + 'session/complete', {
          method: 'POST',
          headers: this.wpHeaders(),
          body: JSON.stringify(data)
        });

        const result = await response.json() as { success: boolean; data: { saved: boolean; message_count: number }; message?: string };

        if (!response.ok || !result.success) {
          console.warn('[BoostBot] Failed to complete conversation:', result.message);
          lastError = new Error(result.message || 'Save failed');
          continue; // Retry
        }

        return result.data;
      } catch (err) {
        lastError = err;
        console.warn(`[BoostBot] completeConversation attempt ${attempt + 1} failed:`, err);
      }
    }

    console.error('[BoostBot] completeConversation failed after all retries:', lastError);
    return { saved: false, message_count: 0 };
  }
  

  /**
   * Send message using Gemini-orchestrated flow (two-phase progressive responses).
   * 
   * Flow:
   * 1. Get session token from WordPress
   * 2. Retrieve two-tier chunks from WordPress
   * 3. Call Gemini orchestrator (decides what to send GPT)
   * 4. Stream quick response from GPT
   * 5. If needs_followup: Show thinking indicator, call orchestrator again, stream detailed response
   * 
   * @param request - The message request
   * @param onChunk - Callback for each content chunk
   * @param onComplete - Callback when complete
   * @param onError - Callback on error
   * @param onThinking - Callback for thinking indicator
   */
  /**
   * Browser-side smart routing: detect simple patterns to skip expensive calls.
   * Returns routing hints based on message analysis.
   */

  async sendMessageOrchestrated(
    request: SendMessageRequest,
    onChunk: (content: string, accumulated: string) => void,
    onComplete: (response: SendMessageResponse) => void,
    onError: (error: Error) => void,
    _onThinking?: (message: string | null) => void // Kept for API compat, ProactiveEngine handles thinking
    // Note: Proactive engagement is now handled by ProactiveTimerManager (centralized)
  ): Promise<void> {
    const visitorId = request.visitor_id;
    const t0 = Date.now();
    const timings: Record<string, number> = {};
    
    console.log('%c[BoostBot][Orchestrated] === FLOW START ===', 'color: #3BC2D0; font-weight: bold');
    console.log('[BoostBot][Orchestrated] Message:', request.message.slice(0, 50) + (request.message.length > 50 ? '...' : ''));
    
    try {
      // Phase 1: Get session token (always needed)
      const session = await this.getSessionToken(visitorId);
      timings.session = Date.now() - t0;
      console.log(`[BoostBot][Orchestrated] Phase 1 - Session token: ${timings.session}ms`);
      
      if (!session.credits_ok) {
        console.error('[BoostBot][Orchestrated] No credits available!');
        throw new Error('No credits available');
      }

      // Phase 2: Retrieve two-tier chunks from WordPress
      const t1 = Date.now();
      const retrieval = await this.retrieveEvidenceOrchestrated({
        message: request.message,
        conversation_id: request.conversation_id ?? undefined,
        visitor_id: visitorId,
        protocol_version: request.protocol_version,
        context: request.context,
        cart_context: request.cart_context,
        history: request.history,
        ai_instructions: request.ai_instructions,
      });
      timings.retrieval = Date.now() - t1;
      console.log(`[BoostBot][Orchestrated] Phase 2 - Retrieval: ${timings.retrieval}ms`);
      console.log('[BoostBot][Orchestrated] Chunks:', {
        small: retrieval.small_chunks?.length || 0,
        large: retrieval.large_chunks?.length || 0,
        outputLimit: retrieval.bot_config.output_limit,
        shownWidgets: retrieval.shown_widgets_in_conversation,
      });

      // Phase 3: Unified respond (orchestration + streaming in one call)
      const t2 = Date.now();
      const idempotencyKey = `${retrieval.conversation_id}:${Date.now()}`;
      let fullContent = '';
      const widgets: Array<{ type: string; id: string | number; config: Record<string, unknown> }> = [];
      let meta: UnifiedRespondMeta | null = null;

      const MAX_MESSAGES = 10;
      const cappedMessages = retrieval.messages?.length > MAX_MESSAGES
        ? retrieval.messages.slice(-MAX_MESSAGES)
        : retrieval.messages;

      // Widget fetch promise - started as soon as meta arrives, resolved before onComplete
      let widgetFetchPromise: Promise<void | void[]> | null = null;

      let streamErrored = false;
      await this.streamFromUnifiedEndpoint(
        session,
        {
          message: request.message,
          history: retrieval.history,
          small_chunks: retrieval.small_chunks,
          large_chunks: retrieval.large_chunks,
          page_content: retrieval.page_content,
          bot_config: retrieval.bot_config,
          shown_widgets: retrieval.shown_widgets_in_conversation,
          shown_widget_ids: retrieval.shown_widget_ids_in_conversation,
          phase: 'initial',
          conversation_id: retrieval.conversation_id,
          idempotency_key: idempotencyKey,
          protocol_version: request.protocol_version,
          cart_context: request.cart_context,
          messages: cappedMessages,
        },
        (metaEvent) => {
          meta = metaEvent;
          timings.orchestration = Date.now() - t2;
          console.log(`[BoostBot][Orchestrated] Meta received in ${timings.orchestration}ms:`, {
            action: meta.action,
            widget: meta.widget?.type || 'none',
            followup: meta.needs_followup,
          });

          // Collect all widgets (primary + extras) and fetch in parallel with streaming
          if (meta.action === 'RESPOND_WITH_WIDGET') {
            const allWidgetRefs = [
              ...(meta.widget ? [meta.widget] : []),
              ...(meta.widgets || []),
            ].filter(w => w.type && w.id);

            if (allWidgetRefs.length > 0) {
              widgetFetchPromise = Promise.all(
                allWidgetRefs.map(wRef =>
                  this.fetchWidgetById(
                    wRef.id,
                    wRef.type,
                    wRef.prefill,
                    retrieval.bot_config.widget_assignments
                  ).then(widgetData => {
                    if (widgetData) {
                      widgets.push(widgetData);
                      console.debug('[BoostBot][Orchestrated] Widget fetched:', widgetData.type, widgetData.id);
                    } else {
                      console.warn('[BoostBot][Orchestrated] Widget rejected:', wRef);
                    }
                  }).catch(err => {
                    console.warn('[BoostBot][Orchestrated] Widget fetch failed:', err);
                  })
                )
              );
            }
          }
        },
        onChunk,
        (content) => {
          fullContent = content;
          console.debug('[BoostBot][Orchestrated] Stream done in', Date.now() - t2, 'ms');
        },
        (error) => {
          streamErrored = true;
          onError(error);
        }
      );

      if (streamErrored) {
        console.warn('[BoostBot][Orchestrated] Stream failed — skipping onComplete');
        return;
      }

      // Handle WAIT_FOR_USER (meta arrived but no content streamed)
      if (meta && (meta as UnifiedRespondMeta).action === 'WAIT_FOR_USER') {
        onComplete({
          success: true,
          data: { reply: '', conversation_id: retrieval.conversation_id, widgets: [], suggestions: [] },
        });
        return;
      }

      // Wait for widget fetch to complete
      if (widgetFetchPromise) {
        await widgetFetchPromise;
      }

      // Save conversation
      try {
        const saveTimeout = new Promise<void>((_, reject) =>
          setTimeout(() => reject(new Error('completeConversation timeout')), 5000)
        );
        await Promise.race([
          this.completeConversation({
            conversation_id: retrieval.conversation_id,
            visitor_id: visitorId,
            user_message: request.message,
            bot_response: fullContent,
            bot_name: retrieval.bot_config?.name,
            bot_id: request.bot_id ?? undefined,
            context: request.context,
          }),
          saveTimeout,
        ]);
      } catch (err) {
        console.warn('[BoostBot] Conversation save did not complete in time:', err);
      }

      timings.total = Date.now() - t0;

      console.log('%c[BoostBot][Orchestrated] === FLOW COMPLETE ===', 'color: #3BC2D0; font-weight: bold');
      console.log('[BoostBot][Orchestrated] Timings:', timings);
      console.log('[BoostBot][Orchestrated] Response length:', fullContent.length, 'chars');
      console.log('[BoostBot][Orchestrated] Widgets:', widgets.map(w => w.type));

      try {
        const debugLogs = JSON.parse(sessionStorage.getItem('boostbot_chat_logs') || '[]');
        debugLogs.push({
          timestamp: new Date().toISOString(),
          message: request.message.slice(0, 50),
          timings,
          responseLen: fullContent.length,
          widgets: widgets.map(w => w.type),
        });
        if (debugLogs.length > 50) debugLogs.shift();
        sessionStorage.setItem('boostbot_chat_logs', JSON.stringify(debugLogs));
      } catch { /* ignore */ }

      onComplete({
        success: true,
        data: {
          reply: fullContent,
          conversation_id: retrieval.conversation_id,
          widgets,
          suggestions: [],
          bot_config: retrieval.bot_config,
        },
      });

    } catch (error) {
      const totalMs = Date.now() - t0;
      console.error('%c[BoostBot][Orchestrated] === ERROR ===', 'color: #ff4444; font-weight: bold');
      console.error('[BoostBot][Orchestrated] Error after', totalMs, 'ms:', error instanceof Error ? error.message : String(error));
      // Per-visitor rate limiting: do NOT route through the generic onError (which just
      // renders a generic failure). Rethrow so the outer chatStore catch can apply the
      // cooldown and render the friendly rate-limit reply.
      if (error instanceof RateLimitError) {
        throw error;
      }
      onError(error instanceof Error ? error : new Error(String(error)));
    }
  }

  /**
   * Retrieve evidence using orchestrated endpoint (two-tier chunks)
   */
  private async retrieveEvidenceOrchestrated(params: {
    message: string;
    conversation_id?: string;
    visitor_id?: string;
    protocol_version?: string;
    context?: Record<string, unknown>;
    cart_context?: CommerceContextLite;
    history?: Array<{ role: string; content: string }>;
    ai_instructions?: string;
  }): Promise<OrchestratedRetrievalResponse> {
    const response = await fetch(this.baseUrl + 'chat/retrieve-orchestrated', {
      method: 'POST',
      headers: this.wpHeaders(),
      body: JSON.stringify(params),
    });

    const data = await response.json();
    
    if (!response.ok || !data.success) {
      throw new Error(data.message || 'Failed to retrieve orchestrated evidence');
    }

    // Per-visitor message rate limiting (Week 33)
    if (data.data?.rate_limited) {
      throw new RateLimitError(data.data as { reply: string; limit_type?: string; retry_after?: number; cooldown_seconds?: number; remaining_hour?: number; remaining_day?: number });
    }

    return data.data;
  }

  // streamFromGptExecutor() removed — the /chat/execute endpoint is deprecated.
  // All streaming now goes through streamFromUnifiedEndpoint() → /chat/respond.

  /**
   * Stream response from unified /chat/respond endpoint.
   * Handles both `meta` events (orchestration decision) and `content` events (response text).
   */
  private async streamFromUnifiedEndpoint(
    session: SessionTokenResponse,
    params: {
      message: string;
      history: Array<{ role: string; content: string }>;
      small_chunks: string;
      large_chunks: string;
      page_content: string;
      bot_config: Record<string, unknown>;
      shown_widgets: string[];
      shown_widget_ids?: number[];
      phase: string;
      conversation_id?: string;
      idempotency_key: string;
      protocol_version?: string;
      cart_context?: CommerceContextLite;
      messages?: Array<{ role: string; content: string }>;
      previous_response?: string;
      proactive_context?: { time_since_last_message_ms: number; was_user_typing: boolean; last_messages_from_bot: number };
      page_transition_context?: {
        previous_bot_id: number;
        new_bot_id: number;
        previous_page_url: string;
        current_page_url: string;
        trigger_reason?: 'bot_change' | 'search_redirect';
        search_query?: string;
      };
    },
    onMeta: (meta: UnifiedRespondMeta) => void,
    onChunk: (content: string, accumulated: string) => void,
    onDone: (fullContent: string) => void,
    onError: (error: Error) => void,
    abortSignal?: AbortSignal
  ): Promise<void> {
    const respondUrl = session.stream_url.replace('/chat/stream', '/chat/respond');
    const MAX_RETRIES = 2;

    let response: Response | undefined;
    for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
      try {
        response = await fetch(respondUrl, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${session.token}`,
            'Accept': 'text/event-stream',
          },
          body: JSON.stringify(params),
          signal: abortSignal,
        });

        if (response.status === 429 && attempt < MAX_RETRIES) {
          const wait = 1000 * (attempt + 1);
          console.warn(`[BoostBot] Respond 429, retrying in ${wait}ms (attempt ${attempt + 1}/${MAX_RETRIES})`);
          await new Promise(r => setTimeout(r, wait));
          continue;
        }
        break;
      } catch (fetchErr) {
        if (attempt < MAX_RETRIES && !(fetchErr instanceof DOMException)) {
          const wait = 1000 * (attempt + 1);
          console.warn(`[BoostBot] Respond fetch failed, retrying in ${wait}ms`, fetchErr);
          await new Promise(r => setTimeout(r, wait));
          continue;
        }
        throw fetchErr;
      }
    }

    try {
      if (!response || !response.ok) {
        // If the response is JSON (WAIT_FOR_USER), parse it instead
        if (response && response.headers.get('content-type')?.includes('application/json')) {
          const data = await response.json();
          if (data.success && data.data?.action === 'WAIT_FOR_USER') {
            onMeta(normalizeMetaEvent({
              ...data.data,
              type: 'meta',
              action: 'WAIT_FOR_USER',
              widget: null,
              needs_followup: false,
              followup_reason: null,
              thinking_message: null,
            }));
            onDone('');
            return;
          }
        }
        const errorData = response ? await response.json().catch(() => ({})) : {};
        throw new Error((errorData as { error?: { message?: string } }).error?.message || 'Respond failed');
      }

      // Check if response is JSON (WAIT_FOR_USER case)
      const contentType = response.headers.get('content-type') || '';
      if (contentType.includes('application/json')) {
        const data = await response.json();
        if (data.success && data.data?.action === 'WAIT_FOR_USER') {
          onMeta(normalizeMetaEvent({
            ...data.data,
            type: 'meta',
            action: 'WAIT_FOR_USER',
            widget: null,
            needs_followup: false,
            followup_reason: null,
            thinking_message: null,
          }));
          onDone('');
          return;
        }
      }

      const reader = response.body?.getReader();
      if (!reader) throw new Error('No response body for streaming');

      const decoder = new TextDecoder();
      let accumulated = '';
      let chunkCount = 0;
      let metaReceived = false;
      let sseBuffer = '';
      const t0 = Date.now();

      const logSSE = (msg: string) => {
        try {
          const logs = JSON.parse(sessionStorage.getItem('boostbot_sse_debug') || '[]');
          logs.push({ t: Date.now() - t0, msg });
          if (logs.length > 100) logs.shift();
          sessionStorage.setItem('boostbot_sse_debug', JSON.stringify(logs));
        } catch { /* ignore */ }
      };

      const processLine = (line: string): boolean => {
        if (!line.startsWith('data: ')) return false;
        const data = line.slice(6).trim();

        if (data === '[DONE]') {
          logSSE(`[DONE]: ${accumulated.length}chars in ${chunkCount} chunks`);
          onDone(accumulated);
          return true;
        }

        try {
          const json = JSON.parse(data);

          if (json.type === 'meta' && !metaReceived) {
            metaReceived = true;
            const normalizedMeta = normalizeMetaEvent(json);
            logSSE(`Meta: action=${normalizedMeta.action} widget=${normalizedMeta.widget?.type || 'none'} protocol=${normalizedMeta.protocol_version || 'legacy'}`);
            onMeta(normalizedMeta);
            return false;
          }

          if (json.content) {
            accumulated += json.content;
            chunkCount++;
            if (chunkCount % 10 === 0) {
              logSSE(`Chunk #${chunkCount}: ${accumulated.length}chars`);
            }
            onChunk(json.content, accumulated);
          }
        } catch {
          // Ignore parse errors
        }
        return false;
      };

      logSSE('Starting unified stream...');

      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          if (sseBuffer.trim()) {
            processLine(sseBuffer);
          }
          logSSE(`Stream ended: ${chunkCount} chunks, ${accumulated.length}chars`);
          break;
        }

        sseBuffer += decoder.decode(value, { stream: true });
        const lines = sseBuffer.split('\n');
        sseBuffer = lines.pop() || '';

        for (const line of lines) {
          if (processLine(line)) return;
        }
      }

      logSSE(`Loop ended without [DONE]: ${accumulated.length}chars`);
      onDone(accumulated);
    } catch (error) {
      if (error instanceof Error && error.name === 'AbortError') {
        console.log('[BoostBot] Unified stream aborted');
        return;
      }
      onError(error instanceof Error ? error : new Error(String(error)));
    }
  }

  /**
   * Fetch widget by ID from WordPress API.
   * Falls back to default config if API call fails.
   *
   * @param allowedAssignments - If provided, widget must match an assignment (by ID or type).
   *   This prevents rendering widgets not assigned to the current bot.
   */
  private static readonly WIDGET_TYPE_ALIASES: Record<string, string> = {
    lead_form: 'form',
    lead_capture: 'form',
    contact_form: 'form',
    dynamic_form: 'form',
    calendar: 'calendar_meeting',
  };

  private normalizeWidgetType(type: string): string {
    return ChatApiClient.WIDGET_TYPE_ALIASES[type] || type;
  }

  /**
   * Fetch product data from WooCommerce
   */
  private normalizeProductIdInput(productId: number | string): number | null {
    if (typeof productId === 'number' && Number.isFinite(productId) && productId > 0) {
      return Math.trunc(productId)
    }

    if (typeof productId === 'string') {
      const match = productId.match(/\d+/)
      if (match) {
        const parsed = Number(match[0])
        if (Number.isFinite(parsed) && parsed > 0) {
          return Math.trunc(parsed)
        }
      }
    }

    return null
  }

  private async fetchProductData(productId: number | string): Promise<Record<string, unknown> | null> {
    const normalizedProductId = this.normalizeProductIdInput(productId)
    if (!normalizedProductId) {
      return null
    }

    try {
      const response = await fetch(`${this.baseUrl}woocommerce/products/${normalizedProductId}`, {
        method: 'GET',
        headers: this.wpHeaders(),
      });
      if (!response.ok) return null;
      const data = await response.json();
      if (data?.success) {
        return data.data?.product || data.data || null;
      }
      return data?.product || data || null;
    } catch (error) {
      console.warn('[BoostBot] Failed to fetch product data:', productId, error);
      return null;
    }
  }

  private applyWidgetPrefill(
    widgetType: string,
    config: Record<string, unknown>,
    prefill?: Record<string, string>
  ): void {
    if (!prefill || Object.keys(prefill).length === 0) {
      return
    }

    config.initial_values = prefill

    const canonicalType = this.normalizeWidgetType(widgetType)
    if (canonicalType !== 'bundle_builder') {
      return
    }

    const rawBudget = prefill.budget || prefill.max_price || prefill.price_range
    const budget = rawBudget ? Number(rawBudget) : NaN
    if (Number.isFinite(budget) && budget > 0) {
      config.budget = budget
    }

    if (prefill.category) {
      config.category = prefill.category
    }

    const preferences = prefill.preferences || prefill.preference
    if (preferences) {
      config.preferences = preferences
    }
  }

  private async fetchWidgetById(
    widgetId: number | undefined, 
    widgetType: string,
    prefill?: Record<string, string>,
    allowedAssignments?: Array<{ type: string; id?: number }>
  ): Promise<{ type: string; id: string | number; config: Record<string, unknown> } | null> {
    const canonicalType = this.normalizeWidgetType(widgetType);

    // Client-side guard: reject widgets not in the bot's assignment list
    if (allowedAssignments && allowedAssignments.length > 0) {
      const matchById = widgetId != null && widgetId > 0 && allowedAssignments.some(a => a.id === widgetId);
      const matchByType = allowedAssignments.some(a => a.type === canonicalType || a.type === widgetType);
      if (!matchById && !matchByType) {
        console.warn(`[BoostBot] Widget guard: rejected type="${widgetType}"(canonical="${canonicalType}") id=${widgetId} — not in bot assignments`);
        return null;
      }
    }

    // If we have a specific widget ID, fetch it from the API
    if (widgetId && widgetId > 0) {
      try {
        const params = new URLSearchParams();
        const activeBotId = window.boostbot_config?.bot_id;
        const language = this.getLanguageParam();
        if (activeBotId) {
          params.set('bot_id', String(activeBotId));
        }
        if (language) {
          params.set('language', language);
        }

        const widgetUrl = `${this.baseUrl}widgets/${widgetId}/public${params.toString() ? `?${params.toString()}` : ''}`;

        const response = await fetch(widgetUrl, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
          },
        });

        if (response.ok) {
          const data = await response.json();
          if (data.success && data.data) {
            // API returns { widget: { id, name, type, config, fields? } }
            const widgetData = data.data.widget || data.data;
            console.debug('[BoostBot] Fetched widget by ID:', widgetId, widgetData);
            
            // Build config with fields for form widgets
            const config: Record<string, unknown> = widgetData.config || {};
            if (widgetData.fields && widgetData.fields.length > 0) {
              config.fields = widgetData.fields;
            }
            if (widgetData.name) {
              config.title = config.title || widgetData.name;
            }

            // Merge prefill data from Gemini orchestration (for form widgets with phone pre-fill)
            if (prefill && Object.keys(prefill).length > 0) {
              this.applyWidgetPrefill(canonicalType, config, prefill)
              console.debug('[BoostBot] Added prefill to widget config:', prefill);

              // Handle product_card product_id pre-fill
              if (canonicalType === 'product_card' && prefill.product_id) {
                const productData = await this.fetchProductData(prefill.product_id);
                if (productData) {
                  const normalizedProductId = this.normalizeProductIdInput(prefill.product_id)
                  config.product = productData;
                  config.product_id = normalizedProductId ?? prefill.product_id;
                } else {
                  console.warn('[BoostBot] Product data fetch returned null for ID:', prefill.product_id);
                  // If product fetch fails, we skip this widget by returning null
                  return null;
                }
              }
            }
            
            return {
              type: widgetData.type || widgetType,
              id: widgetData.id || widgetId,
              config,
            };
          }
        }
      } catch (error) {
        console.warn('[BoostBot] Failed to fetch widget by ID:', widgetId, error);
      }
    }

    // Fall back to default config
    const defaultConfig = this.getDefaultWidgetConfig(widgetType);

    // If it's a product_card and has prefill, we still try to fetch the product
    if (canonicalType === 'product_card' && prefill?.product_id) {
      const productData = await this.fetchProductData(prefill.product_id);
      if (productData) {
        const normalizedProductId = this.normalizeProductIdInput(prefill.product_id)
        defaultConfig.product = productData;
        defaultConfig.product_id = normalizedProductId ?? prefill.product_id;
      } else {
        return null; // Reject if product not found
      }
    }

    this.applyWidgetPrefill(canonicalType, defaultConfig, prefill)

    if (Object.keys(defaultConfig).length > 0) {
      return {
        type: widgetType,
        id: `default_${widgetType}`,
        config: defaultConfig,
      };
    }

    return null;
  }

  private getDefaultWidgetConfig(widgetType: string): Record<string, unknown> {
    const canonical = this.normalizeWidgetType(widgetType);
    switch (canonical) {
      case 'form':
        return {
          title: t('Leave your details'),
          description: t('We will get back to you shortly'),
          fields: [
            { name: 'name', label: t('Name'), type: 'text', required: true },
            { name: 'email', label: t('Email'), type: 'email', required: true },
            { name: 'phone', label: t('Phone'), type: 'tel', required: false },
            { name: 'message', label: t('Message'), type: 'textarea', required: false },
          ],
          submit_text: t('Send'),
        };
      case 'contact_card': {
        return {
          contact: {
            name: t('Contact Us'),
            title: '',
            phone: '',
            email: '',
          },
          show_call_button: true,
          show_message_button: true,
          is_rtl: window.boostbot_config?.is_rtl ?? true,
        };
      }
      case 'calendar_meeting':
      case 'calendar_call':
        return { title: t('Book an Appointment'), is_rtl: window.boostbot_config?.is_rtl ?? true };
      case 'whatsapp': {
        return {
          phone_number: '',
          message: t('Hello, I would like to receive more information'),
          button_text: t('Send WhatsApp Message'),
          is_rtl: window.boostbot_config?.is_rtl ?? true,
        };
      }
      case 'gallery':
        return { images: [], title: t('Gallery'), is_rtl: window.boostbot_config?.is_rtl ?? true };
      case 'youtube':
        return { video_url: '', title: t('Watch Video'), is_rtl: window.boostbot_config?.is_rtl ?? true };
      case 'survey':
        return { title: t('We would love to hear from you'), is_rtl: window.boostbot_config?.is_rtl ?? true };
      case 'product_card':
        return {
          product_id: null,
          show_add_to_cart: true,
          show_price: true,
          show_rating: true,
          is_rtl: window.boostbot_config?.is_rtl ?? true,
        };
      case 'bundle_builder':
        return {
          allow_customization: true,
          is_rtl: window.boostbot_config?.is_rtl ?? true,
        };
      case 'coupon':
        return {
          coupons: [],
          is_rtl: window.boostbot_config?.is_rtl ?? true,
        };
      default:
        return {};
    }
  }

  /**
   * Proactive engagement - called by ProactiveEngine after user idle time.
   *
   * Uses the unified /chat/respond endpoint (phase: 'proactive') which handles
   * both orchestration and streaming in a single call. For WAIT_FOR_USER the
   * endpoint returns lightweight JSON; for RESPOND/RESPOND_WITH_WIDGET it
   * streams SSE. Streaming is handled internally via the provided callbacks.
   *
   * @returns Action summary (widget/wait/respond). For RESPOND, streaming
   *          already happened through the callbacks — no gpt_payload is returned.
   */
  async proactiveEngage(params: {
    visitorId: string;
    conversationId: string;
    lastBotResponse: string;
    history: Array<{ role: string; content: string }>;
    shownWidgetIds: number[];
    timeSinceLastMessage: number;
    wasUserTyping: boolean;
    botConfig: {
      name: string;
      personality?: string;
      output_limit: number;
      enabled_widgets: string[];
      language?: string;
      widget_assignments?: Array<{ type: string; id?: number }>;
    };
    onStreamStart?: () => void;
    onChunk?: (chunk: string, accumulated: string) => void;
    onStreamComplete?: (content: string) => void;
    onStreamError?: (error: Error) => void;
    abortSignal?: AbortSignal;
  }): Promise<{
    action: 'ADD_INFO' | 'SHOW_WIDGET' | 'ASK_FOLLOWUP' | 'WAIT' | 'RESPOND';
    content?: string;
    widget?: { type: string; id: string | number; config: Record<string, unknown> };
  } | null> {
    console.log('[BoostBot][Proactive] Engagement triggered after', params.timeSinceLastMessage, 'ms idle');

    if (!params.visitorId || !params.conversationId) {
      console.warn('[BoostBot][Proactive] Missing visitorId or conversationId - skipping');
      return null;
    }
    if (!params.botConfig?.name || !params.botConfig?.enabled_widgets) {
      console.warn('[BoostBot][Proactive] Incomplete botConfig - skipping');
      return null;
    }
    if (!params.history || params.history.length === 0) {
      console.warn('[BoostBot][Proactive] No history - skipping');
      return null;
    }

    try {
      const session = await this.getSessionToken(params.visitorId);
      if (!session.credits_ok) {
        console.warn('[BoostBot][Proactive] No credits - skipping');
        return null;
      }

      const fullBotConfig = {
        name: params.botConfig.name,
        personality: params.botConfig.personality || '',
        output_limit: params.botConfig.output_limit || 800,
        enabled_widgets: params.botConfig.enabled_widgets || [],
        language: params.botConfig.language || 'he',
        behavior: { proactive_engagement: true },
        widget_assignments: params.botConfig.widget_assignments,
      };

      const MAX_MESSAGES = 10;
      const cappedMessages = params.history.length > MAX_MESSAGES
        ? params.history.slice(-MAX_MESSAGES)
        : params.history;

      const idempotencyKey = `${params.conversationId}:proactive:${Date.now()}`;
      let resultAction: string = 'WAIT_FOR_USER';
      let resultWidget: { type: string; id: string | number; config: Record<string, unknown> } | undefined;
      let widgetFetchPromise: Promise<void> | null = null;
      let streamErrored = false;

      await this.streamFromUnifiedEndpoint(
        session,
        {
          message: '[PROACTIVE_TRIGGER]',
          history: params.history,
          small_chunks: '(proactive phase - no retrieval needed)',
          large_chunks: '(proactive phase - context from history)',
          page_content: '',
          bot_config: fullBotConfig,
          shown_widgets: [],
          shown_widget_ids: params.shownWidgetIds,
          phase: 'proactive',
          previous_response: params.lastBotResponse,
          conversation_id: params.conversationId,
          idempotency_key: idempotencyKey,
          protocol_version: '2.0',
          messages: cappedMessages,
          proactive_context: {
            time_since_last_message_ms: params.timeSinceLastMessage,
            was_user_typing: params.wasUserTyping,
            last_messages_from_bot: this.countLastBotMessages(params.history),
          },
        },
        (meta) => {
          resultAction = meta.action;
          console.log('[BoostBot][Proactive] Decision (via respond):', meta.action);

          if (meta.action === 'WAIT_FOR_USER') return;

          params.onStreamStart?.();

          if (meta.action === 'RESPOND_WITH_WIDGET') {
            const allWidgetRefs = [
              ...(meta.widget ? [meta.widget] : []),
              ...(meta.widgets || []),
            ].filter(w => w.type && w.id);
            if (allWidgetRefs.length > 0) {
              widgetFetchPromise = this.fetchWidgetById(
                allWidgetRefs[0].id,
                allWidgetRefs[0].type,
                allWidgetRefs[0].prefill,
                fullBotConfig.widget_assignments,
              ).then(wd => { if (wd) resultWidget = wd; })
               .catch(err => console.warn('[BoostBot][Proactive] Widget fetch failed:', err));
            }
          }
        },
        (chunk, accumulated) => { params.onChunk?.(chunk, accumulated); },
        (content) => { params.onStreamComplete?.(content); },
        (error) => { streamErrored = true; params.onStreamError?.(error); },
        params.abortSignal,
      );

      if (streamErrored) return null;

      if (widgetFetchPromise) await widgetFetchPromise;

      if (resultAction === 'WAIT_FOR_USER') return { action: 'WAIT' };

      if (resultAction === 'RESPOND_WITH_WIDGET' && resultWidget) {
        return { action: 'SHOW_WIDGET', widget: resultWidget };
      }

      if (resultAction === 'RESPOND') return { action: 'RESPOND' };

      return { action: 'WAIT' };
    } catch (error) {
      if (error instanceof Error && error.name === 'AbortError') {
        console.log('[BoostBot][Proactive] Aborted (tokens saved!)');
        return null;
      }
      console.error('[BoostBot][Proactive] Error:', error);
      return null;
    }
  }

  /**
   * Count how many of the last messages are from the bot
   */
  private countLastBotMessages(history: Array<{ role: string; content: string }>): number {
    let count = 0;
    for (let i = history.length - 1; i >= 0; i--) {
      if (history[i].role === 'assistant') {
        count++;
      } else {
        break;
      }
    }
    return count;
  }

  /**
   * Fire a page transition trigger when user navigates to a page with a different bot.
   * Uses the unified /chat/respond endpoint (phase: 'page_transition').
   * Streaming is handled internally via the provided callbacks.
   */
  async pageTransitionEngage(params: {
    visitorId: string;
    conversationId?: string | null;
    history: Array<{ role: string; content: string }>;
    lastBotResponse: string;
    shownWidgetIds: number[];
    pageTransitionContext: {
      previousBotId: number;
      newBotId: number;
      previousPageUrl: string;
      currentPageUrl: string;
      triggerReason?: 'bot_change' | 'search_redirect';
      searchQuery?: string;
    };
    botConfig: {
      name: string;
      personality: string;
      output_limit: number;
      enabled_widgets: number[];
      language: string;
      widget_assignments?: Array<{ type: string; id?: number }>;
    };
    onMetaReceived?: () => void;
    onStreamStart?: () => void;
    onChunk?: (chunk: string, accumulated: string) => void;
    onStreamComplete?: (content: string) => void;
    onStreamError?: (error: Error) => void;
    abortSignal?: AbortSignal;
  }): Promise<ProactiveAction | null> {
    console.log('[BoostBot][PageTransition] Engaging with context:', params.pageTransitionContext);

    const hasSearchRedirectIntent =
      params.pageTransitionContext.triggerReason === 'search_redirect'
      && !!params.pageTransitionContext.searchQuery?.trim();

    if ((!params.history || params.history.length === 0) && !hasSearchRedirectIntent) {
      console.warn('[BoostBot][PageTransition] No history - skipping');
      return null;
    }

    try {
      const session = await this.getSessionToken(params.visitorId);
      if (!session.credits_ok) {
        console.warn('[BoostBot][PageTransition] No credits - skipping');
        return null;
      }

      const fullBotConfig = {
        name: params.botConfig.name,
        personality: params.botConfig.personality || '',
        output_limit: params.botConfig.output_limit || 800,
        enabled_widgets: params.botConfig.enabled_widgets || [],
        language: params.botConfig.language || 'he',
        behavior: { proactive_engagement: true },
        widget_assignments: params.botConfig.widget_assignments,
      };

      const MAX_MESSAGES = 10;
      const cappedMessages = params.history.length > MAX_MESSAGES
        ? params.history.slice(-MAX_MESSAGES)
        : params.history;

      const idempotencyKey = `${params.conversationId || params.visitorId}:page_transition:${Date.now()}`;
      let resultAction: string = 'WAIT_FOR_USER';
      let resultWidget: { type: string; id: string | number; config: Record<string, unknown> } | undefined;
      let widgetFetchPromise: Promise<void> | null = null;
      let streamErrored = false;

      await this.streamFromUnifiedEndpoint(
        session,
        {
          message: '[PAGE_TRANSITION_TRIGGER]',
          history: params.history,
          small_chunks: '(page transition - no retrieval needed)',
          large_chunks: '(page transition - context from history)',
          page_content: '',
          bot_config: fullBotConfig,
          shown_widgets: [],
          shown_widget_ids: params.shownWidgetIds,
          phase: 'page_transition',
          previous_response: params.lastBotResponse,
          conversation_id: params.conversationId || undefined,
          idempotency_key: idempotencyKey,
          protocol_version: '2.0',
          messages: cappedMessages,
          page_transition_context: {
            previous_bot_id: params.pageTransitionContext.previousBotId,
            new_bot_id: params.pageTransitionContext.newBotId,
            previous_page_url: params.pageTransitionContext.previousPageUrl,
            current_page_url: params.pageTransitionContext.currentPageUrl,
            trigger_reason: params.pageTransitionContext.triggerReason || 'bot_change',
            search_query: params.pageTransitionContext.searchQuery?.trim() || undefined,
          },
        },
        (meta) => {
          resultAction = meta.action;
          console.log('[BoostBot][PageTransition] Decision (via respond):', meta.action);
          params.onMetaReceived?.();

          if (meta.action === 'WAIT_FOR_USER') return;

          params.onStreamStart?.();

          if (meta.action === 'RESPOND_WITH_WIDGET') {
            const allWidgetRefs = [
              ...(meta.widget ? [meta.widget] : []),
              ...(meta.widgets || []),
            ].filter(w => w.type && w.id);
            if (allWidgetRefs.length > 0) {
              widgetFetchPromise = this.fetchWidgetById(
                allWidgetRefs[0].id,
                allWidgetRefs[0].type,
                allWidgetRefs[0].prefill,
                fullBotConfig.widget_assignments,
              ).then(wd => { if (wd) resultWidget = wd; })
               .catch(err => console.warn('[BoostBot][PageTransition] Widget fetch failed:', err));
            }
          }
        },
        (chunk, accumulated) => { params.onChunk?.(chunk, accumulated); },
        (content) => { params.onStreamComplete?.(content); },
        (error) => { streamErrored = true; params.onStreamError?.(error); },
        params.abortSignal,
      );

      if (streamErrored) return null;

      if (widgetFetchPromise) await widgetFetchPromise;

      if (resultAction === 'WAIT_FOR_USER') return { action: 'WAIT' };

      if (resultAction === 'RESPOND_WITH_WIDGET' && resultWidget) {
        return { action: 'SHOW_WIDGET', widget: resultWidget };
      }

      if (resultAction === 'RESPOND') return { action: 'RESPOND', gpt_payload: { context: '', instructions: '', max_response_chars: 0 } };

      return { action: 'WAIT' };
    } catch (error) {
      console.error('[BoostBot][PageTransition] Error:', error);
      return null;
    }
  }

  // streamProactiveResponse() removed — proactive/page-transition streaming
  // is now handled inside proactiveEngage() and pageTransitionEngage() via
  // the unified /chat/respond endpoint (streamFromUnifiedEndpoint).
}

// Export singleton instance
export const chatApi = new ChatApiClient();
