/**
 * Execution Engine
 *
 * Core workflow execution engine that processes instances through nodes.
 * Handles graph traversal, action execution, and flow control logic.
 */

import { getModels } from '../../models';
import { NodeRegistry, nodeRegistry } from './NodeRegistry';
import { DelayScheduler } from './DelayScheduler';
import { InstanceManager } from './InstanceManager';
import { AnalyticsService } from './AnalyticsService';
import type {
  NodeExecutionResult,
  ExecutionContext,
  ProcessResult,
  ContactData,
  WorkflowSettings,
} from './types';
import type {
  IAutomationWorkflow,
  IAutomationInstance,
  IWorkflowNode,
  IWorkflowEdge,
  INodeExecutionState,
} from '../../models/AutomationWorkflow';
import mongoose from 'mongoose';

// ============================================
// EXECUTION ENGINE CLASS
// ============================================

export class ExecutionEngine {
  private static instance: ExecutionEngine;

  static getInstance(): ExecutionEngine {
    if (!ExecutionEngine.instance) {
      ExecutionEngine.instance = new ExecutionEngine();
    }
    return ExecutionEngine.instance;
  }
  private nodeRegistry: NodeRegistry;
  private delayScheduler: DelayScheduler;
  private instanceManager: InstanceManager;
  private analyticsService: AnalyticsService;

  constructor() {
    this.nodeRegistry = nodeRegistry;
    this.delayScheduler = new DelayScheduler();
    this.instanceManager = new InstanceManager();
    this.analyticsService = new AnalyticsService();
  }

  /**
   * Process a workflow instance
   * Called by trigger service or background worker
   */
  async processInstance(instanceId: string): Promise<ProcessResult> {
    const { AutomationInstance, AutomationWorkflow, AutomationLog } = getModels();

    // Fetch instance
    const instance = await AutomationInstance.findById(instanceId).lean();
    if (!instance) {
      return {
        success: false,
        instanceId,
        status: 'error',
        error: 'Instance not found',
      };
    }

    // Fetch workflow
    const workflow = await AutomationWorkflow.findById(instance.workflowId).lean();
    if (!workflow) {
      return {
        success: false,
        instanceId,
        status: 'error',
        error: 'Workflow not found',
      };
    }

    // Check if workflow is active
    if (workflow.status !== 'active') {
      return {
        success: false,
        instanceId,
        status: 'error',
        error: 'Workflow is not active',
      };
    }

    // Build execution context
    const context = await this.buildExecutionContext(instance, workflow);

    try {
      // Update instance status to running
      await AutomationInstance.findByIdAndUpdate(instanceId, {
        $set: {
          status: 'running',
          lastProcessedAt: new Date(),
        },
      });

      // Get current node
      let currentNodeId = instance.currentNodeId;
      let shouldContinue = true;

      while (shouldContinue) {
        // Find current node in workflow
        const currentNode = workflow.nodes.find((n: any) => n.id === currentNodeId);
        if (!currentNode) {
          throw new Error(`Node ${currentNodeId} not found in workflow`);
        }

        // Trigger nodes are starting points — skip them and move to the next node
        // They don't have handlers in the NodeRegistry because they're entry
        // points, not executable actions.
        if (currentNode.type.startsWith('trigger_')) {
          console.log(`[ExecutionEngine] Skipping trigger node ${currentNodeId} (${currentNode.type})`);

          // Mark the trigger as executed
          await AutomationInstance.findByIdAndUpdate(instanceId, {
            $push: { executedNodes: currentNodeId },
          });

          // Find next node via edges
          const nextNodes = this.getNextNodes(workflow, currentNodeId, { success: true });
          if (nextNodes.length === 0) {
            // No nodes connected to the trigger — workflow complete
            await this.instanceManager.completeInstance(instanceId, 'completed');
            return {
              success: true,
              instanceId,
              status: 'completed',
            };
          }

          // Move to the first connected node
          currentNodeId = nextNodes[0];
          await AutomationInstance.findByIdAndUpdate(instanceId, {
            $set: { currentNodeId, lastProcessedAt: new Date() },
          });
          continue; // Process the next node
        }

        // Get node handler
        const handler = this.nodeRegistry.get(currentNode.type);
        if (!handler) {
          throw new Error(`No handler registered for node type: ${currentNode.type}`);
        }

        // Log start
        const startTime = Date.now();
        await this.logExecution(instanceId, workflow._id.toString(), currentNode, 'started', context);

        // Execute node
        let result: NodeExecutionResult;
        try {
          result = await handler.execute!(instance, currentNode, context);
        } catch (error: any) {
          result = {
            success: false,
            error: error.message || 'Unknown error during execution',
          };
        }

        // Calculate duration
        const duration = Date.now() - startTime;

        // Log completion
        await this.logExecution(
          instanceId,
          workflow._id.toString(),
          currentNode,
          result.success ? 'completed' : 'error',
          context,
          result.output,
          result.error,
          duration
        );

        // Handle execution result
        if (!result.success) {
          // Error occurred
          await this.handleExecutionError(instanceId, currentNodeId, result.error || 'Unknown error');
          return {
            success: false,
            instanceId,
            status: 'error',
            currentNodeId,
            error: result.error,
          };
        }

        // Check if this is a delay node
        if (result.resumeAt) {
          // Schedule instance for later processing
          await this.delayScheduler.scheduleInstance(instanceId, result.resumeAt);
          return {
            success: true,
            instanceId,
            status: 'paused',
            currentNodeId,
          };
        }

        // Check if this is a wait for event node
        if (currentNode.type === 'flow_wait_until_event') {
          // Set instance to wait for event
          await AutomationInstance.findByIdAndUpdate(instanceId, {
            $set: {
              status: 'paused',
              waitingForEvent: currentNode.config.waitEventTrigger,
            },
          });
          return {
            success: true,
            instanceId,
            status: 'paused',
            currentNodeId,
          };
        }

        // Check if this is exit node
        if (currentNode.type === 'flow_exit') {
          await this.instanceManager.completeInstance(instanceId, 'exited', result.output?.reason);
          return {
            success: true,
            instanceId,
            status: 'exited',
            currentNodeId,
          };
        }

        // Check if this is end node
        if (currentNode.type === 'flow_end') {
          await this.instanceManager.completeInstance(instanceId, 'completed');
          return {
            success: true,
            instanceId,
            status: 'completed',
            currentNodeId,
          };
        }

        // Check if this is goal node
        if (currentNode.type === 'flow_goal') {
          await AutomationInstance.findByIdAndUpdate(instanceId, {
            $set: {
              goalReached: true,
              goalReachedAt: new Date(),
            },
          });
        }

        // Get next node(s)
        const nextNodes = this.getNextNodes(workflow, currentNodeId, result);

        if (nextNodes.length === 0) {
          // No more nodes - workflow complete
          await this.instanceManager.completeInstance(instanceId, 'completed');
          return {
            success: true,
            instanceId,
            status: 'completed',
            currentNodeId,
          };
        }

        // For now, follow the first path (for conditions/splits, result.branch determines path)
        const nextNodeId = nextNodes[0];

        // Update instance state
        await AutomationInstance.findByIdAndUpdate(instanceId, {
          $set: {
            currentNodeId: nextNodeId,
            lastProcessedAt: new Date(),
          },
          $push: {
            executedNodes: currentNodeId,
          },
        });

        // Move to next node
        currentNodeId = nextNodeId;

        // Continue loop
      }

      return {
        success: true,
        instanceId,
        status: 'completed',
      };
    } catch (error: any) {
      console.error(`[ExecutionEngine] Error processing instance ${instanceId}:`, error);

      // Update instance to error state
      await this.handleExecutionError(instanceId, instance.currentNodeId, error.message);

      return {
        success: false,
        instanceId,
        status: 'error',
        currentNodeId: instance.currentNodeId,
        error: error.message,
      };
    }
  }

  /**
   * Build execution context for a workflow instance
   */
  private async buildExecutionContext(
    instance: IAutomationInstance,
    workflow: IAutomationWorkflow
  ): Promise<ExecutionContext> {
    // Use contact data from trigger event variables (stored by TriggerService)
    // This provides real email, name, etc. from the webhook event
    // Falls back to fetchContactData if not available
    const variables = instance.variables || {};
    const triggerContact = variables.contact as ContactData | undefined;
    const contact = triggerContact || await this.fetchContactData(instance.contactId);

    return {
      companyId: instance.companyId,
      workflowId: instance.workflowId.toString(),
      instanceId: instance._id.toString(),
      contact,
      variables,
      settings: workflow.settings as WorkflowSettings,
      isTestMode: workflow.settings?.testMode || false,
    };
  }

  /**
   * Fetch contact data from Contact model
   * Falls back to a placeholder if no Contact model is available
   */
  private async fetchContactData(contactId: string): Promise<ContactData> {
    // TODO: Integrate with actual Contact model when available
    // TriggerService now stores contact data in instance.variables.contact,
    // so this fallback is rarely needed
    return {
      id: contactId,
      email: '',
      phone: '',
      firstName: '',
      lastName: '',
      tags: [],
      customFields: {},
      lists: [],
      segments: [],
    };
  }

  /**
   * Get next nodes based on edge connections and execution result
   */
  private getNextNodes(
    workflow: IAutomationWorkflow,
    currentNodeId: string,
    result: NodeExecutionResult
  ): string[] {
    // Find edges from current node
    const outgoingEdges = workflow.edges.filter((edge) => edge.source === currentNodeId);

    if (outgoingEdges.length === 0) {
      return [];
    }

    // For condition/split nodes with branch result, follow the correct path
    if (result.branch) {
      const branchEdge = outgoingEdges.find((edge) => edge.sourceHandle === result.branch);
      if (branchEdge) {
        return [branchEdge.target];
      }
    }

    // For nodes with multiple outputs (conditions), check sourceHandle
    // If no branch specified but multiple edges exist, return first one
    if (outgoingEdges.length === 1) {
      return [outgoingEdges[0].target];
    }

    // Multiple edges without branch - this shouldn't happen in valid workflows
    // Return the first one as fallback
    return [outgoingEdges[0].target];
  }

  /**
   * Log execution to AutomationLog
   */
  private async logExecution(
    instanceId: string,
    workflowId: string,
    node: IWorkflowNode,
    status: 'started' | 'completed' | 'error' | 'skipped',
    context: ExecutionContext,
    output?: Record<string, any>,
    error?: string,
    duration?: number
  ): Promise<void> {
    const { AutomationLog } = getModels();

    try {
      await AutomationLog.create({
        companyId: context.companyId,
        instanceId: new mongoose.Types.ObjectId(instanceId),
        workflowId: new mongoose.Types.ObjectId(workflowId),
        nodeId: node.id,
        nodeType: node.type,
        action: node.type,
        status,
        input: { config: node.config },
        output,
        error,
        timestamp: new Date(),
        duration,
      });
    } catch (err) {
      console.error('[ExecutionEngine] Failed to log execution:', err);
    }
  }

  /**
   * Handle execution error
   */
  private async handleExecutionError(
    instanceId: string,
    nodeId: string,
    errorMessage: string
  ): Promise<void> {
    const { AutomationInstance } = getModels();

    await AutomationInstance.findByIdAndUpdate(instanceId, {
      $set: {
        status: 'error',
        lastProcessedAt: new Date(),
      },
      $push: {
        executionErrors: {
          nodeId,
          message: errorMessage,
          timestamp: new Date(),
          retryCount: 0,
        },
      },
    });
  }
}