import { WorkflowDefinition } from './definition';
import { NodeRegistry } from './nodeRegistry';
import { ILogger, IPersistence, IScheduler, IEventManager } from '../services';
import { IWorkflowContext, INodeContext, NodeStatus, IExecutionRecord } from '../types/runtime';
import { StepConfig, StepType, TaskStepConfig, ConditionStepConfig, SubWorkflowStepConfig } from '../types/config';
import { createWorkflowContext, createNodeContext } from './context'; // Assuming these exist
import { WorkflowError, NodeExecutionError, TimeoutError, ConfigurationError, AuthorizationError } from '../errors/index';
import { BaseNode } from '../nodes/baseNode';
import { NodeFunction } from '../types/config';


export interface WorkflowExecutorOptions {
    nodeRegistry: NodeRegistry;
    logger: ILogger;
    persistence?: IPersistence;
    scheduler?: IScheduler;
    eventManager?: IEventManager;
    getWorkflowDefinition: (id: string) => Promise<WorkflowDefinition | undefined>;
    maxLoopIterations?: number;
}

export class WorkflowExecutor {
  private readonly nodeRegistry: NodeRegistry;
  private readonly logger: ILogger;
  private readonly persistence?: IPersistence;
  private readonly scheduler?: IScheduler;
  private readonly eventManager?: IEventManager;
  private readonly getWorkflowDefinition: (id: string) => Promise<WorkflowDefinition | undefined>;
  private readonly maxLoopIterations: number;

  constructor(options: WorkflowExecutorOptions) {
    this.nodeRegistry = options.nodeRegistry;
    this.logger = options.logger;
    this.persistence = options.persistence;
    this.scheduler = options.scheduler;
    this.eventManager = options.eventManager;
    this.getWorkflowDefinition = options.getWorkflowDefinition;
    this.maxLoopIterations = options.maxLoopIterations ?? 1000;
  }

  async run(
      definitionOrId: WorkflowDefinition | string,
      input: Record<string, any>,
      contextExtras: Partial<IWorkflowContext> = {}
  ): Promise<IWorkflowContext> {

      const definition = typeof definitionOrId === 'string'
          ? await this.getWorkflowDefinition(definitionOrId)
          : definitionOrId;

      if (!definition) {
          throw new ConfigurationError(`Workflow definition '${definitionOrId}' not found.`);
      }
      if (!definition.startStepId) {
          throw new ConfigurationError(`Workflow definition '${definition.id}' has no start step defined.`);
      }
       if (!definition.validate()) { // Added validation call
            throw new ConfigurationError(`Workflow definition '${definition.id}' failed validation.`);
        }

      // Initialize context
      const context = createWorkflowContext(definition.id, definition.name, input, contextExtras);
      this.logger.info(`Starting workflow run: ${context.workflowId} (Def: ${definition.id})`);
      await this.emitEvent('workflow.started', context); // Emit event

      let currentStepId: string | undefined = definition.startStepId;
      let loopGuard = 0;

      try {
          while (currentStepId && loopGuard++ < this.maxLoopIterations) {
              const stepConfig = definition.getStep(currentStepId);
              if (!stepConfig) {
                  throw new WorkflowError(`Step '${currentStepId}' not found in workflow '${definition.id}'.`, context.workflowId);
              }

              context.logs.push(`Executing step: ${currentStepId} (Type: ${stepConfig.type})`);
              const stepResult = await this.executeStep(stepConfig, context);

              // Update history and persistence
              this.updateHistory(context, stepResult.record);
              if (this.persistence) await this.persistence.saveState(context);

              if (stepResult.status === NodeStatus.FAILED || stepResult.status === NodeStatus.CANCELLED) {
                  context.status = stepResult.status;
                  context.output = stepResult.output; // Capture last output on failure
                  this.logger.error(`Workflow run ${context.workflowId} failed at step ${currentStepId}.`, stepResult.error);
                  await this.emitEvent('workflow.failed', context, { stepId: currentStepId, error: stepResult.error?.message });
                  break; // Stop execution
              }

               if (stepResult.status === NodeStatus.COMPENSATING) {
                  // TODO: Implement compensation flow triggering
                   this.logger.warn(`Compensation requested at step ${currentStepId}, stopping normal flow.`);
                   context.status = NodeStatus.FAILED; // Mark workflow as failed if compensation needed? Or specific status?
                   break;
               }

              currentStepId = stepResult.nextStepId; // Determine next step

              if (!currentStepId) {
                   context.status = NodeStatus.COMPLETED;
                   context.output = stepResult.output; // Capture final output
                   this.logger.info(`Workflow run ${context.workflowId} completed successfully.`);
                   await this.emitEvent('workflow.completed', context);
                   break; // Workflow finished
              }
          } // End while loop

          if (loopGuard >= this.maxLoopIterations) {
              throw new WorkflowError(`Maximum loop iterations (${this.maxLoopIterations}) reached. Potential infinite loop detected.`, context.workflowId);
          }

      } catch (error: any) {
          context.status = NodeStatus.FAILED;
          context.error = error; // Store workflow-level error
          this.logger.error(`Workflow run ${context.workflowId} encountered an unhandled error.`, error);
           await this.emitEvent('workflow.failed', context, { error: error.message });
          // No re-throw, return context with FAILED status
      } finally {
          context.endTime = new Date();
          if (this.persistence) {
              try {
                  await this.persistence.saveState(context); // Final state save
              } catch (saveError) {
                  this.logger.error(`Failed to save final workflow state for ${context.workflowId}`, saveError as Error);
              }
          }
      }

      return context;
  }

  // --- Step Execution Logic (Needs detailed implementation) ---
  private async executeStep(stepConfig: StepConfig, workflowContext: IWorkflowContext): Promise<{ status: NodeStatus; nextStepId?: string; output?: any; error?: Error; record: IExecutionRecord }> {
      const stepStartTime = new Date();
      let stepStatus = NodeStatus.PENDING;
      let stepOutput: any = undefined;
      let stepError: Error | undefined = undefined;
      let nextStepId: string | undefined = stepConfig.nextStepId; // Default next step

      const record: Partial<IExecutionRecord> = {
          stepId: stepConfig.id,
          startTime: stepStartTime,
          status: NodeStatus.PENDING,
      };

      try {
           await this.emitEvent('step.started', workflowContext, { stepId: stepConfig.id, type: stepConfig.type });
           stepStatus = NodeStatus.RUNNING;
           record.status = stepStatus; // Update record status

           switch (stepConfig.type) {
              case StepType.TASK:
                  const taskResult = await this.executeTaskStep(stepConfig, workflowContext);
                  stepStatus = taskResult.status;
                  stepOutput = taskResult.output;
                  stepError = taskResult.error;
                  record.nodeId = stepConfig.nodeId;
                  // Use default nextStepId unless overridden
                  break;
              case StepType.CONDITION:
                  const conditionResult = await this.executeConditionStep(stepConfig, workflowContext);
                  stepStatus = NodeStatus.COMPLETED; // Condition itself completes
                  nextStepId = conditionResult.nextStepId; // Condition determines the *actual* next step
                  record.output = { branchTaken: conditionResult.branchKey }; // Record which branch was taken
                   if (!nextStepId) { // No matching branch, potentially end workflow if this was the last path
                       this.logger.warn(`Condition step '${stepConfig.id}' resulted in no matching branch ('${conditionResult.branchKey}'). Ending this path.`);
                   }
                  break;
               case StepType.PARALLEL:
                   // TODO: Implement parallel execution logic
                   // This involves running multiple steps concurrently and waiting for all.
                   // Needs careful state management and error handling for partial failures.
                   this.logger.warn(`Parallel step execution not fully implemented yet for step: ${stepConfig.id}`);
                   stepStatus = NodeStatus.SKIPPED; // Mark as skipped for now
                   break;
               case StepType.SUB_WORKFLOW:
                   // TODO: Implement sub-workflow execution
                   // Needs to call executor.run() recursively with the sub-workflow def/id
                   // and handle input/output mapping.
                   this.logger.warn(`SubWorkflow step execution not fully implemented yet for step: ${stepConfig.id}`);
                   stepStatus = NodeStatus.SKIPPED; // Mark as skipped for now
                   break;
                // TODO: Implement EVENT_TRIGGER, EVENT_LISTENER
               default:
                   throw new ConfigurationError(`Unsupported step type: ${(stepConfig as any).type}`);
           }

           if (stepStatus === NodeStatus.FAILED && stepConfig.compensateOnFailure) {
               // TODO: Trigger compensation logic
               this.logger.warn(`Compensation requested for failed step '${stepConfig.id}'. Compensation logic not fully implemented.`);
               // Potentially change status to COMPENSATING and stop normal flow?
               stepStatus = NodeStatus.COMPENSATING; // Signal need for compensation
           }

      } catch (err: any) {
          stepStatus = NodeStatus.FAILED;
          stepError = err instanceof Error ? err : new WorkflowError(String(err), workflowContext.workflowId);
          this.logger.error(`Error executing step ${stepConfig.id}: ${stepError.message}`, stepError);
      } finally {
          record.status = stepStatus;
          record.endTime = new Date();
          record.output = stepOutput; // Record step output
          record.error = stepError?.message;
          await this.emitEvent(`step.${stepStatus.toLowerCase()}`, workflowContext, { stepId: stepConfig.id, type: stepConfig.type, error: stepError?.message });
      }

      return { status: stepStatus, nextStepId, output: stepOutput, error: stepError, record: record as IExecutionRecord };
  }

  // --- Task Step Execution with Retries/Timeout ---
  private async executeTaskStep(config: TaskStepConfig, workflowContext: IWorkflowContext): Promise<{ status: NodeStatus; output?: any; error?: Error }> {
    const maxRetries = config.retryOptions?.maxRetries ?? 0;
    const delayMs = config.retryOptions?.delayMs ?? 0;
    const timeoutMs = config.timeoutMs; // Optional timeout

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        const nodeContext = createNodeContext(workflowContext, config.id, config.nodeId, attempt);
        const nodeInfo = this.nodeRegistry.get(config.nodeId);

        if (!nodeInfo) {
            throw new ConfigurationError(`Node '${config.nodeId}' not registered.`);
        }

        // --- Resolve Input Mapping ---
        this.resolveInputMapping(config, workflowContext, nodeContext);

        let executionPromise: Promise<void>;
        let nodeOutput: any = undefined;
        let nodeError: Error | undefined = undefined;
        let nodeStatus: NodeStatus = NodeStatus.RUNNING;

        try {
            // --- Get Node Implementation ---
            const implementation = nodeInfo.implementation;
            let nodeExecuteFn: (ctx: INodeContext) => Promise<void>;

            if (nodeInfo.isClassInstance) {
                const nodeInstance = implementation as BaseNode;
                // --- Optional: Role Check ---
                if (nodeInstance.requiredRoles && !this.checkRoles(workflowContext.userRole, nodeInstance.requiredRoles)) {
                    throw new AuthorizationError(`User role '${workflowContext.userRole}' not authorized for node '${config.nodeId}'. Required: ${nodeInstance.requiredRoles.join(', ')}`);
                }
                // --- Optional: Input Validation ---
                if (nodeInstance.validate) await nodeInstance.validate(nodeContext);

                nodeExecuteFn = nodeInstance.execute.bind(nodeInstance); // Bind context
            } else {
                // Simple function node
                 const simpleFunc = implementation as NodeFunction;
                 // Wrap simple function to match BaseNode execute signature if needed,
                 // or adjust BaseNode execute signature/context passing.
                 // For simplicity, assume NodeFunction takes INodeContext now.
                 nodeExecuteFn = async (ctx) => {
                     // For simple functions, maybe pass resolved input directly for convenience?
                     const result = await (simpleFunc as any)(ctx, ctx.input); // Adjust signature as needed
                     if (result !== undefined) {
                         ctx.output = result; // Assume direct return is output
                     }
                 };
            }


            // --- Execute with Timeout ---
            executionPromise = nodeExecuteFn(nodeContext);

            if (timeoutMs && timeoutMs > 0) {
                const timeoutPromise = new Promise<never>((_, reject) =>
                    setTimeout(() => reject(new TimeoutError(`Node '${config.nodeId}' timed out after ${timeoutMs}ms.`, config.nodeId, config.id)), timeoutMs)
                );
                await Promise.race([executionPromise, timeoutPromise]);
            } else {
                await executionPromise;
            }

            // --- Execution Success ---
            nodeStatus = NodeStatus.COMPLETED;
            nodeOutput = nodeContext.output; // Get output potentially set on context

             // --- Resolve Output Mapping ---
             this.resolveOutputMapping(config, nodeContext, workflowContext);

            break; // Exit retry loop on success

        } catch (err: any) {
            // --- Execution Error ---
            nodeError = err instanceof Error ? err : new NodeExecutionError(String(err), config.nodeId, config.id);
            nodeContext.error = nodeError; // Record error on node context for logging/history
            nodeStatus = err instanceof TimeoutError ? NodeStatus.TIMEOUT : NodeStatus.FAILED;
            this.logger.warn(`Attempt ${attempt + 1}/${maxRetries + 1} failed for node ${config.nodeId} in step ${config.id}: ${nodeError.message}`);

            if (attempt >= maxRetries) {
                this.logger.error(`Node ${config.nodeId} failed after ${maxRetries} retries in step ${config.id}.`, nodeError);
                break; // Exit loop after max retries
            }

            if (delayMs > 0) {
                await new Promise(resolve => setTimeout(resolve, delayMs)); // Wait before retry
            }
        } finally {
             nodeContext.status = nodeStatus; // Final status for this attempt
             nodeContext.endTime = new Date();
             // Log node execution attempt details if necessary
             this.updateHistory(workflowContext, { // Add attempt info to history? Or just final?
                 stepId: config.id, nodeId: config.nodeId, status: nodeStatus,
                 startTime: nodeContext.startTime, endTime: nodeContext.endTime,
                 error: nodeError?.message, retries: attempt
             });
        }

    } // End retry loop

    return { status: NodeStatus.COMPLETED };
}

  // --- Condition Step Execution ---
 private async executeConditionStep(config: ConditionStepConfig, workflowContext: IWorkflowContext): Promise<{ nextStepId?: string, branchKey: string | boolean }> {
    const conditionFn = config.condition;
    const branchKey = await conditionFn(workflowContext); // Evaluate condition
    const nextStepId = config.branches[String(branchKey)]; // Find next step based on result

    this.logger.info(`Condition step '${config.id}' evaluated to '${branchKey}'. Next step: ${nextStepId || 'None'}`);
    return { nextStepId, branchKey };
 }

 // --- Helper Methods ---
 private updateHistory(context: IWorkflowContext, record: IExecutionRecord) {
    // Ensure history is mutable locally if needed, or handle immutability
    (context.history as IExecutionRecord[]).push(record);
    // Optional: Limit history size
 }

 private resolveInputMapping(stepConfig: TaskStepConfig | SubWorkflowStepConfig, wfCtx: IWorkflowContext, nodeCtx: INodeContext) {
     const resolvedInput: Record<string, any> = { ...stepConfig.input }; // Start with static input

     if (stepConfig.inputMapping) {
         for (const targetField in stepConfig.inputMapping) {
             const sourcePath = stepConfig.inputMapping[targetField];
             resolvedInput[targetField] = this.resolveContextPath(sourcePath, wfCtx);
         }
     }
     // Make resolved input available on node context
     (nodeCtx as any).input = Object.freeze(resolvedInput); // Make immutable
     this.logger.debug(`Resolved input for step ${stepConfig.id}:`, resolvedInput);
 }

 private resolveOutputMapping(stepConfig: TaskStepConfig | SubWorkflowStepConfig, nodeCtx: INodeContext, wfCtx: IWorkflowContext) {
     if (stepConfig.outputMapping && nodeCtx.output) {
         for (const targetPath in stepConfig.outputMapping) {
             const sourceField = stepConfig.outputMapping[targetPath];
             const value = (nodeCtx.output as any)[sourceField];
             this.setContextPath(targetPath, value, wfCtx);
             this.logger.debug(`Mapped output '${sourceField}' to '${targetPath}' for step ${stepConfig.id}`);
         }
     }
 }


  // Resolve value from context using dot notation (e.g., "variables.user.id", "input.orderId", "steps.step1.output.result")
  private resolveContextPath(path: string, context: IWorkflowContext): any {
      const parts = path.split('.');
      let current: any = context;

      if (parts[0] === 'variables') {
          current = context.variables;
          parts.shift();
      } else if (parts[0] === 'input') {
          current = context.input;
          parts.shift();
      } else if (parts[0] === 'steps') {
          // Access output from previous steps via history? Or a dedicated step output cache?
          // Accessing history might be complex. Let's assume direct variable access for now.
          // This part needs careful design. For simplicity, only support 'variables' and 'input' for now.
          this.logger.warn(`Resolving step outputs via path ('${path}') not fully supported yet. Use variables.`);
          return undefined;
      } else {
          // Assume it's a top-level context property or variable implicitly
          current = context.variables; // Default to checking variables
      }


      for (const part of parts) {
          if (current === null || typeof current !== 'object') return undefined;
          current = current[part];
      }
      return current;
  }

  // Set value in context using dot notation (only supports 'variables' for now)
  private setContextPath(path: string, value: any, context: IWorkflowContext): void {
      const parts = path.split('.');
      if (parts[0] !== 'variables' || parts.length < 2) {
          this.logger.error(`Output mapping path '${path}' is invalid. Only 'variables.xxx' is supported.`);
          return;
      }

      let current = context.variables;
      for (let i = 1; i < parts.length - 1; i++) {
          const part = parts[i];
          if (current[part] === undefined || typeof current[part] !== 'object') {
              current[part] = {}; // Create nested objects if they don't exist
          }
          current = current[part];
      }
      current[parts[parts.length - 1]] = value;
  }


 private checkRoles(userRole: string | undefined, requiredRoles: string[]): boolean {
     if (!requiredRoles || requiredRoles.length === 0) return true; // No roles required
     if (!userRole) return false; // Roles required but user has none
     // Simple check: user must have at least one of the required roles
     // Could be more complex (e.g., all roles required)
     return requiredRoles.includes(userRole);
 }

  // Helper to emit events via the event manager
  private async emitEvent(eventName: string, context: IWorkflowContext, payload?: Record<string, any>) {
     if (this.eventManager) {
         try {
             await this.eventManager.emit(eventName, { ...(payload || {}), workflowId: context.workflowId, definitionId: context.definitionId }, context);
         } catch (err) {
             this.logger.error(`Failed to emit event '${eventName}' for workflow ${context.workflowId}`, err as Error);
         }
     }
 }
}