/**
 * ProactiveTimerManager - Centralized timer for proactive engagement
 * 
 * This manager ensures only ONE proactive timer runs at a time, preventing
 * duplicate triggers and ensuring fresh context is used when firing.
 * 
 * State Machine:
 * - FROZEN: Timer paused (user engaged, SSE active, etc.)
 * - COUNTING: Timer running down to trigger
 * - TRIGGERING: Proactive call in progress
 */

type ProactiveState = 'frozen' | 'counting' | 'triggering';

interface TriggerContext {
  conversationId: string;
  visitorId: string;
  history: Array<{ role: string; content: string }>;
  lastBotResponse: string;
  lastMessageAtMs?: number;
  shownWidgetIds: number[];
  botConfig: {
    name: string;
    personality?: string;
    output_limit: number;
    enabled_widgets: string[];
    language?: string;
    widget_assignments?: Array<{ type: string; id?: number }>;
  };
}

type TriggerCallback = (context: TriggerContext) => Promise<{
  action: string;
  gpt_payload?: { context: string; instructions: string; max_response_chars: number };
  widget?: { type: string; id: string | number; config: Record<string, unknown> };
} | null>;

type StreamProactiveCallback = (
  gptPayload: { context: string; instructions: string; max_response_chars: number },
  context: TriggerContext
) => Promise<void>;

class ProactiveTimerManager {
  private static instance: ProactiveTimerManager;
  
  private state: ProactiveState = 'frozen';
  private timerId: ReturnType<typeof setTimeout> | null = null;
  private delayMs: number = 8000; // Default 8 seconds
  private enabled: boolean = false;
  
  // AbortController for in-flight proactive requests
  // Used to cancel GPT generation when user sends a new message (saves tokens!)
  private currentAbortController: AbortController | null = null;
  
  // Callbacks set by chatStore
  private getContext: (() => TriggerContext | null) | null = null;
  private onProactiveEngage: TriggerCallback | null = null;
  private onStreamProactive: StreamProactiveCallback | null = null;
  private onShowWidget: ((widget: { type: string; id: string | number; config: Record<string, unknown> }) => void) | null = null;
  
  // Live widget check - returns true if a widget is displayed as the last message
  // This is the definitive check: if ANY widget is in front of the user, we stay frozen.
  private getHasActiveWidget: (() => boolean) | null = null;
  
  // Tracked conditions
  private inputFocused: boolean = false;
  private sseActiveCount: number = 0;
  private widgetActive: boolean = false;
  
  // User message count at trigger time (to detect when user sends a new message
  // during the proactive call). We count ONLY user messages — not total messages —
  // because the proactive flow itself adds a bot message to the store.
  private userCountAtTrigger: number = 0;
  private getUserMessageCount: (() => number) | null = null;
  
  // Track if proactive already fired for current bot response
  // Reset when user sends new message
  private triggeredForCurrentResponse: boolean = false;
  
  // Generation counter to invalidate in-flight proactive calls
  // Increments on: reset, navigation, refresh
  private proactiveGeneration: number = 0;
  
  static getInstance(): ProactiveTimerManager {
    if (!ProactiveTimerManager.instance) {
      ProactiveTimerManager.instance = new ProactiveTimerManager();
    }
    return ProactiveTimerManager.instance;
  }
  
  // Configuration
  setEnabled(enabled: boolean) {
    this.enabled = enabled;
    this.log(`Enabled: ${enabled}`);
    if (!enabled) {
      this.resetAndFreeze('disabled');
    }
  }
  
  setDelay(ms: number) {
    this.delayMs = Math.max(1000, Math.min(ms, 60000)); // 1-60 seconds
    this.log(`Delay set to ${this.delayMs}ms`);
  }
  
  // Callbacks
  setContextGetter(getter: () => TriggerContext | null) {
    this.getContext = getter;
  }
  
  setProactiveEngageCallback(callback: TriggerCallback) {
    this.onProactiveEngage = callback;
  }
  
  setStreamProactiveCallback(callback: StreamProactiveCallback) {
    this.onStreamProactive = callback;
  }
  
  setShowWidgetCallback(callback: (widget: { type: string; id: string | number; config: Record<string, unknown> }) => void) {
    this.onShowWidget = callback;
  }
  
  setMessageCountGetter(_getter: () => number) {
    // Legacy — kept for backward compat; stale-response guard now uses user message count
  }
  
  setUserMessageCountGetter(getter: () => number) {
    this.getUserMessageCount = getter;
  }
  
  /**
   * Set a live getter that returns true when a widget is displayed as the
   * last message in the chat (lead form, calendar, gallery, survey, etc.).
   * When true, the timer stays frozen — we never interrupt the user while
   * they're looking at or interacting with a widget. Only a new user
   * message (via onMessageSent) will restart the cycle.
   */
  setHasActiveWidgetGetter(getter: () => boolean) {
    this.getHasActiveWidget = getter;
  }
  
  // Input events (called by InputArea.tsx)
  onInputFocus() {
    this.inputFocused = true;
    this.resetAndFreeze('input_focus');
  }
  
  onInputBlur() {
    this.inputFocused = false;
    this.tryUnfreeze('input_blur');
  }
  
  onUserTyping() {
    this.resetAndFreeze('user_typing');
  }
  
  // Message events (called by chatStore)
  onMessageSent() {
    this.triggeredForCurrentResponse = false; // Allow new proactive trigger for next bot response
    
    // IMPORTANT: Abort any in-flight proactive request to save tokens!
    // If user sends a message while proactive GPT is streaming, abort it immediately
    if (this.currentAbortController) {
      this.log('Aborting in-flight proactive request (user sent message)');
      this.currentAbortController.abort();
      this.currentAbortController = null;
    }
    
    this.resetAndFreeze('message_sent');
  }
  
  // Get a new AbortController for the current proactive request
  getAbortController(): AbortController {
    // Abort any previous request first
    if (this.currentAbortController) {
      this.currentAbortController.abort();
    }
    this.currentAbortController = new AbortController();
    return this.currentAbortController;
  }
  
  // Clear the current abort controller (called when request completes normally)
  clearAbortController() {
    this.currentAbortController = null;
  }
  
  onSSEStart() {
    this.sseActiveCount += 1;
    this.resetAndFreeze('sse_start');
  }
  
  onSSEComplete() {
    this.sseActiveCount = Math.max(0, this.sseActiveCount - 1);
    if (this.sseActiveCount > 0) {
      this.log(`SSE complete received but ${this.sseActiveCount} stream(s) still active`);
      return;
    }
    this.tryUnfreeze('sse_complete');
  }
  
  // Widget events
  onWidgetInteractionStart() {
    this.widgetActive = true;
    this.resetAndFreeze('widget_start');
  }
  
  onWidgetInteractionEnd() {
    this.widgetActive = false;
    this.tryUnfreeze('widget_end');
  }
  
  // Reset the timer (called when chat is cleared, etc.)
  reset() {
    this.clearTimer();
    this.state = 'frozen';
    this.inputFocused = false;
    this.sseActiveCount = 0;
    this.widgetActive = false;
    this.log('Full reset');
  }
  
  /**
   * Called when chat is cleared/reset
   * Increments generation to invalidate any in-flight proactive calls
   * Freezes timer until user sends new message
   */
  onChatReset() {
    this.proactiveGeneration++;
    this.triggeredForCurrentResponse = false;
    this.clearTimer();
    this.state = 'frozen';
    this.sseActiveCount = 0;
    this.log(`Chat reset - generation=${this.proactiveGeneration}, frozen until user sends message`);
  }
  
  /**
   * Called when user navigates to a new page
   * Increments generation to invalidate any in-flight idle triggers
   * Freezes idle timer (but does NOT affect page transition triggers)
   */
  onPageNavigation() {
    this.proactiveGeneration++;
    this.triggeredForCurrentResponse = false;
    this.clearTimer();
    this.state = 'frozen';
    this.sseActiveCount = 0;
    this.log(`Page navigation - generation=${this.proactiveGeneration}, idle timer frozen`);
  }
  
  /**
   * Called on page refresh/visibility change
   * Freezes timer until user sends new message
   */
  onPageRefresh() {
    this.proactiveGeneration++;
    this.triggeredForCurrentResponse = false;
    this.clearTimer();
    this.state = 'frozen';
    this.sseActiveCount = 0;
    this.log(`Page refresh - generation=${this.proactiveGeneration}, frozen`);
  }
  
  /**
   * Get current generation (for external checks)
   */
  getGeneration(): number {
    return this.proactiveGeneration;
  }

  private isPendingQuestionAwaitingUser(text: string | undefined): boolean {
    if (!text) return false;
    const raw = text.trim();
    if (!raw) return false;
    const cleaned = raw.replace(/[\s.!…,;:'"”’)\]\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]+$/gu, '');
    if (!cleaned) return false;
    if (cleaned.endsWith('?') || cleaned.endsWith('؟')) return true;
    if (/[?؟]\s*$/.test(raw)) return true;
    const lowered = cleaned.toLowerCase();
    // NOTE: Keep in sync with backend/src/lib/prompt-strings.ts questionStarters.
    const questionPrefixes = [
      'האם ', 'איך ', 'מה ', 'מתי ', 'למה ', 'כמה ', 'איפה ', 'מי ',
      'אפשר ', 'תרצה ', 'תרצי ', 'רוצה ',
      'can you ', 'would you ', 'do you ', 'what ', 'how ', 'when ', 'why ',
      'could you ', 'is there ', 'are there ', 'should i ', 'should we ',
      'will you ', 'does ', 'did you ', 'have you ',
    ];
    return questionPrefixes.some(prefix => lowered.startsWith(prefix));
  }
  
  // Internal methods
  private resetAndFreeze(reason: string) {
    this.clearTimer();
    this.state = 'frozen';
    this.log(`Reset + Freeze (${reason})`);
  }
  
  private tryUnfreeze(reason: string) {
    // Check if enabled
    if (!this.enabled) {
      this.log(`Cannot unfreeze: proactive not enabled`);
      return;
    }
    
    // Check all freeze conditions
    if (this.inputFocused) {
      this.log(`Cannot unfreeze: input focused`);
      return;
    }
    if (this.sseActiveCount > 0) {
      this.log(`Cannot unfreeze: SSE active`);
      return;
    }
    if (this.widgetActive) {
      this.log(`Cannot unfreeze: widget interaction active`);
      return;
    }
    
    // CRITICAL: Check if a widget is displayed as the last message.
    // Never interrupt the user while they're looking at a lead form,
    // calendar, gallery, survey, or any other widget. It's a matter of manners.
    if (this.getHasActiveWidget?.()) {
      this.log(`Cannot unfreeze: widget displayed as last message — waiting for user input`);
      return;
    }
    
    if (this.state === 'triggering') {
      this.log(`Cannot unfreeze: proactive in progress`);
      return;
    }
    
    // Check if we have a valid context
    if (!this.getContext) {
      this.log(`Cannot unfreeze: no context getter`);
      return;
    }
    
    const context = this.getContext();
    if (!context || !context.conversationId) {
      this.log(`Cannot unfreeze: no conversation context`);
      return;
    }
    
    this.state = 'counting';
    this.startTimer();
    this.log(`Unfrozen (${reason}), starting ${this.delayMs}ms countdown`);
  }
  
  private clearTimer() {
    if (this.timerId) {
      clearTimeout(this.timerId);
      this.timerId = null;
    }
  }
  
  private startTimer() {
    this.clearTimer();
    this.timerId = setTimeout(() => this.onTimerFire(), this.delayMs);
  }
  
  private async onTimerFire() {
    // Double-check conditions before firing
    if (this.inputFocused || this.sseActiveCount > 0 || this.widgetActive) {
      this.log(`Timer fired but conditions changed, aborting`);
      this.state = 'frozen';
      return;
    }
    
    // Safety net: re-check live widget state at fire time.
    // Even if tryUnfreeze passed, the widget state may have changed.
    if (this.getHasActiveWidget?.()) {
      this.log(`Timer fired but widget displayed as last message — aborting, freezing until user input`);
      this.state = 'frozen';
      return;
    }
    
    if (!this.enabled) {
      this.log(`Timer fired but proactive disabled, aborting`);
      this.state = 'frozen';
      return;
    }
    
    if (!this.onProactiveEngage || !this.getContext) {
      this.log(`No callbacks set, aborting`);
      this.state = 'frozen';
      return;
    }
    
    // Only ONE proactive trigger per bot response
    if (this.triggeredForCurrentResponse) {
      this.log(`Already triggered for current response, staying frozen until user sends new message`);
      this.state = 'frozen';
      return;
    }
    
    // Get CURRENT context (not stale)
    const context = this.getContext();
    if (!context || !context.conversationId) {
      this.log(`No valid context, aborting`);
      this.state = 'frozen';
      return;
    }

    // Hard guard on client side too: if the last bot message is a pending question,
    // do not allow idle triggers until user sends a new message.
    if (this.isPendingQuestionAwaitingUser(context.lastBotResponse)) {
      this.log(`Pending question detected in last bot response — skipping proactive trigger`);
      this.state = 'frozen';
      return;
    }
    
    // Mark as triggered for this response
    this.triggeredForCurrentResponse = true;
    
    this.state = 'triggering';
    this.userCountAtTrigger = this.getUserMessageCount ? this.getUserMessageCount() : 0;
    const generationAtTrigger = this.proactiveGeneration;
    this.log(`TRIGGERING proactive engagement (userMessageCount: ${this.userCountAtTrigger}, generation: ${generationAtTrigger})`);
    
    try {
      const result = await this.onProactiveEngage(context);
      
      // Check if generation changed (reset/navigation/refresh occurred)
      if (this.proactiveGeneration !== generationAtTrigger) {
        this.log(`Generation changed during proactive call (${generationAtTrigger} -> ${this.proactiveGeneration}), discarding result`);
        return;
      }
      
      // Check if user sent message during proactive call
      // IMPORTANT: use getUserMessageCount (not total message count) because
      // the proactive engage callback itself adds a bot message to the store,
      // which would falsely trigger this guard and discard the widget.
      const userCountNow = this.getUserMessageCount ? this.getUserMessageCount() : 0;
      if (userCountNow > this.userCountAtTrigger) {
        this.log(`User sent message during proactive call (${this.userCountAtTrigger} -> ${userCountNow}), discarding result`);
        return;
      }
      
      // Handle WAIT response (null result or WAIT_FOR_USER action)
      const isWait = !result || result.action === 'WAIT_FOR_USER';
      if (isWait) {
        this.log(`Proactive returned WAIT - freezing until next user message`);
        // Gemini decided not to proactively engage - freeze until user sends new message
        // This is a deliberate decision, don't retry
        return;
      }
      
      this.log(`Proactive result: ${result.action}`);
      
      if ((result.action === 'SHOW_WIDGET' || result.action === 'RESPOND_WITH_WIDGET') && result.widget && this.onShowWidget) {
        this.onShowWidget(result.widget);
      } else if (result.action === 'RESPOND' && result.gpt_payload && this.onStreamProactive) {
        // Stream the proactive GPT response
        await this.onStreamProactive(result.gpt_payload, context);
      }
      
      // After any successful proactive action, freeze until next user message
      // ONE proactive per bot response - don't keep pestering the user
      this.log(`Proactive action complete - freezing until next user message`);
      
    } catch (err) {
      this.log(`Trigger error: ${err}`);
    } finally {
      // Always freeze after proactive trigger completes
      // Timer only restarts when user sends a NEW message (via onMessageSent -> onSSEComplete)
      this.state = 'frozen';
    }
  }
  
  private log(message: string) {
    try {
      const logs = JSON.parse(sessionStorage.getItem('boostbot_proactive_timer_logs') || '[]');
      logs.push({ 
        timestamp: new Date().toISOString(), 
        state: this.state,
        enabled: this.enabled,
        inputFocused: this.inputFocused,
        sseActive: this.sseActiveCount > 0,
        sseActiveCount: this.sseActiveCount,
        message 
      });
      if (logs.length > 50) logs.shift();
      sessionStorage.setItem('boostbot_proactive_timer_logs', JSON.stringify(logs));
    } catch {
      // Ignore storage errors
    }
  }
  
  // For debugging - expose globally
  getState() {
    return {
      state: this.state,
      enabled: this.enabled,
      inputFocused: this.inputFocused,
      sseActive: this.sseActiveCount > 0,
      sseActiveCount: this.sseActiveCount,
      widgetActive: this.widgetActive,
      lastMessageHasWidget: this.getHasActiveWidget?.() ?? false,
      delayMs: this.delayMs,
      triggeredForCurrentResponse: this.triggeredForCurrentResponse,
      proactiveGeneration: this.proactiveGeneration,
    };
  }
}

// Singleton instance
export const proactiveTimer = ProactiveTimerManager.getInstance();

// Expose for debugging and set up page visibility handler
if (typeof window !== 'undefined') {
  (window as unknown as Record<string, unknown>).__boostbot_proactiveTimer = proactiveTimer;
  
  // Handle page visibility changes (refresh, tab switch, etc.)
  // When page becomes hidden, freeze the timer
  // When page becomes visible again after being hidden, treat as potential refresh
  let wasHidden = false;
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      wasHidden = true;
      proactiveTimer.onPageRefresh();
    } else if (wasHidden) {
      wasHidden = false;
      // Page became visible again - timer stays frozen until user interacts
      // This is handled by the generation counter - any in-flight calls are invalidated
    }
  });
  
  // Handle beforeunload (actual page navigation/refresh)
  window.addEventListener('beforeunload', () => {
    proactiveTimer.onPageRefresh();
  });
}
