// ========================================
// useWebSocket Hook
// ========================================
// Typed WebSocket connection management with auto-reconnect

import { useEffect, useRef, useCallback } from 'react';
import { useNotificationStore } from '@/stores';
import { useExecutionStore } from '@/stores/executionStore';
import { useFlowStore } from '@/stores';
import { useCliSessionStore } from '@/stores/cliSessionStore';
import {
  handleSessionLockedMessage,
  handleSessionUnlockedMessage,
  useSessionManagerStore,
} from '@/stores/sessionManagerStore';
import {
  useExecutionMonitorStore,
  type ExecutionWSMessage,
} from '@/stores/executionMonitorStore';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import {
  OrchestratorMessageSchema,
  type OrchestratorWebSocketMessage,
  type ExecutionLog,
} from '../types/execution';
import { SurfaceUpdateSchema } from '../packages/a2ui-runtime/core/A2UITypes';
import type { ToolCallKind, ToolCallExecution } from '../types/toolCall';

// Constants
const RECONNECT_DELAY_BASE = 1000; // 1 second
const RECONNECT_DELAY_MAX = 30000; // 30 seconds
const RECONNECT_DELAY_MULTIPLIER = 1.5;
const WORKSPACE_SCOPED_CLI_MESSAGE_TYPES = new Set([
  'CLI_SESSION_CREATED',
  'CLI_SESSION_OUTPUT',
  'CLI_SESSION_CLOSED',
  'CLI_SESSION_PAUSED',
  'CLI_SESSION_RESUMED',
  'CLI_SESSION_LOCKED',
  'CLI_SESSION_UNLOCKED',
]);

// Access store state/actions via getState() - avoids calling hooks in callbacks/effects
// This is the zustand-recommended pattern for non-rendering store access
function getStoreState() {
  const notification = useNotificationStore.getState();
  const execution = useExecutionStore.getState();
  const flow = useFlowStore.getState();
  const cliSessions = useCliSessionStore.getState();
  return {
    // Notification store
    setWsStatus: notification.setWsStatus,
    setWsLastMessage: notification.setWsLastMessage,
    incrementReconnectAttempts: notification.incrementReconnectAttempts,
    resetReconnectAttempts: notification.resetReconnectAttempts,
    addA2UINotification: notification.addA2UINotification,
    // Execution store
    setExecutionStatus: execution.setExecutionStatus,
    setNodeStarted: execution.setNodeStarted,
    setNodeCompleted: execution.setNodeCompleted,
    setNodeFailed: execution.setNodeFailed,
    addLog: execution.addLog,
    completeExecution: execution.completeExecution,
    currentExecution: execution.currentExecution,
    // Tool call actions
    startToolCall: execution.startToolCall,
    updateToolCall: execution.updateToolCall,
    completeToolCall: execution.completeToolCall,
    toggleToolCallExpanded: execution.toggleToolCallExpanded,
    // Tool call getters
    getToolCallsForNode: execution.getToolCallsForNode,
    // Node output actions
    addNodeOutput: execution.addNodeOutput,
    // Flow store
    updateNode: flow.updateNode,

    // CLI session store (PTY-backed terminal)
    upsertCliSession: cliSessions.upsertSession,
    removeCliSession: cliSessions.removeSession,
    appendCliSessionOutput: cliSessions.appendOutput,
    updateCliSessionPausedState: cliSessions.updateSessionPausedState,
  };
}

function normalizeWorkspacePath(path: string | null | undefined): string | null {
  if (typeof path !== 'string') return null;

  const normalized = path.trim().replace(/\\/g, '/').replace(/\/+$/, '');
  if (!normalized) return null;

  return /^[a-z]:/i.test(normalized) ? normalized.toLowerCase() : normalized;
}

function getCurrentWorkspacePath(): string | null {
  return normalizeWorkspacePath(selectProjectPath(useWorkflowStore.getState()));
}

function isProjectPathInCurrentWorkspace(projectPath: string | null | undefined): boolean {
  const currentWorkspacePath = getCurrentWorkspacePath();
  if (!currentWorkspacePath) return true;

  return normalizeWorkspacePath(projectPath) === currentWorkspacePath;
}

function isPathInCurrentWorkspace(candidatePath: string | null | undefined): boolean {
  const currentWorkspacePath = getCurrentWorkspacePath();
  if (!currentWorkspacePath) return true;

  const normalizedCandidatePath = normalizeWorkspacePath(candidatePath);
  if (!normalizedCandidatePath) return false;

  return (
    normalizedCandidatePath === currentWorkspacePath ||
    normalizedCandidatePath.startsWith(`${currentWorkspacePath}/`)
  );
}

function isKnownCliSession(sessionKey: string | null | undefined): boolean {
  if (typeof sessionKey !== 'string' || !sessionKey) return false;

  if (sessionKey in useCliSessionStore.getState().sessions) {
    return true;
  }

  return sessionKey in useSessionManagerStore.getState().terminalMetas;
}

function shouldHandleCliSessionMessage(data: { type?: string; payload?: Record<string, unknown> }): boolean {
  const currentWorkspacePath = getCurrentWorkspacePath();
  if (!currentWorkspacePath) return true;

  const payload = data.payload ?? {};
  if (typeof payload.projectPath === 'string') {
    return isProjectPathInCurrentWorkspace(payload.projectPath);
  }

  if (data.type === 'CLI_SESSION_CREATED') {
    const session = payload.session as { workingDir?: string } | undefined;
    return isPathInCurrentWorkspace(session?.workingDir);
  }

  return isKnownCliSession(typeof payload.sessionKey === 'string' ? payload.sessionKey : null);
}

function shouldHandleExecutionWsMessage(message: ExecutionWSMessage): boolean {
  const currentWorkspacePath = getCurrentWorkspacePath();
  if (!currentWorkspacePath) return true;

  if (typeof message.payload.projectPath === 'string') {
    return isProjectPathInCurrentWorkspace(message.payload.projectPath);
  }

  if (message.payload.executionId in useExecutionMonitorStore.getState().activeExecutions) {
    return true;
  }

  if (message.type === 'EXECUTION_STARTED') {
    return isKnownCliSession(message.payload.sessionKey ?? null);
  }

  return false;
}

export interface UseWebSocketOptions {
  enabled?: boolean;
  onMessage?: (message: OrchestratorWebSocketMessage) => void;
}

export interface UseWebSocketReturn {
  isConnected: boolean;
  send: (message: unknown) => void;
  reconnect: () => void;
}

// ========== Tool Call Parsing Helpers ==========

/**
 * Parse tool call metadata from content
 * Expected format: "[Tool] toolName(args)"
 */
function parseToolCallMetadata(content: string): { toolName: string; args: string } | null {
  // Handle string content
  if (typeof content === 'string') {
    const match = content.match(/^\[Tool\]\s+(\w+)\((.*)\)$/);
    if (match) {
      return { toolName: match[1], args: match[2] || '' };
    }
  }

  // Handle object content with toolName field
  try {
    const parsed = typeof content === 'string' ? JSON.parse(content) : content;
    if (parsed && typeof parsed === 'object' && 'toolName' in parsed) {
      return {
        toolName: String(parsed.toolName),
        args: parsed.parameters ? JSON.stringify(parsed.parameters) : '',
      };
    }
  } catch {
    // Not valid JSON, return null
  }

  return null;
}

/**
 * Infer tool call kind from tool name
 */
function inferToolCallKind(toolName: string): ToolCallKind {
  const name = toolName.toLowerCase();

  if (name === 'exec_command' || name === 'execute') return 'execute';
  if (name === 'apply_patch' || name === 'patch') return 'patch';
  if (name === 'web_search' || name === 'exa_search') return 'web_search';
  if (name.startsWith('mcp_') || name.includes('mcp')) return 'mcp_tool';
  if (name.includes('file') || name.includes('read') || name.includes('write')) return 'file_operation';
  if (name.includes('think') || name.includes('reason')) return 'thinking';

  // Default to execute
  return 'execute';
}

/**
 * Generate unique tool call ID
 */
function generateToolCallId(): string {
  return `tool_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}

export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketReturn {
  const { enabled = true, onMessage } = options;

  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const reconnectDelayRef = useRef(RECONNECT_DELAY_BASE);
  const mountedRef = useRef(true);

  // Handle incoming WebSocket messages
  const handleMessage = useCallback(
    (event: MessageEvent) => {
      // Guard against state updates after unmount
      if (!mountedRef.current) {
        return;
      }

      try {
        const data = JSON.parse(event.data);
        const stores = getStoreState();

        // Store last message for debugging
        stores.setWsLastMessage(data);

        // Handle CLI messages
        if (data.type?.startsWith('CLI_')) {
          if (
            WORKSPACE_SCOPED_CLI_MESSAGE_TYPES.has(data.type) &&
            !shouldHandleCliSessionMessage(data as { type?: string; payload?: Record<string, unknown> })
          ) {
            return;
          }

          switch (data.type) {
            // ========== PTY CLI Sessions ==========
            case 'CLI_SESSION_CREATED': {
              const session = data.payload?.session;
              if (session?.sessionKey) {
                stores.upsertCliSession(session);
              }
              break;
            }

            case 'CLI_SESSION_OUTPUT': {
              const { sessionKey, data: chunk } = data.payload ?? {};
              if (typeof sessionKey === 'string' && typeof chunk === 'string') {
                stores.appendCliSessionOutput(sessionKey, chunk);
              }
              break;
            }

            case 'CLI_SESSION_CLOSED': {
              const { sessionKey } = data.payload ?? {};
              if (typeof sessionKey === 'string') {
                stores.removeCliSession(sessionKey);
              }
              break;
            }

            case 'CLI_SESSION_PAUSED': {
              const { sessionKey } = data.payload ?? {};
              if (typeof sessionKey === 'string') {
                stores.updateCliSessionPausedState(sessionKey, true);
              }
              break;
            }

            case 'CLI_SESSION_RESUMED': {
              const { sessionKey } = data.payload ?? {};
              if (typeof sessionKey === 'string') {
                stores.updateCliSessionPausedState(sessionKey, false);
              }
              break;
            }

            case 'CLI_SESSION_LOCKED': {
              const { sessionKey, reason, executionId, timestamp } = data.payload ?? {};
              if (typeof sessionKey === 'string') {
                handleSessionLockedMessage({
                  sessionKey,
                  reason: reason ?? 'Workflow execution',
                  executionId,
                  timestamp: timestamp ?? new Date().toISOString(),
                });
              }
              break;
            }

            case 'CLI_SESSION_UNLOCKED': {
              const { sessionKey, timestamp } = data.payload ?? {};
              if (typeof sessionKey === 'string') {
                handleSessionUnlockedMessage({
                  sessionKey,
                  timestamp: timestamp ?? new Date().toISOString(),
                });
              }
              break;
            }

            case 'CLI_OUTPUT': {
              const { chunkType, data: outputData, unit } = data.payload;

              // Handle structured output
              const unitContent = unit?.content || outputData;
              const unitType = unit?.type || chunkType;

              // Convert content to string for display
              let content: string;
              if (unitType === 'tool_call' && typeof unitContent === 'object' && unitContent !== null) {
                // Format tool_call display
                content = JSON.stringify(unitContent);
              } else {
                content = typeof unitContent === 'string' ? unitContent : JSON.stringify(unitContent);
              }

              // ========== Tool Call Processing ==========
              // Parse and start new tool call if this is a tool_call type
              if (unitType === 'tool_call') {
                const metadata = parseToolCallMetadata(content);
                if (metadata) {
                  const callId = generateToolCallId();
                  const currentNodeId = stores.currentExecution?.currentNodeId;

                  if (currentNodeId) {
                    stores.startToolCall(currentNodeId, callId, {
                      kind: inferToolCallKind(metadata.toolName),
                      description: metadata.args
                        ? `${metadata.toolName}(${metadata.args})`
                        : metadata.toolName,
                    });

                    // Also add to node output for streaming display
                    stores.addNodeOutput(currentNodeId, {
                      type: 'tool_call',
                      content,
                      timestamp: Date.now(),
                    });
                  }
                }
              }

              // ========== Stream Processing ==========
              // Update tool call output buffer if we have an active tool call for this node
              const currentNodeId = stores.currentExecution?.currentNodeId;
              if (currentNodeId && (unitType === 'stdout' || unitType === 'stderr')) {
                const toolCalls = stores.getToolCallsForNode?.(currentNodeId);
                const activeCall = toolCalls?.find((c: ToolCallExecution) => c.status === 'executing');

                if (activeCall) {
                  stores.updateToolCall(currentNodeId, activeCall.callId, {
                    outputChunk: content,
                    stream: unitType === 'stderr' ? 'stderr' : 'stdout',
                  });
                }
              }

              break;
            }
          }
          return;
        }

        // Handle EXECUTION messages (from orchestrator execution-in-session)
        if (data.type?.startsWith('EXECUTION_')) {
          const executionMessage = data as ExecutionWSMessage;
          if (!shouldHandleExecutionWsMessage(executionMessage)) {
            return;
          }

          const handleExecutionMessage = useExecutionMonitorStore.getState().handleExecutionMessage;
          handleExecutionMessage(executionMessage);
          return;
        }

        // Handle A2UI surface messages
        if (data.type === 'a2ui-surface') {
          const parsed = SurfaceUpdateSchema.safeParse(data.payload);
          if (parsed.success) {
            stores.addA2UINotification(parsed.data, 'Interactive UI');
          } else {
            console.warn('[WebSocket] Invalid A2UI surface:', parsed.error.issues);
          }
          return;
        }

        // Check if this is an orchestrator message
        if (!data.type?.startsWith('ORCHESTRATOR_')) {
          return;
        }

        // Validate message with zod schema
        const parsed = OrchestratorMessageSchema.safeParse(data);
        if (!parsed.success) {
          console.warn('[WebSocket] Invalid orchestrator message:', parsed.error.issues);
          return;
        }

        // Cast validated data to our TypeScript interface
        const message = parsed.data as OrchestratorWebSocketMessage;

        // Only process messages for current execution
        const { currentExecution } = stores;
        if (currentExecution && message.execId !== currentExecution.execId) {
          return;
        }

        // Dispatch to execution store based on message type
        switch (message.type) {
          case 'ORCHESTRATOR_STATE_UPDATE':
            stores.setExecutionStatus(message.status, message.currentNodeId);
            // Check for completion
            if (message.status === 'completed' || message.status === 'failed') {
              stores.completeExecution(message.status);
            }
            break;

          case 'ORCHESTRATOR_NODE_STARTED':
            stores.setNodeStarted(message.nodeId);
            // Update canvas node status
            stores.updateNode(message.nodeId, { executionStatus: 'running' });
            break;

          case 'ORCHESTRATOR_NODE_COMPLETED':
            stores.setNodeCompleted(message.nodeId, message.result);
            // Update canvas node status
            stores.updateNode(message.nodeId, {
              executionStatus: 'completed',
              executionResult: message.result,
            });
            break;

          case 'ORCHESTRATOR_NODE_FAILED':
            stores.setNodeFailed(message.nodeId, message.error);
            // Update canvas node status
            stores.updateNode(message.nodeId, {
              executionStatus: 'failed',
              executionError: message.error,
            });
            break;

          case 'ORCHESTRATOR_LOG':
            stores.addLog(message.log as ExecutionLog);
            break;
        }

        // Call custom message handler if provided
        onMessage?.(message);
      } catch (error) {
        console.error('[WebSocket] Failed to parse message:', error);
      }
    },
    [onMessage] // Only dependency is onMessage, store access via getState()
  );

  // Connect to WebSocket
  // Use ref to avoid circular dependency with scheduleReconnect
  const connectRef = useRef<(() => void) | null>(null);

  // Schedule reconnection with exponential backoff
  // Define this first to avoid circular dependency
  const scheduleReconnect = useCallback(() => {
    // Don't reconnect after unmount
    if (!mountedRef.current) return;

    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current);
    }

    const delay = reconnectDelayRef.current;
    console.log(`[WebSocket] Reconnecting in ${delay}ms...`);

    const stores = getStoreState();
    stores.setWsStatus('reconnecting');
    stores.incrementReconnectAttempts();

    reconnectTimeoutRef.current = setTimeout(() => {
      connectRef.current?.();
    }, delay);

    // Increase delay for next attempt (exponential backoff)
    reconnectDelayRef.current = Math.min(
      reconnectDelayRef.current * RECONNECT_DELAY_MULTIPLIER,
      RECONNECT_DELAY_MAX
    );
  }, []); // No dependencies - uses connectRef and getStoreState()

  const connect = useCallback(() => {
    if (!enabled || !mountedRef.current) return;

    // Close existing connection to avoid orphaned sockets
    if (wsRef.current) {
      wsRef.current.onclose = null; // Prevent onclose from triggering reconnect
      wsRef.current.close();
      wsRef.current = null;
    }

    // Construct WebSocket URL
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    const wsUrl = `${protocol}//${window.location.host}/ws`;

    try {
      getStoreState().setWsStatus('connecting');

      const ws = new WebSocket(wsUrl);
      wsRef.current = ws;

      ws.onopen = () => {
        console.log('[WebSocket] Connected');
        const s = getStoreState();
        s.setWsStatus('connected');
        s.resetReconnectAttempts();
        reconnectDelayRef.current = RECONNECT_DELAY_BASE;

        // Request any pending questions from backend
        ws.send(JSON.stringify({
          type: 'FRONTEND_READY',
          payload: { action: 'requestPendingQuestions' }
        }));
      };

      ws.onmessage = handleMessage;

      ws.onclose = () => {
        console.log('[WebSocket] Disconnected');
        getStoreState().setWsStatus('disconnected');
        wsRef.current = null;
        scheduleReconnect();
      };

      ws.onerror = () => {
        console.warn('[WebSocket] Connection error');
        getStoreState().setWsStatus('error');
      };
    } catch (error) {
      console.error('[WebSocket] Failed to connect:', error);
      getStoreState().setWsStatus('error');
      scheduleReconnect();
    }
  }, [enabled, handleMessage, scheduleReconnect]);

  // Update connect ref after connect is defined
  connectRef.current = connect;

  // Send message through WebSocket
  const send = useCallback((message: unknown) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify(message));
    } else {
      console.warn('[WebSocket] Cannot send message: not connected');
    }
  }, []);

  // Manual reconnect
  // Use connectRef to avoid depending on connect
  const reconnect = useCallback(() => {
    if (wsRef.current) {
      wsRef.current.close();
    }
    reconnectDelayRef.current = RECONNECT_DELAY_BASE;
    connectRef.current?.();
  }, []); // No dependencies - uses connectRef

  // Check connection status
  const isConnected = wsRef.current?.readyState === WebSocket.OPEN;

  // Connect on mount, cleanup on unmount
  useEffect(() => {
    // Reset mounted flag (needed after React Strict Mode remount)
    mountedRef.current = true;

    if (enabled) {
      connect();
    }

    // Listen for A2UI action events and send via WebSocket
    const handleA2UIAction = (event: CustomEvent) => {
      if (wsRef.current?.readyState === WebSocket.OPEN) {
        wsRef.current.send(JSON.stringify(event.detail));
      } else {
        console.warn('[WebSocket] Cannot send A2UI action: not connected');
      }
    };

    // Type the event listener properly
    window.addEventListener('a2ui-action', handleA2UIAction as EventListener);

    return () => {
      // Mark as unmounted to prevent state updates in handleMessage
      mountedRef.current = false;

      window.removeEventListener('a2ui-action', handleA2UIAction as EventListener);
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current);
      }
      if (wsRef.current) {
        wsRef.current.onclose = null; // Prevent onclose from triggering orphaned reconnect
        wsRef.current.close();
        wsRef.current = null;
      }
    };
  }, [enabled, connect]);

  return {
    isConnected,
    send,
    reconnect,
  };
}

export default useWebSocket;
