import { Injectable } from '@nestjs/common';
import { ProgramRegistration, WorkflowConfiguration, WorkflowStage } from 'src/common/entities';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AppLoggerService } from 'src/common/services/logger.service';
import { WorkflowConfigurationService } from './services/workflow-configuration.service';
import { WorkflowStageService } from './services/workflow-stage.service';
import { RegistrationWorkflowStageService } from './services/registration-workflow-stage.service';
import { WorkflowValidationService } from './services/workflow-validation.service';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { handleKnownErrors } from 'src/common/utils/handle-error.util';

@Injectable()
export class WorkflowService {
  constructor(
    @InjectRepository(ProgramRegistration)
    private readonly programRegistrationRepository: Repository<ProgramRegistration>,
    private readonly logger: AppLoggerService,
    private readonly workflowConfigurationService: WorkflowConfigurationService,
    private readonly workflowStageService: WorkflowStageService,
    private readonly registrationWorkflowStageService: RegistrationWorkflowStageService,
    private readonly workflowValidationService: WorkflowValidationService,
  ) {}

  async getWorkflowByProgramType(programTypeKey: string) {
    this.logger.log('WorkflowService.getWorkflowByProgramType', { programTypeKey });
    try {
      return await this.workflowConfigurationService.getWorkflowByProgramType(programTypeKey);
    } catch (error) {
      handleKnownErrors(ERROR_CODES.WORKFLOW_CONFIGURATION_GET_FAILED, error);
    }
  }

  async getWorkflowProgress(registrationId: number) {
    this.logger.log('WorkflowService.getWorkflowProgress', { registrationId });
    try {
      const { workflow, stages } = await this.getWorkflowDetailsForRegistration(registrationId);
      // Removed initialize to make GET read-only
      const progress = await this.registrationWorkflowStageService.getByRegistrationId(registrationId);

      return {
        registrationId,
        workflowId: workflow.id,
        stages: stages.map(stage => {
          const matched = progress.find(progressItem => progressItem.workflowStage.id === stage.id);
          return {
            stageKey: stage.stageKey,
            stageName: stage.stageName,
            stageOrder: stage.stageOrder,
            status: matched?.status ?? 'pending',
            completedAt: matched?.completedAt ?? null,
          };
        }),
      };
    } catch (error) {
      handleKnownErrors(ERROR_CODES.WORKFLOW_REGISTRATION_STAGE_GET_FAILED, error);
    }
  }

  async getCurrentStage(registrationId: number) {
    this.logger.log('WorkflowService.getCurrentStage', { registrationId });
    try {
      const progressData = await this.getWorkflowProgress(registrationId);
      const currentStage = progressData.stages.find(stage => stage.status !== 'completed') ?? progressData.stages[progressData.stages.length - 1];
      return {
        registrationId,
        workflowId: progressData.workflowId,
        ...currentStage,
      };
    } catch (error) {
      handleKnownErrors(ERROR_CODES.WORKFLOW_REGISTRATION_STAGE_GET_FAILED, error);
    }
  }

  async getWorkflowContextForRegistration(registrationId: number): Promise<{
    workflowId: number;
    workflowRequiredFields: Record<string, unknown>;
    currentWorkflowStage: {
      stageKey: string;
      stageName: string;
      stageOrder: number;
      status: string;
    } | null;
  }> {
    const { registration, workflow, stages } = await this.getWorkflowDetailsForRegistration(registrationId);

    // Get workflow required fields using catalog-based variable system
    const workflowRequiredFields = await this.workflowValidationService.getWorkflowDataForRegistration(
      registrationId,
      workflow.id,
    );

    // Find current workflow stage (first not completed, or last if all completed)
    // Only consider initiated
    const progress = await this.registrationWorkflowStageService.getByRegistrationId(registrationId);
    const activeStages = progress?.filter(stage => 
      stage.status === 'initiated'
    ) ?? [];
    

    return {
      workflowId: workflow.id,
      workflowRequiredFields,
      currentWorkflowStage: activeStages.length > 0
        ? {
            stageKey: activeStages[0].workflowStage?.stageKey,
            stageName: activeStages[0].workflowStage?.stageName,
            stageOrder: activeStages[0].workflowStage?.stageOrder,
            status: activeStages[0].status,
          }
        : null,
    };
  }

  /**
   * OPTIMIZED: Batch fetch workflow context for multiple registrations
   * Avoids N+1 queries by fetching all data in bulk
   */
  async getWorkflowContextForMultipleRegistrations(registrationIds: number[]): Promise<Map<number, {
    workflowId: number;
    workflowRequiredFields: Record<string, unknown>;
    currentWorkflowStage: {
      stageKey: string;
      stageName: string;
      stageOrder: number;
      status: string;
    } | null;
  }>> {
    const contextMap = new Map();
    
    if (registrationIds.length === 0) {
      return contextMap;
    }

    try {
      // 1. Fetch all registrations with their programs and workflow configs in ONE query
      // Select only required fields to minimize data transfer
      const registrations = await this.programRegistrationRepository
        .createQueryBuilder('registration')
        .select([
          'registration.id',
          'program.id',
          'program.workflowId',
          'workflowConfiguration.id',
          'workflowConfiguration.workflowKey',
          'stages.id',
          'stages.stageKey',
          'stages.stageName',
          'stages.stageOrder'
        ])
        .leftJoin('registration.program', 'program')
        .leftJoin('program.workflowConfiguration', 'workflowConfiguration')
        .leftJoin('workflowConfiguration.stages', 'stages')
        .where('registration.id IN (:...ids)', { ids: registrationIds })
        .addOrderBy('stages.stageOrder', 'ASC')
        .getMany();

      // 2. Fetch all workflow stage progress for all registrations in ONE query
      const allProgress = await this.registrationWorkflowStageService.getByMultipleRegistrationIds(registrationIds);

      // 3. Build a map of registration ID -> progress array
      const progressMap = new Map<number, any[]>();
      allProgress.forEach(progress => {
        if (!progressMap.has(progress.registrationId)) {
          progressMap.set(progress.registrationId, []);
        }
        progressMap.get(progress.registrationId)!.push(progress);
      });

      // 4. Group registrations by workflowId to enable batch fetching
      const workflowGroups = new Map<number, number[]>();
      registrations.forEach(registration => {
        const workflowId = registration.program?.workflowConfiguration?.id;
        if (workflowId) {
          if (!workflowGroups.has(workflowId)) {
            workflowGroups.set(workflowId, []);
          }
          workflowGroups.get(workflowId)!.push(registration.id);
        }
      });

      // 5. Batch fetch workflow data per workflow (eliminates N+1)
      const workflowDataMap = new Map<number, Record<string, unknown>>();
      for (const [workflowId, regIds] of workflowGroups.entries()) {
        try {
          this.logger.log('Batch fetching workflow data', { 
            workflowId, 
            registrationCount: regIds.length 
          });
          
          const batchData = await this.workflowValidationService.getWorkflowDataForMultipleRegistrations(
            regIds,
            workflowId,
          );
          
          // Merge batch results into main map
          batchData.forEach((data, regId) => {
            workflowDataMap.set(regId, data);
          });
        } catch (err) {
          this.logger.warn('Failed to batch fetch workflow data for workflow', {
            workflowId,
            registrationIds: regIds,
            error: err?.message,
          });
          // Fill with empty objects on error
          regIds.forEach(regId => workflowDataMap.set(regId, {}));
        }
      }

      // 6. Build context map using pre-fetched workflow data
      for (const registration of registrations) {
        try {
          const workflow = registration.program?.workflowConfiguration;
          if (!workflow) {
            continue;
          }

          // Get pre-fetched workflow data (no N+1 query!)
          const workflowRequiredFields = workflowDataMap.get(registration.id) ?? {};

          // Find current active workflow stage
          const progress = progressMap.get(registration.id) ?? [];
          const activeStages = progress.filter(stage => stage.status === 'initiated');

          contextMap.set(registration.id, {
            workflowId: workflow.id,
            workflowRequiredFields,
            currentWorkflowStage: activeStages.length > 0
              ? {
                  stageKey: activeStages[0].workflowStage?.stageKey,
                  stageName: activeStages[0].workflowStage?.stageName,
                  stageOrder: activeStages[0].workflowStage?.stageOrder,
                  status: activeStages[0].status,
                }
              : null,
          });
        } catch (e) {
          this.logger.warn('Failed to build workflow context for registration', { 
            registrationId: registration.id, 
            error: e?.message 
          });
        }
      }

      return contextMap;
    } catch (error) {
      this.logger.error('Error in getWorkflowContextForMultipleRegistrations', error?.stack, { registrationIds });
      return contextMap;
    }
  }


  async syncStageByKey(registrationId: number, stageKey: string, status: 'pending' | 'completed' | 'initiated', userId?: number): Promise<void> {
    const { registration, stages, workflow } = await this.getWorkflowDetailsForRegistration(registrationId);

    const stage = stages.find(item => item.stageKey === stageKey);
    if (!stage) {
      return;
    }

    // Always set status as 'initiated' for any stage
    await this.registrationWorkflowStageService.upsertByStageKey(registrationId, stageKey, 'initiated', userId);

    // Update registration status based on stage and workflow criteria
    await this.updateRegistrationStatusBasedOnStage(registration, stageKey, workflow);
  }

  /**
   * Re-evaluate the workflow's completion criteria (registrationRules) against the registration's
   * current live data and flip it to COMPLETED when every criterion is satisfied.
   *
   * Unlike syncStageByKey, this does not move or upsert a stage — use it for events that change
   * completion-relevant data without advancing a stage, most notably payment capture/realization.
   * It is idempotent and only ever sets COMPLETED (never downgrades), so it is safe to call after
   * every payment confirmation. Programs without a workflow are a no-op.
   */
  async reevaluateRegistrationCompletion(registrationId: number): Promise<void> {
    const { registration, workflow } = await this.getWorkflowDetailsForRegistration(registrationId);
    await this.updateRegistrationStatusBasedOnStage(
      registration,
      'PAYMENT_COMPLETION_REEVAL',
      workflow,
    );
  }

  private async updateRegistrationStatusBasedOnStage(
    registration: ProgramRegistration,
    stageKey: string,
    workflow: WorkflowConfiguration,
  ): Promise<void> {
    try {
      // Get workflow data for evaluation
      const workflowData = await this.workflowValidationService.getWorkflowDataForRegistration(
        registration.id,
        workflow.id,
      );

      // Evaluate completion criteria if defined
      if (workflow.registrationRules) {
        const criteria = workflow.registrationRules as any;
        const isComplete = this.evaluateCriteria(criteria.criteria, workflowData);
        
        if (isComplete) {
          await this.programRegistrationRepository.update(
            { id: registration.id },
            { registrationStatus: 'completed' as any },
          );
          return;
        }
      }

    } catch (error) {
      this.logger.error('Failed to update registration status', error?.stack, { registrationId: registration.id, stageKey });
      // Don't throw error to avoid blocking stage sync
    }
  }

  private evaluateCriteria(criteria: any, data: Record<string, unknown>): boolean {
    if (!criteria) return false;

    // If it's an array, treat as AND (all must be true)
    if (Array.isArray(criteria)) {
      return criteria.every(criterion => this.evaluateCriteria(criterion, data));
    }

    // If it's a logical block (AND/OR)
    if (criteria.operator && Array.isArray(criteria.conditions)) {
      const { operator, conditions } = criteria;
      if (operator === 'AND') {
        return conditions.every(cond => this.evaluateCriteria(cond, data));
      } else if (operator === 'OR') {
        return conditions.some(cond => this.evaluateCriteria(cond, data));
      }
    }

    // Otherwise, it's a leaf criterion
    const { field, operator, value } = criteria;
    const fieldValue = data[field];
    switch (operator) {
      case 'EQUALS':
        return fieldValue === value;
      case 'NOT_EQUALS':
        return fieldValue !== value;
      case 'IN':
        return Array.isArray(value) && value.includes(fieldValue);
      case 'NOT_IN':
        return Array.isArray(value) && !value.includes(fieldValue);
      case 'IS_NULL':
        return fieldValue === null || fieldValue === undefined;
      case 'IS_NOT_NULL':
        return fieldValue !== null && fieldValue !== undefined;
      default:
        return false;
    }
  }
  private async getWorkflowDetailsForRegistration(registrationId: number): Promise<{
    registration: ProgramRegistration;
    workflow: WorkflowConfiguration;
    stages: WorkflowStage[];
  }> {
    const registration = await this.programRegistrationRepository.findOne({
      where: { id: registrationId },
      relations: ['program', 'program.type'],
    });

    if (!registration) {
      throw new InifniNotFoundException(ERROR_CODES.WORKFLOW_REGISTRATION_NOTFOUND, null, null, String(registrationId));
    }

    const workflowId = registration.program?.workflowId;
    if (!workflowId) {
      throw new InifniNotFoundException(ERROR_CODES.WORKFLOW_ID_MISSING_IN_PROGRAM, null, null, String(registrationId));
    }

    const workflow = await this.workflowConfigurationService.findById(workflowId);
    if (!workflow) {
      throw new InifniNotFoundException(ERROR_CODES.WORKFLOW_CONFIGURATION_NOTFOUND_BY_ID, null, null, String(workflowId));
    }

    const stages = await this.workflowStageService.getStagesByWorkflowConfigurationId(workflow.id);
    return { registration, workflow, stages };
  }
    async getWorkFlowByKey(workflowKey: string) {
    this.logger.log('WorkflowService.getWorkFlowByKey', { workflowKey });
    try {
      const workflow = await this.workflowConfigurationService.findByWorkflowKey(workflowKey);
      if (!workflow) {
        throw new InifniNotFoundException(ERROR_CODES.WORKFLOW_CONFIGURATION_NOTFOUND_BY_KEY, null, null, workflowKey);
      }
      return this.transformWorkflowEntity(workflow);
    } catch (error) {
      handleKnownErrors(ERROR_CODES.WORKFLOW_CONFIGURATION_GET_FAILED, error);
    }
  }

  async getWorkFlowById(id: number) {
    this.logger.log('WorkflowService.getWorkFlowById', { id });
    try {
      const workflow = await this.workflowConfigurationService.findById(id);
      if (!workflow) {
        throw new InifniNotFoundException(ERROR_CODES.WORKFLOW_CONFIGURATION_NOTFOUND_BY_ID, null, null, String(id));
      }
      return this.transformWorkflowEntity(workflow);
    } catch (error) {
      handleKnownErrors(ERROR_CODES.WORKFLOW_CONFIGURATION_GET_FAILED, error);
    }
  }

  private transformWorkflowEntity(workflow: WorkflowConfiguration) {
    return {
      workflowId: workflow.id,
      workflowKey: workflow.workflowKey,
      workflowName: workflow.workflowName,
      programType: workflow.programTypeKey,
      workflowVersion: workflow.workflowVersion,
      lifecycleStatus: workflow.lifecycleStatus,
      description: workflow.description,
      lastUpdated: workflow.updatedAt?.toISOString() || workflow.createdAt?.toISOString() || new Date().toISOString(),
      registrationRules: workflow.registrationRules,
      initialStage: workflow.initialStage,
      stages: workflow.stages?.map(stage => ({
        stageId: stage.stageKey.toLowerCase(),
        stageKey: stage.stageKey,
        stageName: stage.stageName,
        stageOrder: stage.stageOrder,
        stageCategory: stage.stageCategory,
        description: stage.description,
        prerequisiteRulesConfig: stage.prerequisiteRulesConfig,
        executionRulesConfig: stage.executionRulesConfig,
        actionButtons: stage.actionButtons,
        completionActionsConfig: stage.completionActionsConfig,
      })) || [],
      independentActions: workflow.actions?.reduce((acc, action) => {
        acc[action.actionKey] = {
          actionKey: action.actionKey,
          actionName: action.actionName,
          description: action.description,
          permissions: action.permissions,
          endpoints: action.endpoints,
        };
        return acc;
      }, {} as Record<string, any>) || {},
    };
  }

  transformWorkflowToDto(workflow: any) {
    if (!workflow) return null;
    return {
      workflowId: workflow.workflowId,
      workflowName: workflow.workflowName,
      workflowKey: workflow.workflowKey,
      programType: workflow.programType,
      workflowVersion: workflow.version,
      lifecycleStatus: workflow.status,
      description: workflow.description,
      lastUpdated: workflow.lastUpdated,
      registrationRules: workflow.registrationRules,
      initialStage: workflow.initialStage,
      stages: workflow.stages,
      independentActions: workflow.independentActions,
    };
  }
}