import { Op } from 'sequelize';
import { sequelize } from '../config/database';
import logger from '../utils/logger';
import { WorkflowRequest } from '../models/WorkflowRequest';
import { DealerClaimDetails } from '../models/DealerClaimDetails';
import { DealerProposalDetails } from '../models/DealerProposalDetails';
import { DealerCompletionDetails } from '../models/DealerCompletionDetails';
import { DealerProposalCostItem } from '../models/DealerProposalCostItem';
import { InternalOrder, IOStatus } from '../models/InternalOrder';
import { ClaimBudgetTracking, BudgetStatus } from '../models/ClaimBudgetTracking';
import { ClaimInvoice } from '../models/ClaimInvoice';
import { ClaimCreditNote } from '../models/ClaimCreditNote';
import { ClaimCreditNoteItem } from '../models/ClaimCreditNoteItem';
import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
import { DealerCompletionExpense } from '../models/DealerCompletionExpense';
import { ApprovalLevel } from '../models/ApprovalLevel';
import { Participant } from '../models/Participant';
import { User } from '../models/User';
import { DealerClaimHistory, SnapshotType } from '../models/DealerClaimHistory';
import { ActivityType } from '../models/ActivityType';
import { Document } from '../models/Document';
import { Dealer } from '../models/Dealer';
import { WorkflowService } from './workflow.service';
import { DealerClaimApprovalService } from './dealerClaimApproval.service';
import { buildWfmClaimCsvRow, generateRequestNumber, padDealerCode } from '../utils/helpers';
import { Priority, WorkflowStatus, ApprovalStatus, ParticipantType } from '../types/common.types';
import { sapIntegrationService } from './sapIntegration.service';
import { pwcIntegrationService } from './pwcIntegration.service';
import { wfmFileService } from './wfmFile.service';
import { findDealerLocally } from './dealer.service';
import { notificationService } from './notification.service';
import { activityService } from './activity.service';
import { UserService } from './user.service';
import { dmsIntegrationService } from './dmsIntegration.service';
import { validateDealerUser } from './userEnrichment.service';
import { sanitizeStrict, sanitizePermissive, isHtmlEmpty } from '../utils/sanitizer';
// findDealerLocally removed (duplicate)


const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';


let workflowServiceInstance: any;
let approvalServiceInstance: any;
let userServiceInstance: any;

/**
 * Dealer Claim Service
 * Handles business logic specific to dealer claim management workflow
 */
export class DealerClaimService {
  private getWorkflowService(): WorkflowService {
    if (!workflowServiceInstance) {
      const { WorkflowService } = require('./workflow.service');
      workflowServiceInstance = new WorkflowService();
    }
    return workflowServiceInstance;
  }

  private getApprovalService(): DealerClaimApprovalService {
    if (!approvalServiceInstance) {
      const { DealerClaimApprovalService } = require('./dealerClaimApproval.service');
      approvalServiceInstance = new DealerClaimApprovalService();
    }
    return approvalServiceInstance;
  }

  private getUserService(): UserService {
    if (!userServiceInstance) {
      const { UserService } = require('./user.service');
      userServiceInstance = new UserService();
    }
    return userServiceInstance;
  }

  /**
   * Create a new dealer claim request
   */
  async createClaimRequest(
    userId: string,
    claimData: {
      activityName: string;
      activityType: string;
      dealerCode: string;
      dealerName: string;
      dealerEmail?: string;
      dealerPhone?: string;
      dealerAddress?: string;
      activityDate?: Date;
      location: string;
      requestDescription: string;
      periodStartDate?: Date;
      periodEndDate?: Date;
      estimatedBudget?: number;
      approvers?: Array<{
        email: string;
        name?: string;
        userId?: string;
        level: number;
        tat?: number | string;
        tatType?: 'hours' | 'days';
        stepName?: string;
        isAdditional?: boolean;
        originalLevel?: number;
      }>;
    }
  ): Promise<WorkflowRequest> {
    const transaction = await sequelize.transaction();
    try {
      // 1. Sanitize user inputs
      const sanitizedName = sanitizeStrict(claimData.activityName);
      const sanitizedLocation = sanitizeStrict(claimData.location);
      const sanitizedDesc = sanitizePermissive(claimData.requestDescription);

      // Verify that sanitization didn't empty the required fields
      if (!sanitizedName) {
        throw new Error('Activity Name is required and must contain valid text');
      }
      if (!sanitizedLocation) {
        throw new Error('Location is required and must contain valid text');
      }
      if (isHtmlEmpty(sanitizedDesc)) {
        throw new Error('Description is required and must contain valid text');
      }

      // 2. Map and validate dealer user
      const dealerCode = claimData.dealerCode;
      
      logger.info(`[DealerClaimService] Resolving dealer mapping for code: ${dealerCode}`);
      // Fetch dealer info (this handles external lookup and resolves parentCode)
      const dealerInfo = await findDealerLocally(dealerCode);
      const userLookupCode = dealerInfo?.parentCode || dealerCode;
      
      logger.info(`[DealerClaimService] Validating dealer user for lookup code: ${userLookupCode}`);
      const dealerUser = await validateDealerUser(userLookupCode);

      // Validate Dealer Item Group against Activity Credit Posting
      const activityType = await ActivityType.findOne({ where: { title: claimData.activityType } });
      if (activityType && activityType.creditPostingOn) {
        // Fetch full dealer info (including external API data like itemGroup)
        const fullDealerInfo = await findDealerLocally(dealerCode);
        
        if (fullDealerInfo && fullDealerInfo.itemGroup) {
          const creditPostingOn = activityType.creditPostingOn.toLowerCase();
          const itemGroup = fullDealerInfo.itemGroup.toLowerCase();

          let isMatch = false;
          if (creditPostingOn === 'vehicle' && itemGroup === 'vehicle') {
            isMatch = true;
          } else if (creditPostingOn === 'spares' && itemGroup === 'spares') {
            isMatch = true;
          } else if (creditPostingOn === 'gma' && itemGroup === 'gma') {
            isMatch = true;
          } else if (creditPostingOn === 'apparel' && itemGroup === 'apparel') {
            isMatch = true;
          } else if (creditPostingOn === itemGroup) {
            isMatch = true; // Fallback for direct match
          }

          if (!isMatch) {
            logger.warn(`[DealerClaimService] Validation failed: Activity ${claimData.activityType} (${creditPostingOn}) vs Dealer ${dealerCode} (${itemGroup})`);
            throw new Error('incorrect Delercode for selected service group');
          }
          
          logger.info(`[DealerClaimService] Validation successful: Activity ${creditPostingOn} matched Dealer group ${itemGroup}`);
        }
      }

      // Update claim data with validated dealer user details if not provided
      claimData.dealerName = dealerUser.displayName || claimData.dealerName;
      claimData.dealerEmail = dealerUser.email || claimData.dealerEmail;
      claimData.dealerPhone = (dealerUser as any).mobilePhone || (dealerUser as any).phone || claimData.dealerPhone;

      // Generate request number
      const requestNumber = await generateRequestNumber();

      // Validate initiator - check if userId is a valid UUID first
      const isValidUUID = (str: string): boolean => {
        const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
        return uuidRegex.test(str);
      };

      if (!isValidUUID(userId)) {
        throw new Error(`Invalid initiator ID format. Expected UUID, got: ${userId}`);
      }

      const initiator = await User.findByPk(userId);
      if (!initiator) {
        throw new Error('Initiator not found');
      }

      // Fallback: Enrichment from local dealer table if data is missing or incomplete
      const localDealer = await findDealerLocally(claimData.dealerCode, claimData.dealerEmail);
      if (localDealer) {
        logger.info(`[DealerClaimService] Enriched claim request with local dealer data: ${localDealer.dealerCode}`);
        claimData.dealerName = claimData.dealerName || localDealer.dealerName;
        claimData.dealerEmail = claimData.dealerEmail || localDealer.dealerPrincipalEmailId || localDealer.email;
        claimData.dealerPhone = claimData.dealerPhone || localDealer.phone;
      }

      // Validate approvers array is provided
      if (!claimData.approvers || !Array.isArray(claimData.approvers) || claimData.approvers.length === 0) {
        throw new Error('Approvers array is required. Please assign approvers for all workflow steps.');
      }

      // 1. Transform approvers and ensure users exist in database
      const userService = this.getUserService();
      const transformedLevels = [];

      // Define step names mapping
      const standardStepNames: Record<number, string> = {
        1: 'Dealer Proposal Submission',
        2: 'Requestor Evaluation',
        3: 'Department Lead Approval',
        4: 'Dealer Completion Documents',
        5: 'Requestor Claim Approval'
      };

      // Determine the maximum level to correctly identify the final approver
      const maxLevel = claimData.approvers && claimData.approvers.length > 0 
        ? Math.max(...claimData.approvers.map(approver => approver.level))
        : 5;

      if (claimData.approvers) {
        let nextStandardStep = 1;
        for (const approver of claimData.approvers) {
          let approverUserId = approver.userId;
          const currentLevel = approver.level;
          
          // Determine if this is an additional approver or a standard step
          // Rule: If isAdditional flag is set OR it has a custom stepName (not in standard names), 
          // it's an insertion and doesn't consume a standard step slot.
          const isAdditional = (approver as any).isAdditional === true;
          const customName = (approver as any).stepName;
          const isStandardName = customName && Object.values(standardStepNames).includes(customName);
          
          // Determine originalLevel hint
          let originalLevel = (approver as any).originalLevel;
          if (!originalLevel) {
            if (isAdditional || (customName && !isStandardName)) {
              // This is an additional/custom step, it doesn't have a standard original level
              originalLevel = undefined;
            } else {
              // This is a standard step, take the next available standard slot
              originalLevel = nextStandardStep++;
            }
          } else if (!isAdditional) {
            // If originalLevel is explicitly provided, sync our counter
            nextStandardStep = Math.max(nextStandardStep, originalLevel + 1);
          }

          // Determine level name
          let levelName = customName; 

          if (!levelName) {
            // If not provided, use originalLevel mapping or fallback to current level mapping
            levelName = (originalLevel && standardStepNames[originalLevel]) || `Additional Approver`;
          }

          // If this is a Dealer-specific step (originalLevel 1 or 4), ensure we use the validated dealerUser
          if (originalLevel === 1 || originalLevel === 4) {
            logger.info(`[DealerClaimService] Assigning validated dealer user to ${levelName} (Original Level ${originalLevel}, Current Level ${currentLevel})`);
            approverUserId = dealerUser.userId;
            approver.email = dealerUser.email;
            approver.name = dealerUser.displayName || dealerUser.email;
          }

          // Determine if current approverUserId is a valid UUID
          const isNotUuid = approverUserId && !isValidUUID(approverUserId);

          // If userId missing or is not a valid UUID (e.g. an Okta ID), ensure user exists in local DB
          if ((!approverUserId || isNotUuid) && approver.email) {
            try {
              const user = await userService.ensureUserExists({ 
                email: approver.email,
                userId: isNotUuid ? approverUserId : undefined 
              });
              approverUserId = user.userId;
              
              if (isNotUuid) {
                logger.info(`[DealerClaimService] Resolved Okta ID ${approver.userId} to UUID ${approverUserId} for ${approver.email}`);
              }
            } catch (e) {
              logger.warn(`[DealerClaimService] Could not resolve user for email ${approver.email}:`, e);
              if (isNotUuid) {
                approverUserId = ''; 
              }
            }
          }

          let tatHours = 24; // Default
          if (approver.tat) {
            const val = typeof approver.tat === 'number' ? approver.tat : parseInt(approver.tat as string);
            tatHours = (approver as any).tatType === 'days' ? val * 24 : val;
          }

          transformedLevels.push({
            levelNumber: currentLevel,
            levelName: levelName,
            approverId: approverUserId || '', 
            approverEmail: approver.email,
            approverName: approver.name || approver.email,
            tatHours: tatHours,
            isFinalApprover: currentLevel === maxLevel
          });
        }
      }

      // 2. Transform participants
      const transformedParticipants = [
        {
          userId: userId,
          userName: initiator.displayName || initiator.email,
          userEmail: initiator.email,
          participantType: 'INITIATOR' as any,
        }
      ];

      // Add approvers as participants
      for (const level of transformedLevels) {
        if (level.approverId) {
          transformedParticipants.push({
            userId: level.approverId,
            userName: level.approverName,
            userEmail: level.approverEmail,
            participantType: 'APPROVER' as any
          });
        }
      }

      // 2. Create the workflow as a DRAFT first
      // This prevents immediate notification triggering within the transaction
      const workflowService = this.getWorkflowService();
      const workflow = await workflowService.createWorkflow(userId, {
        templateType: 'DEALER CLAIM',
        workflowType: 'CLAIM_MANAGEMENT',
        title: `Dealer Claim: ${sanitizedName} (${dealerCode})`,
        description: sanitizedDesc,
        priority: (claimData as any).priority || Priority.STANDARD,
        approvalLevels: transformedLevels,
        participants: transformedParticipants,
        isDraft: true // Create as draft initially
      } as any, { transaction, ipAddress: null, userAgent: 'System/DealerClaimService' });

      // Create claim details
      await DealerClaimDetails.create({
        requestId: workflow.requestId,
        activityName: sanitizedName,
        activityType: claimData.activityType,
        dealerCode: dealerCode,
        dealerName: claimData.dealerName,
        dealerEmail: claimData.dealerEmail,
        dealerPhone: claimData.dealerPhone,
        dealerAddress: claimData.dealerAddress,
        activityDate: claimData.activityDate,
        location: sanitizedLocation,
        periodStartDate: claimData.periodStartDate,
        periodEndDate: claimData.periodEndDate,
      } as any, { transaction });

      // Initialize budget tracking
      await ClaimBudgetTracking.upsert({
        requestId: workflow.requestId,
        initialEstimatedBudget: claimData.estimatedBudget,
        budgetStatus: BudgetStatus.DRAFT,
        currency: 'INR',
      }, { transaction });

      // 3. Commit transaction
      await transaction.commit();
      logger.info(`[DealerClaimService] Transaction committed for workflow: ${workflow.requestNumber}`);

      // 4. Submit the workflow - this triggers notifications after transaction commit
      // This ensures that NotificationService (and DealerClaimEmailService) can find the claim details
      const submittedWorkflow = await workflowService.submitWorkflow(workflow.requestId);
      
      logger.info(`[DealerClaimService] Submitted claim request: ${workflow.requestNumber}`);
      return submittedWorkflow || workflow;
    } catch (error: any) {
      if (transaction) await transaction.rollback();
      const errorDetails: any = {
        message: error.message,
        name: error.name,
      };
      if (error.errors && Array.isArray(error.errors)) {
        errorDetails.validationErrors = error.errors.map((e: any) => ({
          field: e.path,
          message: e.message,
          value: e.value,
        }));
      }
      if (error.parent) {
        errorDetails.databaseError = {
          message: error.parent.message,
          code: error.parent.code,
          detail: error.parent.detail,
        };
      }
      logger.error('[DealerClaimService] Error creating claim request:', errorDetails);
      throw error;
    }
  }

  /**
   * Create 5-step approval levels for claim management from approvers array
   * Validates and creates approval levels based on user-provided approvers
   * Note: Activity Creation, E-Invoice Generation, and Credit Note Confirmation are handled as activity logs only, not approval steps
   */
  private async createClaimApprovalLevelsFromApprovers(
    requestId: string,
    initiatorId: string,
    dealerEmail?: string,
    approvers: Array<{
      email: string;
      name?: string;
      userId?: string;
      level: number;
      tat?: number | string;
      tatType?: 'hours' | 'days';
      stepName?: string; // For additional approvers
      isAdditional?: boolean; // Flag for additional approvers
      originalStepLevel?: number; // Original step level for fixed steps
    }> = []
  ): Promise<void> {
    const initiator = await User.findByPk(initiatorId);
    if (!initiator) {
      throw new Error('Initiator not found');
    }

    // Step definitions with default TAT (only manual approval steps)
    const stepDefinitions = [
      { level: 1, name: 'Dealer Proposal Submission', defaultTat: 72, isAuto: false },
      { level: 2, name: 'Requestor Evaluation', defaultTat: 48, isAuto: false },
      { level: 3, name: 'Department Lead Approval', defaultTat: 72, isAuto: false },
      { level: 4, name: 'Dealer Completion Documents', defaultTat: 120, isAuto: false },
      { level: 5, name: 'Requestor Claim Approval', defaultTat: 48, isAuto: false },
    ];

    // Sort approvers by level to process in order
    const sortedApprovers = [...approvers].sort((a, b) => a.level - b.level);

    // Track which original steps have been processed
    const processedOriginalSteps = new Set<number>();

    // Determine the maximum level to correctly identify the final approver
    const maxLevel = approvers.length > 0 
      ? Math.max(...approvers.map(a => a.level))
      : 5;

    // Process approvers in order by their level
    for (const approver of sortedApprovers) {
      let approverId: string | null = null;
      let approverEmail = '';
      let approverName = 'System';
      let tatHours = 48; // Default TAT
      let levelName = '';

      // Check if this is a system step by email (for backwards compatibility)
      const systemEmails = [`system@${appDomain}`];
      const financeEmails = [`finance@${appDomain}`];
      const isSystemEmail = systemEmails.includes(approver.email) || financeEmails.includes(approver.email);

      let stepDef = null;

      if (approver.isAdditional) {
        // Additional approver - use stepName from frontend
        levelName = approver.stepName || 'Additional Approver';
      } else {
        // Fixed step - find by originalStepLevel first, then by matching level
        const originalLevel = approver.originalStepLevel || approver.level;
        stepDef = stepDefinitions.find(s => s.level === originalLevel);

        if (!stepDef) {
          // Try to find by current level if originalStepLevel not provided
          stepDef = stepDefinitions.find(s => s.level === approver.level);
        }

        // System steps (Activity Creation, E-Invoice Generation, Credit Note Confirmation) are no longer approval steps
        if (!stepDef && isSystemEmail) {
          logger.info(`[DealerClaimService] Skipping system step approver at level ${approver.level}`);
          continue; // Skip creating approval level for system steps
        }

        if (stepDef) {
          levelName = stepDef.name;
          processedOriginalSteps.add(stepDef.level);
        } else {
          // Fallback
          levelName = `Step ${approver.level}`;
          logger.warn(`[DealerClaimService] Could not find step definition for approver at level ${approver.level}`);
        }

        // Truncate levelName if too long (max 100 chars)
        if (levelName.length > 100) {
          levelName = levelName.substring(0, 97) + '...';
        }
      }

      // User-provided approver (fixed or additional)
      if (!approver.email) {
        throw new Error(`Approver email is required for level ${approver.level}: ${levelName}`);
      }

      // Calculate TAT in hours
      if (approver.tat) {
        const tat = Number(approver.tat);
        if (!isNaN(tat) && tat > 0) {
          tatHours = approver.tatType === 'days' ? tat * 24 : tat;
        }
      } else if (stepDef) {
        tatHours = stepDef.defaultTat;
      }

      // Ensure user exists in database (create from Okta if needed)
      let user: User | null = null;
      const isValidUUIDLocal = (str: string): boolean => {
        const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
        return uuidRegex.test(str);
      };

      // Try to find user by userId if it's a valid UUID
      if (approver.userId && isValidUUIDLocal(approver.userId)) {
        try {
          user = await User.findByPk(approver.userId);
        } catch (error: any) {
          logger.debug(`[DealerClaimService] Could not find user by userId ${approver.userId}`);
        }
      }

      // If user not found by ID (or userId was not a valid UUID), try email
      if (!user && approver.email) {
        user = await User.findOne({ where: { email: approver.email.toLowerCase() } });
        if (!user) {
          try {
            const userService = this.getUserService();
            user = await userService.ensureUserExists({
              email: approver.email.toLowerCase(),
              userId: approver.userId, 
            }) as any;
          } catch (oktaError: any) {
            logger.error(`[DealerClaimService] Failed to sync user from Okta: ${approver.email}`, oktaError);
            throw new Error(`User email '${approver.email}' not found in organization directory.`);
          }
        }
      }

      if (!user) {
        throw new Error(`Could not resolve user for level ${approver.level}: ${approver.email}`);
      }

      approverId = user.userId;
      approverEmail = user.email;
      approverName = approver.name || user.displayName || user.email || 'Approver';

      const now = new Date();
      const isStep1 = approver.level === 1;

      try {
        await ApprovalLevel.create({
          requestId,
          levelNumber: approver.level,
          levelName: levelName,
          approverId: approverId,
          approverEmail: approverEmail || '',
          approverName: approverName || 'Unknown',
          tatHours: tatHours || 0,
          status: ApprovalStatus.PENDING,
          isFinalApprover: approver.level === maxLevel,
          elapsedHours: 0,
          remainingHours: tatHours || 0,
          tatPercentageUsed: 0,
          levelStartTime: isStep1 ? now : undefined,
          tatStartTime: isStep1 ? now : undefined,
        } as any);
      } catch (createError: any) {
        logger.error(`[DealerClaimService] Failed to create approval level for level ${approver.level}:`, createError);
        throw new Error(`Failed to create approval level ${approver.level} (${levelName}): ${createError.message}`);
      }
    }

    // Validate that required fixed steps were processed
    for (const step of stepDefinitions) {
      if (!processedOriginalSteps.has(step.level)) {
        logger.warn(`[DealerClaimService] Required step ${step.level} (${step.name}) was not found`);
      }
    }
  }

  /**
   * Create participants for claim management workflow
   * Includes: Initiator, Dealer, Department Lead, Finance Approver
   * Excludes: System users
   */
  private async createClaimParticipants(
    requestId: string,
    initiatorId: string,
    dealerEmail?: string
  ): Promise<void> {
    try {
      const initiator = await User.findByPk(initiatorId);
      if (!initiator) {
        throw new Error('Initiator not found');
      }

      // Get all approval levels to extract approvers
      const approvalLevels = await ApprovalLevel.findAll({
        where: { requestId },
        order: [['levelNumber', 'ASC']],
      });

      const participantsToAdd: Array<{
        userId: string;
        userEmail: string;
        userName: string;
        participantType: ParticipantType;
      }> = [];

      // 1. Add Initiator
      participantsToAdd.push({
        userId: initiatorId,
        userEmail: initiator.email,
        userName: initiator.displayName || initiator.email || 'Initiator',
        participantType: ParticipantType.INITIATOR,
      });

      // 2. Add Dealer (treated as Okta/internal user - sync from Okta if needed)
      if (dealerEmail && dealerEmail.toLowerCase() !== `system@${appDomain}`) {
        let dealerUser = await User.findOne({
          where: { email: dealerEmail.toLowerCase() },
        });

        if (!dealerUser) {
          logger.info(`[DealerClaimService] Dealer ${dealerEmail} not found in DB for participants, syncing from Okta`);
          try {
            const userService = this.getUserService();
            dealerUser = await userService.ensureUserExists({
              email: dealerEmail.toLowerCase(),
            }) as any;
            logger.info(`[DealerClaimService] Successfully synced dealer ${dealerEmail} from Okta for participants`);
          } catch (oktaError: any) {
            logger.error(`[DealerClaimService] Failed to sync dealer from Okta for participants: ${dealerEmail}`, oktaError);
            // Don't throw - dealer might be added later, but log the error
            logger.warn(`[DealerClaimService] Skipping dealer participant creation for ${dealerEmail}`);
          }
        }

        if (dealerUser) {
          participantsToAdd.push({
            userId: dealerUser.userId,
            userEmail: dealerUser.email,
            userName: dealerUser.displayName || dealerUser.email || 'Dealer',
            participantType: ParticipantType.APPROVER,
          });
        }
      }

      // 3. Add all approvers from approval levels (excluding system and duplicates)
      const addedUserIds = new Set<string>([initiatorId]);
      const systemEmails = [`system@${appDomain}`];

      for (const level of approvalLevels) {
        const approverEmail = (level as any).approverEmail?.toLowerCase();
        const approverId = (level as any).approverId;

        // Skip if system user or already added
        if (
          !approverId ||
          systemEmails.includes(approverEmail || '') ||
          addedUserIds.has(approverId)
        ) {
          continue;
        }

        // Skip if email is system email
        if (approverEmail && systemEmails.includes(approverEmail)) {
          continue;
        }

        // Helper function to check if a string is a valid UUID
        const isValidUUID = (str: string): boolean => {
          const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
          return uuidRegex.test(str);
        };

        // Only try to find user if approverId is a valid UUID
        if (!approverId || !isValidUUID(approverId)) {
          logger.warn(`[DealerClaimService] Missing or invalid UUID format for approverId: ${approverId}, attempting to resolve by email: ${approverEmail}`);
          
          if (approverEmail) {
            try {
              const userService = this.getUserService();
              const user = await userService.ensureUserExists({ 
                email: approverEmail.toLowerCase(),
                userId: approverId && !isValidUUID(approverId) ? approverId : undefined 
              });
              // Update with resolved UUID
              const resolvedUserId = user.userId;
              
              if (resolvedUserId && isValidUUID(resolvedUserId)) {
                participantsToAdd.push({
                  userId: resolvedUserId,
                  userEmail: user.email,
                  userName: user.displayName || user.email || 'Approver',
                  participantType: ParticipantType.APPROVER,
                });
                addedUserIds.add(resolvedUserId);
                continue;
              }
            } catch (err) {
              logger.error(`[DealerClaimService] Failed to resolve participant by email ${approverEmail}:`, err);
            }
          }
          continue;
        }

        const approverUser = await User.findByPk(approverId);
        if (approverUser) {
          participantsToAdd.push({
            userId: approverId,
            userEmail: approverUser.email,
            userName: approverUser.displayName || approverUser.email || 'Approver',
            participantType: ParticipantType.APPROVER,
          });
          addedUserIds.add(approverId);
        }
      }

      // Create participants (deduplicate by userId)
      const participantMap = new Map<string, typeof participantsToAdd[0]>();
      const rolePriority: Record<string, number> = {
        'INITIATOR': 3,
        'APPROVER': 2,
        'SPECTATOR': 1,
      };

      for (const participantData of participantsToAdd) {
        const existing = participantMap.get(participantData.userId);
        if (existing) {
          // Keep higher priority role
          const existingPriority = rolePriority[existing.participantType] || 0;
          const newPriority = rolePriority[participantData.participantType] || 0;
          if (newPriority > existingPriority) {
            participantMap.set(participantData.userId, participantData);
          }
        } else {
          participantMap.set(participantData.userId, participantData);
        }
      }

      // Create participant records
      for (const participantData of participantMap.values()) {
        await Participant.create({
          requestId,
          userId: participantData.userId,
          userEmail: participantData.userEmail,
          userName: participantData.userName,
          participantType: participantData.participantType,
          canComment: true,
          canViewDocuments: true,
          canDownloadDocuments: true,
          notificationEnabled: true,
          addedBy: initiatorId,
          isActive: true,
        } as any);
      }

      logger.info(`[DealerClaimService] Created ${participantMap.size} participants for claim request ${requestId}`);
    } catch (error) {
      logger.error('[DealerClaimService] Error creating participants:', error);
      // Don't throw - participants are not critical for request creation
    }
  }

  /**
   * Resolve Department Lead based on initiator's department/manager
   * If multiple users found with same department, uses the first one
   */
  /**
   * Resolve Department Lead/Manager by searching Okta using manager's displayName
   * Flow:
   * 1. Get manager displayName from initiator's user record
   * 2. Search Okta directory by displayName
   * 3. If empty: Return null (no manager found, fallback to old method)
   * 4. If single: Use that user, create in DB if doesn't exist, return user
   * 5. If multiple: Throw error with list of users (frontend will show confirmation)
   * 
   * @param initiator - The user creating the claim request
   * @returns User object for department lead/manager, or null if not found
   * @throws Error if multiple managers found (frontend should handle confirmation)
   */
  private async resolveDepartmentLeadFromManager(initiator: User): Promise<User | null> {
    try {
      // Get manager displayName from initiator's user record
      const managerDisplayName = initiator.manager; // This is the displayName of the manager

      if (!managerDisplayName) {
        logger.warn(`[DealerClaimService] Initiator ${initiator.email} has no manager displayName set`);
        // Return null - caller will handle the error
        return null;
      }

      logger.info(`[DealerClaimService] Searching Okta for manager with displayName: "${managerDisplayName}"`);

      // Search Okta by displayName
      const userService = this.getUserService();
      const oktaUsers = await userService.searchOktaByDisplayName(managerDisplayName);

      if (oktaUsers.length === 0) {
        logger.warn(`[DealerClaimService] No reporting manager found in Okta for displayName: "${managerDisplayName}"`);
        // Return null - caller will handle the error
        return null;
      }

      if (oktaUsers.length === 1) {
        // Single match - use this user
        const oktaUser = oktaUsers[0];
        const managerEmail = oktaUser.profile.email || oktaUser.profile.login;

        logger.info(`[DealerClaimService] Found single manager match: ${managerEmail} for displayName: "${managerDisplayName}"`);

        // Check if user exists in DB, create if doesn't exist
        const managerUser = await userService.ensureUserExists({
          userId: oktaUser.id,
          email: managerEmail,
          displayName: oktaUser.profile.displayName || `${oktaUser.profile.firstName || ''} ${oktaUser.profile.lastName || ''}`.trim(),
          firstName: oktaUser.profile.firstName,
          lastName: oktaUser.profile.lastName,
          department: oktaUser.profile.department,
          phone: oktaUser.profile.mobilePhone,
        });

        return managerUser;
      }

      // Multiple matches - throw error with list for frontend confirmation
      const managerOptions = oktaUsers.map(u => ({
        userId: u.id,
        email: u.profile.email || u.profile.login,
        displayName: u.profile.displayName || `${u.profile.firstName || ''} ${u.profile.lastName || ''}`.trim(),
        firstName: u.profile.firstName,
        lastName: u.profile.lastName,
        department: u.profile.department,
      }));

      logger.warn(`[DealerClaimService] Multiple managers found (${oktaUsers.length}) for displayName: "${managerDisplayName}"`);

      // Create a custom error with the manager options
      const error: any = new Error(`Multiple reporting managers found. Please select one.`);
      error.code = 'MULTIPLE_MANAGERS_FOUND';
      error.managers = managerOptions;
      throw error;

    } catch (error: any) {
      // If it's our custom multiple managers error, re-throw it
      if (error.code === 'MULTIPLE_MANAGERS_FOUND') {
        throw error;
      }

      // For other errors, log and fallback to old method
      logger.error(`[DealerClaimService] Error resolving manager from Okta:`, error);
      return await this.resolveDepartmentLead(initiator);
    }
  }

  /**
   * Legacy method: Resolve Department Lead using old logic
   * Kept as fallback when Okta search fails or manager displayName not set
   */
  private async resolveDepartmentLead(initiator: User): Promise<User | null> {
    try {
      const { Op } = await import('sequelize');

      logger.info(`[DealerClaimService] Resolving department lead for initiator: ${initiator.email}, department: ${initiator.department}, manager: ${initiator.manager}`);

      // Priority 1: Find user with MANAGEMENT role in same department
      if (initiator.department) {
        const deptLeads = await User.findAll({
          where: {
            department: initiator.department,
            role: 'MANAGEMENT' as any,
            isActive: true,
          },
          order: [['createdAt', 'ASC']], // Get first one if multiple
          limit: 1,
        });
        if (deptLeads.length > 0) {
          logger.info(`[DealerClaimService] Found department lead by MANAGEMENT role: ${deptLeads[0].email} for department: ${initiator.department}`);
          return deptLeads[0];
        } else {
          logger.debug(`[DealerClaimService] No MANAGEMENT role user found in department: ${initiator.department}`);
        }
      } else {
        logger.debug(`[DealerClaimService] Initiator has no department set`);
      }

      // Priority 2: Find users with "Department Lead", "Team Lead", "Team Manager", "Group Manager", "Assistant Manager", "Deputy Manager" in designation, same department
      if (initiator.department) {
        const leads = await User.findAll({
          where: {
            department: initiator.department,
            designation: {
              [Op.or]: [
                { [Op.iLike]: '%department lead%' },
                { [Op.iLike]: '%departmentlead%' },
                { [Op.iLike]: '%dept lead%' },
                { [Op.iLike]: '%deptlead%' },
                { [Op.iLike]: '%team lead%' },
                { [Op.iLike]: '%team manager%' },
                { [Op.iLike]: '%group manager%' },
                { [Op.iLike]: '%assistant manager%' },
                { [Op.iLike]: '%deputy manager%' },
                { [Op.iLike]: '%lead%' },
                { [Op.iLike]: '%head%' },
                { [Op.iLike]: '%manager%' },
              ],
            } as any,
            isActive: true,
          },
          order: [['createdAt', 'ASC']], // Get first one if multiple
          limit: 1,
        });
        if (leads.length > 0) {
          logger.info(`[DealerClaimService] Found lead by designation: ${leads[0].email} (designation: ${leads[0].designation})`);
          return leads[0];
        }
      }

      // Priority 3: Use initiator's manager field
      if (initiator.manager) {
        const manager = await User.findOne({
          where: {
            email: initiator.manager,
            isActive: true,
          },
        });
        if (manager) {
          logger.info(`[DealerClaimService] Using initiator's manager as department lead: ${manager.email}`);
          return manager;
        }
      }

      // Priority 4: Find any user in same department (fallback - use first one)
      if (initiator.department) {
        const anyDeptUser = await User.findOne({
          where: {
            department: initiator.department,
            isActive: true,
            userId: { [Op.ne]: initiator.userId }, // Exclude initiator
          },
          order: [['createdAt', 'ASC']],
        });
        if (anyDeptUser) {
          logger.warn(`[DealerClaimService] Using first available user in department as fallback: ${anyDeptUser.email} (designation: ${anyDeptUser.designation}, role: ${anyDeptUser.role})`);
          return anyDeptUser;
        } else {
          logger.debug(`[DealerClaimService] No other users found in department: ${initiator.department}`);
        }
      }

      // Priority 5: Search across all departments for users with "Department Lead" designation
      logger.debug(`[DealerClaimService] Trying to find any user with "Department Lead" designation...`);
      const anyDeptLead = await User.findOne({
        where: {
          designation: {
            [Op.iLike]: '%department lead%',
          } as any,
          isActive: true,
          userId: { [Op.ne]: initiator.userId }, // Exclude initiator
        },
        order: [['createdAt', 'ASC']],
      });
      if (anyDeptLead) {
        logger.warn(`[DealerClaimService] Found user with "Department Lead" designation across all departments: ${anyDeptLead.email} (department: ${anyDeptLead.department})`);
        return anyDeptLead;
      }

      // Priority 6: Find any user with MANAGEMENT role (across all departments)
      logger.debug(`[DealerClaimService] Trying to find any user with MANAGEMENT role...`);
      const anyManagementUser = await User.findOne({
        where: {
          role: 'MANAGEMENT' as any,
          isActive: true,
          userId: { [Op.ne]: initiator.userId }, // Exclude initiator
        },
        order: [['createdAt', 'ASC']],
      });
      if (anyManagementUser) {
        logger.warn(`[DealerClaimService] Found user with MANAGEMENT role across all departments: ${anyManagementUser.email} (department: ${anyManagementUser.department})`);
        return anyManagementUser;
      }

      // Priority 7: Find any user with ADMIN role (across all departments)
      logger.debug(`[DealerClaimService] Trying to find any user with ADMIN role...`);
      const anyAdminUser = await User.findOne({
        where: {
          role: 'ADMIN' as any,
          isActive: true,
          userId: { [Op.ne]: initiator.userId }, // Exclude initiator
        },
        order: [['createdAt', 'ASC']],
      });
      if (anyAdminUser) {
        logger.warn(`[DealerClaimService] Found user with ADMIN role as fallback: ${anyAdminUser.email} (department: ${anyAdminUser.department})`);
        return anyAdminUser;
      }

      logger.warn(`[DealerClaimService] Could not resolve department lead for initiator: ${initiator.email} (department: ${initiator.department || 'NOT SET'}, manager: ${initiator.manager || 'NOT SET'})`);
      logger.warn(`[DealerClaimService] No suitable department lead found. Please ensure:`);
      logger.warn(`[DealerClaimService] 1. Initiator has a department set: ${initiator.department || 'MISSING'}`);
      logger.warn(`[DealerClaimService] 2. There is at least one user with MANAGEMENT role in the system`);
      logger.warn(`[DealerClaimService] 3. Initiator's manager field is set: ${initiator.manager || 'MISSING'}`);
      return null;
    } catch (error) {
      logger.error('[DealerClaimService] Error resolving department lead:', error);
      return null;
    }
  }

  /**
   * Resolve Finance Team approver for Step 8
   */
  private async resolveFinanceApprover(): Promise<User | null> {
    try {
      const { Op } = await import('sequelize');

      // Priority 1: Find user with department containing "Finance" and MANAGEMENT role
      const financeManager = await User.findOne({
        where: {
          department: {
            [Op.iLike]: '%finance%',
          } as any,
          role: 'MANAGEMENT' as any,
        },
        order: [['createdAt', 'DESC']],
      });
      if (financeManager) {
        logger.info(`[DealerClaimService] Found finance manager: ${financeManager.email}`);
        return financeManager;
      }

      // Priority 2: Find user with designation containing "Finance" or "Accountant"
      const financeUser = await User.findOne({
        where: {
          [Op.or]: [
            { designation: { [Op.iLike]: '%finance%' } as any },
            { designation: { [Op.iLike]: '%accountant%' } as any },
          ],
        },
        order: [['createdAt', 'DESC']],
      });
      if (financeUser) {
        logger.info(`[DealerClaimService] Found finance user by designation: ${financeUser.email}`);
        return financeUser;
      }

      // Priority 3: Check admin configurations for finance team email
      const { getConfigValue } = await import('./configReader.service');
      const financeEmail = await getConfigValue('FINANCE_TEAM_EMAIL');
      if (financeEmail) {
        const financeUserByEmail = await User.findOne({
          where: { email: financeEmail },
        });
        if (financeUserByEmail) {
          logger.info(`[DealerClaimService] Found finance user from config: ${financeEmail}`);
          return financeUserByEmail;
        }
      }

      logger.warn('[DealerClaimService] Could not resolve finance approver, will use default email');
      return null;
    } catch (error) {
      logger.error('[DealerClaimService] Error resolving finance approver:', error);
      return null;
    }
  }

  /**
   * Get claim details with all related data
   */
  async getClaimDetails(requestId: string): Promise<any> {
    try {
      const request = await WorkflowRequest.findByPk(requestId, {
        include: [
          { model: User, as: 'initiator' },
          { model: ApprovalLevel, as: 'approvalLevels' },
        ]
      });

      if (!request) {
        throw new Error('Request not found');
      }

      // Handle backward compatibility: workflowType may be undefined in old environments
      const workflowType = request.workflowType || 'NON_TEMPLATIZED';
      if (workflowType !== 'CLAIM_MANAGEMENT') {
        throw new Error('Request is not a claim management request');
      }

      // Fetch related claim data separately
      const claimDetails = await DealerClaimDetails.findOne({
        where: { requestId }
      });

      const proposalDetails = await DealerProposalDetails.findOne({
        where: { requestId },
        include: [
          {
            model: DealerProposalCostItem,
            as: 'costItems',
            required: false,
            separate: true, // Use separate query for ordering
            order: [['itemOrder', 'ASC']]
          }
        ]
      });

      const completionDetails = await DealerCompletionDetails.findOne({
        where: { requestId }
      });

      // Fetch Internal Order details
      const internalOrders = await InternalOrder.findAll({
        where: { requestId },
        include: [
          { model: User, as: 'organizer', required: false }
        ],
        order: [['createdAt', 'ASC']]
      });

      // Serialize claim details to ensure proper field names
      let serializedClaimDetails = null;
      if (claimDetails) {
        serializedClaimDetails = (claimDetails as any).toJSON ? (claimDetails as any).toJSON() : claimDetails;

        // Fetch default GST rate and taxation type from ActivityType table
        try {
          const activityTypeTitle = (claimDetails.activityType || '').trim();
          logger.info(`[DealerClaimService] Resolving taxationType for activity: "${activityTypeTitle}"`);

          let activity = await ActivityType.findOne({
            where: { title: activityTypeTitle }
          });

          // Fallback 1: Try normalized title (handling en-dash vs hyphen)
          if (!activity && activityTypeTitle) {
            const normalizedTitle = activityTypeTitle.replace(/–/g, '-');
            if (normalizedTitle !== activityTypeTitle) {
              activity = await ActivityType.findOne({
                where: { title: normalizedTitle }
              });
            }
          }

          // Fallback 2: Handle cases where activity is found but taxationType is missing, or activity not found
          if (activity && activity.taxationType) {
            const isNonGstStatus = activity.taxationType.toLowerCase().includes('non');
            serializedClaimDetails.defaultGstRate = isNonGstStatus ? 0 : 18;
            serializedClaimDetails.taxationType = activity.taxationType;
            logger.info(`[DealerClaimService] Resolved from ActivityType record: ${activity.taxationType}`);
          } else {
            // Infer from title if record is missing or incomplete
            const isNonGst = activityTypeTitle.toLowerCase().includes('non');
            serializedClaimDetails.taxationType = isNonGst ? 'Non GST' : 'GST';
            serializedClaimDetails.defaultGstRate = isNonGst ? 0 : 18;

            logger.info(`[DealerClaimService] Inferred taxationType from title: ${serializedClaimDetails.taxationType} (Activity record ${activity ? 'found but missing taxationType' : 'not found'})`);
          }
        } catch (error) {
          logger.warn(`[DealerClaimService] Error fetching activity type for ${claimDetails.activityType}:`, error);
          serializedClaimDetails.defaultGstRate = 18;
          serializedClaimDetails.taxationType = 'GST'; // Safe default
        }


        serializedClaimDetails.internalOrders = internalOrders.map(io => (io as any).toJSON ? (io as any).toJSON() : io);
        // Maintain backward compatibility for single internalOrder field (using first one)
        serializedClaimDetails.internalOrder = internalOrders.length > 0 ? (internalOrders[0] as any).toJSON ? (internalOrders[0] as any).toJSON() : internalOrders[0] : null;

        // Fetch dealer details (GSTIN, State, City) from dealers table / external API enrichment
        try {
          const dealer = await findDealerLocally(claimDetails.dealerCode);

          if (dealer) {

            serializedClaimDetails.dealerGstin = dealer.gstin || null;
            serializedClaimDetails.dealerGSTIN = dealer.gstin || null; // Backward compatibility
            serializedClaimDetails.dealerState = dealer.state || null;
            serializedClaimDetails.dealerCity = dealer.city || null;

            logger.info(`[DealerClaimService] Enriched claim details with dealer info for ${claimDetails.dealerCode}: GSTIN=${dealer.gstin}, State=${dealer.state}`);
          }
        } catch (dealerError) {
          logger.warn(`[DealerClaimService] Error fetching dealer details for ${claimDetails.dealerCode}:`, dealerError);
        }

      }

      // Transform proposal details to include cost items as array
      let transformedProposalDetails = null;
      if (proposalDetails) {
        const proposalData = (proposalDetails as any).toJSON ? (proposalDetails as any).toJSON() : proposalDetails;

        // Get cost items from separate table (dealer_proposal_cost_items)
        let costBreakup: any[] = [];
        if (proposalData.costItems && Array.isArray(proposalData.costItems) && proposalData.costItems.length > 0) {
          // Use cost items from separate table
          costBreakup = proposalData.costItems.map((item: any) => ({
            description: item.itemDescription || item.description,
            amount: Number(item.amount) || 0,
            quantity: Number(item.quantity) || 1,
            hsnCode: item.hsnCode || '',
            gstRate: Number(item.gstRate) || 0,
            gstAmt: Number(item.gstAmt) || 0,
            cgstRate: Number(item.cgstRate) || 0,
            cgstAmt: Number(item.cgstAmt) || 0,
            sgstRate: Number(item.sgstRate) || 0,
            sgstAmt: Number(item.sgstAmt) || 0,
            igstRate: Number(item.igstRate) || 0,
            igstAmt: Number(item.igstAmt) || 0,
            utgstRate: Number(item.utgstRate) || 0,
            utgstAmt: Number(item.utgstAmt) || 0,
            cessRate: Number(item.cessRate) || 0,
            cessAmt: Number(item.cessAmt) || 0,
            totalAmt: Number(item.totalAmt) || 0,
            isService: !!item.isService
          }));
        }
        // Note: costBreakup JSONB field has been removed - only using separate table now

        transformedProposalDetails = {
          ...proposalData,
          costBreakup, // Always return as array for frontend compatibility
          costItems: proposalData.costItems || [] // Also include raw cost items
        };
      }

      // Serialize completion details
      let serializedCompletionDetails = null;
      if (completionDetails) {
        serializedCompletionDetails = (completionDetails as any).toJSON ? (completionDetails as any).toJSON() : completionDetails;
      }

      // Serialize internal order details
      const serializedInternalOrders = internalOrders.map(io => (io as any).toJSON ? (io as any).toJSON() : io);
      // For backward compatibility, also provide serializedInternalOrder (first one)
      const serializedInternalOrder = serializedInternalOrders.length > 0 ? serializedInternalOrders[0] : null;

      // Fetch Budget Tracking details
      const budgetTracking = await ClaimBudgetTracking.findOne({
        where: { requestId }
      });

      // Fetch Invoice details
      const claimInvoice = await ClaimInvoice.findOne({
        where: { requestId }
      });

      // Fetch Credit Note details with items
      const claimCreditNote = await ClaimCreditNote.findOne({
        where: { requestId },
        include: [{ model: ClaimCreditNoteItem, as: 'items' }]
      });

      // Fetch Completion Expenses (individual expense items)
      const completionExpenses = await DealerCompletionExpense.findAll({
        where: { requestId },
        order: [['createdAt', 'ASC']]
      });

      // Serialize new tables
      let serializedBudgetTracking = null;
      if (budgetTracking) {
        serializedBudgetTracking = (budgetTracking as any).toJSON ? (budgetTracking as any).toJSON() : budgetTracking;
      }

      let serializedInvoice = null;
      if (claimInvoice) {
        serializedInvoice = (claimInvoice as any).toJSON ? (claimInvoice as any).toJSON() : claimInvoice;
      }

      let serializedCreditNote = null;
      if (claimCreditNote) {
        serializedCreditNote = (claimCreditNote as any).toJSON ? (claimCreditNote as any).toJSON() : claimCreditNote;
      }

      // Transform completion expenses to array format for frontend
      const expensesBreakdown = completionExpenses.map((expense: any) => {
        const expenseData = expense.toJSON ? expense.toJSON() : expense;
        return {
          description: expenseData.description || '',
          amount: Number(expenseData.amount) || 0,
          gstRate: Number(expenseData.gstRate) || 0,
          gstAmt: Number(expenseData.gstAmt) || 0,
          cgstAmt: Number(expenseData.cgstAmt) || 0,
          sgstAmt: Number(expenseData.sgstAmt) || 0,
          igstAmt: Number(expenseData.igstAmt) || 0,
          totalAmt: Number(expenseData.totalAmt) || 0,
          expenseDate: expenseData.expenseDate
        };
      });

      return {
        request: (request as any).toJSON ? (request as any).toJSON() : request,
        claimDetails: serializedClaimDetails,
        proposalDetails: transformedProposalDetails,
        completionDetails: serializedCompletionDetails,
        internalOrder: serializedInternalOrder,
        internalOrders: serializedInternalOrders, // Return full list for UI
        // New normalized tables
        budgetTracking: serializedBudgetTracking,
        invoice: serializedInvoice,
        creditNote: serializedCreditNote,
        completionExpenses: expensesBreakdown, // Array of expense items
      };
    } catch (error) {
      logger.error('[DealerClaimService] Error getting claim details:', error);
      throw error;
    }
  }

  /**
   * Submit dealer proposal (Step 1)
   */
  async submitDealerProposal(
    requestId: string,
    proposalData: {
      proposalDocumentPath?: string;
      proposalDocumentUrl?: string;
      costBreakup: any[];
      totalEstimatedBudget: number;
      timelineMode: 'date' | 'days';
      expectedCompletionDate?: Date;
      expectedCompletionDays?: number;
      dealerComments: string;
    },
    dealerUserId?: string // Optional dealer user ID for history tracking
  ): Promise<void> {
    try {
      const request = await WorkflowRequest.findByPk(requestId);
      if (!request || request.workflowType !== 'CLAIM_MANAGEMENT') {
        throw new Error('Invalid claim request');
      }

      // Get dealer user ID if not provided - try to find by dealer email from claim details
      let actualDealerUserId: string | null = dealerUserId || null;
      if (!actualDealerUserId) {
        const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
        if (claimDetails?.dealerEmail) {
          const dealerUser = await User.findOne({
            where: { email: claimDetails.dealerEmail }
          });
          actualDealerUserId = dealerUser?.userId || null;
        }
      }

      if (request.currentLevel !== 1) {
        throw new Error('Proposal can only be submitted at step 1');
      }

      // Save proposal details (costBreakup removed - now using separate table)
      const [proposal] = await DealerProposalDetails.upsert({
        requestId,
        proposalDocumentPath: proposalData.proposalDocumentPath,
        proposalDocumentUrl: proposalData.proposalDocumentUrl,
        // costBreakup field removed - now using dealer_proposal_cost_items table
        totalEstimatedBudget: proposalData.totalEstimatedBudget,
        timelineMode: proposalData.timelineMode,
        expectedCompletionDate: proposalData.expectedCompletionDate,
        expectedCompletionDays: proposalData.expectedCompletionDays,
        dealerComments: proposalData.dealerComments,
        submittedAt: new Date(),
      }, {
        returning: true
      });

      // Get proposalId - handle both Sequelize instance and plain object
      let proposalId = (proposal as any).proposalId
        || (proposal as any).proposal_id;

      // If not found, try getDataValue method
      if (!proposalId && (proposal as any).getDataValue) {
        proposalId = (proposal as any).getDataValue('proposalId');
      }

      // If still not found, fetch the proposal by requestId
      if (!proposalId) {
        const existingProposal = await DealerProposalDetails.findOne({
          where: { requestId }
        });
        if (existingProposal) {
          proposalId = (existingProposal as any).proposalId
            || (existingProposal as any).proposal_id
            || ((existingProposal as any).getDataValue ? (existingProposal as any).getDataValue('proposalId') : null);
        }
      }

      if (!proposalId) {
        throw new Error('Failed to get proposal ID after saving proposal details');
      }

      // Clear existing cost items for this proposal (in case of update)
      await DealerProposalCostItem.destroy({
        where: { proposalId }
      });

      // Insert new cost items
      const costItems = proposalData.costBreakup.map((item: any, index: number) => ({
        proposalId,
        requestId,
        itemDescription: item.description || item.itemDescription || '',
        amount: Number(item.amount) || 0,
        quantity: Number(item.quantity) || 1,
        hsnCode: item.hsnCode || '',
        gstRate: Number(item.gstRate) || 0,
        gstAmt: Number(item.gstAmt) || 0,
        cgstRate: Number(item.cgstRate) || 0,
        cgstAmt: Number(item.cgstAmt) || 0,
        sgstRate: Number(item.sgstRate) || 0,
        sgstAmt: Number(item.sgstAmt) || 0,
        igstRate: Number(item.igstRate) || 0,
        igstAmt: Number(item.igstAmt) || 0,
        utgstRate: Number(item.utgstRate) || 0,
        utgstAmt: Number(item.utgstAmt) || 0,
        cessRate: Number(item.cessRate) || 0,
        cessAmt: Number(item.cessAmt) || 0,
        totalAmt: Number(item.totalAmt) || Number(item.amount) || 0,
        isService: !!item.isService,
        itemOrder: index
      }));

      await DealerProposalCostItem.bulkCreate(costItems);
      logger.info(`[DealerClaimService] Saved ${costItems.length} cost items for proposal ${proposalId}`);

      // Calculate total proposed taxable amount for IO blocking
      const totalProposedTaxableAmount = proposalData.costBreakup.reduce((sum: number, item: any) => {
        const amount = Number(item.amount) || 0;
        const quantity = Number(item.quantity) || 1;
        return sum + (amount * quantity);
      }, 0);

      // Update taxable amount in DealerClaimDetails for IO blocking reference
      await DealerClaimDetails.update(
        { totalProposedTaxableAmount },
        { where: { requestId } }
      );

      // Update budget tracking with proposed expenses
      await ClaimBudgetTracking.upsert({
        requestId,
        proposalEstimatedBudget: proposalData.totalEstimatedBudget,
        proposalSubmittedAt: new Date(),
        budgetStatus: BudgetStatus.PROPOSED,
        currency: 'INR',
      });

      // Approve Dealer Proposal Submission step dynamically (by levelName, not hardcoded step number)
      let dealerProposalLevel = await ApprovalLevel.findOne({
        where: {
          requestId,
          levelName: 'Dealer Proposal Submission'
        }
      });

      // Fallback: try to find by levelNumber 1 (for backwards compatibility)
      if (!dealerProposalLevel) {
        dealerProposalLevel = await ApprovalLevel.findOne({
          where: { requestId, levelNumber: 1 }
        });
      }

      if (dealerProposalLevel) {
        // Use dealer's comment if provided, otherwise use default message
        const approvalComment = proposalData.dealerComments?.trim()
          ? proposalData.dealerComments.trim()
          : 'Dealer proposal submitted';

        // Perform the approval action FIRST - only save snapshot if action succeeds
        const approvalService = this.getApprovalService();
        await approvalService.approveLevel(
          dealerProposalLevel.levelId,
          { action: 'APPROVE', comments: approvalComment },
          actualDealerUserId || (request as any).initiatorId || 'system', // Use dealer or initiator ID
          { ipAddress: null, userAgent: null }
        );

        // Save proposal history AFTER approval succeeds (this is the only snapshot needed for dealer submission)
        // Use dealer user ID if available, otherwise use initiator ID as fallback
        const historyUserId = actualDealerUserId || (request as any).initiatorId || null;
        if (!historyUserId) {
          logger.warn(`[DealerClaimService] No user ID available for proposal history, skipping history save`);
        } else {
          try {
            await this.saveProposalHistory(
              requestId,
              dealerProposalLevel.levelId,
              dealerProposalLevel.levelNumber,
              `Proposal Submitted: ${approvalComment}`,
              historyUserId
            );
            // Note: We don't save workflow history here - proposal history is sufficient
            // Workflow history will be saved when the level is approved and moves to next level
          } catch (snapshotError) {
            // Log error but don't fail the submission - snapshot is for audit, not critical
            logger.error(`[DealerClaimService] Failed to save proposal history snapshot (non-critical):`, snapshotError);
          }
        }
      }

      logger.info(`[DealerClaimService] Dealer proposal submitted for request: ${requestId}`);
    } catch (error) {
      logger.error('[DealerClaimService] Error submitting dealer proposal:', error);
      throw error;
    }
  }

  /**
   * Submit dealer completion documents (Step 5)
   */
  async submitCompletionDocuments(
    requestId: string,
    completionData: {
      activityCompletionDate: Date;
      numberOfParticipants?: number;
      closedExpenses: any[];
      totalClosedExpenses: number;
      invoicesReceipts?: any[];
      attendanceSheet?: any;
      completionDescription?: string;
    },
    dealerUserId?: string // Optional dealer user ID for history tracking
  ): Promise<void> {
    try {
      const request = await WorkflowRequest.findByPk(requestId);
      // Handle backward compatibility: workflowType may be undefined in old environments
      const workflowType = request?.workflowType || 'NON_TEMPLATIZED';
      if (!request || workflowType !== 'CLAIM_MANAGEMENT') {
        throw new Error('Invalid claim request');
      }

      // Find the "Dealer Completion Documents" step by levelName (handles step shifts due to additional approvers)
      const approvalLevels = await ApprovalLevel.findAll({
        where: { requestId },
        order: [['levelNumber', 'ASC']]
      });

      const dealerCompletionStep = approvalLevels.find((level: any) => {
        const levelName = (level.levelName || '').toLowerCase();
        return levelName.includes('dealer completion') || levelName.includes('completion documents');
      });

      if (!dealerCompletionStep) {
        throw new Error('Dealer Completion Documents step not found');
      }

      // Check if current level matches the Dealer Completion Documents step (handles step shifts)
      if (request.currentLevel !== dealerCompletionStep.levelNumber) {
        throw new Error(`Completion documents can only be submitted at the Dealer Completion Documents step (currently at step ${request.currentLevel})`);
      }

      // Save completion details
      const [completionDetails] = await DealerCompletionDetails.upsert({
        requestId,
        activityCompletionDate: completionData.activityCompletionDate,
        numberOfParticipants: completionData.numberOfParticipants,
        totalClosedExpenses: completionData.totalClosedExpenses,
        submittedAt: new Date(),
      });

      // Persist individual closed expenses to dealer_completion_expenses
      const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
      const dealer = await findDealerLocally(claimDetails?.dealerCode, claimDetails?.dealerEmail);
      const buyerStateCode = "33";
      let dealerStateCode = "33";
      if (dealer?.gstin && dealer.gstin.length >= 2 && !isNaN(Number(dealer.gstin.substring(0, 2)))) {
        dealerStateCode = dealer.gstin.substring(0, 2);
      } else if (dealer?.state) {
        if (dealer.state.toLowerCase().includes('tamil nadu')) dealerStateCode = "33";
        else dealerStateCode = "00";
      }
      const isIGST = dealerStateCode !== buyerStateCode;

      const completionId = (completionDetails as any)?.completionId;
      const expenseRows: any[] = [];
      if (completionData.closedExpenses && completionData.closedExpenses.length > 0) {
        // Determine taxation type for fallback logic
        const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
        let isNonGst = false;
        if (claimDetails?.activityType) {
          const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
          const taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST');
          isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
        }

        // Clear existing expenses for this request to avoid duplicates
        await DealerCompletionExpense.destroy({ where: { requestId } });
        completionData.closedExpenses.forEach((item: any) => {
          const amount = Number(item.amount) || 0;
          const quantity = Number(item.quantity) || 1;
          const baseTotal = amount * quantity;

          // Tax calculations (simplified for brevity, matching previous logic)
          const gstRate = isNonGst ? 0 : (Number(item.gstRate) || 18);
          const totalTaxAmt = baseTotal * (gstRate / 100);

          let finalCgstRate = 0, finalCgstAmt = 0, finalSgstRate = 0, finalSgstAmt = 0, finalIgstRate = 0, finalIgstAmt = 0, finalUtgstRate = 0, finalUtgstAmt = 0;

          if (!isNonGst) {
            if (isIGST) {
              finalIgstRate = gstRate;
              finalIgstAmt = totalTaxAmt;
            } else {
              finalCgstRate = gstRate / 2;
              finalCgstAmt = totalTaxAmt / 2;
              finalSgstRate = gstRate / 2;
              finalSgstAmt = totalTaxAmt / 2;
            }
          }

          expenseRows.push({
            requestId,
            completionId,
            description: item.description,
            amount,
            quantity,
            taxableAmt: baseTotal, // Added for Scenario 1 comparison
            hsnCode: item.hsnCode || '',
            gstRate,
            gstAmt: totalTaxAmt,
            cgstRate: finalCgstRate,
            cgstAmt: finalCgstAmt,
            sgstRate: finalSgstRate,
            sgstAmt: finalSgstAmt,
            igstRate: finalIgstRate,
            igstAmt: finalIgstAmt,
            utgstRate: finalUtgstRate,
            utgstAmt: finalUtgstAmt,
            cessRate: Number(item.cessRate) || 0,
            cessAmt: Number(item.cessAmt) || 0,
            totalAmt: Number(item.totalAmt) || (baseTotal + totalTaxAmt),
            isService: !!item.isService,
            expenseDate: item.date instanceof Date ? item.date : (item.date ? new Date(item.date) : (completionData.activityCompletionDate || new Date())),
          });
        });
        await DealerCompletionExpense.bulkCreate(expenseRows);
      }

      // Update budget tracking with closed expenses (Gross and Taxable)
      const totalTaxableClosedExpenses = expenseRows.reduce((sum, item) => sum + Number(item.taxableAmt || 0), 0);

      await ClaimBudgetTracking.upsert({
        requestId,
        closedExpenses: completionData.totalClosedExpenses, // Gross amount
        taxableClosedExpenses: totalTaxableClosedExpenses,  // Taxable amount (for Scenario 1 comparison)
        closedExpensesSubmittedAt: new Date(),
        budgetStatus: BudgetStatus.CLOSED,
        currency: 'INR',
      });

      // Scenario 1: Unblocking excess budget if actual taxable expenses < total blocked amount
      const allInternalOrders = await InternalOrder.findAll({ where: { requestId, status: IOStatus.BLOCKED } });
      const totalBlockedAmount = allInternalOrders.reduce((sum, io) => sum + Number(io.ioBlockedAmount || 0), 0);

      if (totalTaxableClosedExpenses < totalBlockedAmount) {
        const amountToRelease = parseFloat((totalBlockedAmount - totalTaxableClosedExpenses).toFixed(2));
        logger.info(`[DealerClaimService] Scenario 1: Actual taxable expenses (₹${totalTaxableClosedExpenses}) < Total blocked (₹${totalBlockedAmount}). Releasing ₹${amountToRelease}.`);

        // Release budget from the most recent IO record (or first available)
        // In a more complex setup, we might release proportionally, but here we pick the one with enough balance
        const ioToRelease = allInternalOrders.sort((a, b) => (b.createdAt as any) - (a.createdAt as any))[0];

        if (ioToRelease && amountToRelease > 0) {
          try {
            const releaseResult = await sapIntegrationService.releaseBudget(
              ioToRelease.ioNumber,
              amountToRelease,
              String((request as any).requestNumber || (request as any).request_number || requestId),
              ioToRelease.sapDocumentNumber || undefined
            );

            if (releaseResult.success) {
              logger.info(`[DealerClaimService] Successfully released ₹${amountToRelease} from IO ${ioToRelease.ioNumber}`);
            } else {
              logger.warn(`[DealerClaimService] SAP release failed: ${releaseResult.error}`);
            }
          } catch (releaseError) {
            logger.error(`[DealerClaimService] Error during budget release:`, releaseError);
          }
        }
      } else if (totalTaxableClosedExpenses > totalBlockedAmount) {
        // Scenario 2: Actual taxable expenses > Total blocked amount
        const additionalAmountNeeded = parseFloat((totalTaxableClosedExpenses - totalBlockedAmount).toFixed(2));
        logger.info(`[DealerClaimService] Scenario 2: Actual taxable expenses (₹${totalTaxableClosedExpenses}) > Total blocked (₹${totalBlockedAmount}). Additional ₹${additionalAmountNeeded} needs to be blocked.`);

        // Update DealerClaimDetails with the new total required amount for IO blocking reference
        await DealerClaimDetails.update(
          { totalProposedTaxableAmount: totalTaxableClosedExpenses },
          { where: { requestId } }
        );

        // Signal that re-blocking is needed by updating status back to PROPOSED
        await ClaimBudgetTracking.update(
          { budgetStatus: BudgetStatus.PROPOSED },
          { where: { requestId } }
        );
      }

      // Approve Dealer Completion Documents step dynamically (by levelName, not hardcoded step number)
      let dealerCompletionLevel = await ApprovalLevel.findOne({
        where: {
          requestId,
          levelName: 'Dealer Completion Documents'
        }
      });

      // Fallback: try to find by levelNumber 4 (new position after removing system steps)
      if (!dealerCompletionLevel) {
        dealerCompletionLevel = await ApprovalLevel.findOne({
          where: { requestId, levelNumber: 4 }
        });
      }

      if (dealerCompletionLevel) {
        // Use dealer's completion description if provided, otherwise use default message
        const approvalComment = completionData.completionDescription?.trim()
          ? completionData.completionDescription.trim()
          : 'Completion documents submitted';

        // Get dealer user ID if not provided - try to find by dealer email from claim details
        let actualDealerUserId: string | null = dealerUserId || null;
        if (!actualDealerUserId) {
          const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
          if (claimDetails?.dealerEmail) {
            const dealerUser = await User.findOne({
              where: { email: claimDetails.dealerEmail }
            });
            actualDealerUserId = dealerUser?.userId || null;
          }
        }

        // Perform the approval action FIRST - only save snapshot if action succeeds
        const approvalService = this.getApprovalService();
        await approvalService.approveLevel(
          dealerCompletionLevel.levelId,
          { action: 'APPROVE', comments: approvalComment },
          actualDealerUserId || (request as any).initiatorId || 'system',
          { ipAddress: null, userAgent: null }
        );

        // Save completion history AFTER approval succeeds (this is the only snapshot needed for dealer completion)
        // Use dealer user ID if available, otherwise use initiator ID as fallback
        const historyUserId = actualDealerUserId || (request as any).initiatorId || null;
        if (!historyUserId) {
          logger.warn(`[DealerClaimService] No user ID available for completion history, skipping history save`);
        } else {
          try {
            await this.saveCompletionHistory(
              requestId,
              dealerCompletionLevel.levelId,
              dealerCompletionLevel.levelNumber,
              `Completion Submitted: ${approvalComment}`,
              historyUserId
            );
            // Note: We don't save workflow history here - completion history is sufficient
            // Workflow history will be saved when the level is approved and moves to next level
          } catch (snapshotError) {
            // Log error but don't fail the submission - snapshot is for audit, not critical
            logger.error(`[DealerClaimService] Failed to save completion history snapshot (non-critical):`, snapshotError);
          }
        }
      }

      logger.info(`[DealerClaimService] Completion documents submitted for request: ${requestId}`);
    } catch (error) {
      logger.error('[DealerClaimService] Error submitting completion documents:', error);
      throw error;
    }
  }

  /**
   * Update IO details (Step 3 - Department Lead)
   * Validates IO number with SAP and blocks budget
   */
  /**
   * Update IO details and block amount in SAP
   * Only stores data when blocking amount > 0
   * This method is called when user actually blocks the amount
   */
  async updateIODetails(
    requestId: string,
    ioData: {
      ioNumber: string;
      ioRemark?: string;
      availableBalance?: number;
      blockedAmount?: number;
      remainingBalance?: number;
    },
    organizedByUserId?: string
  ): Promise<void> {
    try {
      // Ensure blockedAmount is rounded to exactly 2 decimal places from the start
      const blockedAmount = ioData.blockedAmount ? parseFloat(ioData.blockedAmount.toFixed(2)) : 0;

      // If blocking amount > 0, proceed with SAP integration and blocking
      // If blocking amount is 0 but ioNumber is provided, just save the IO details without blocking
      if (blockedAmount <= 0) {
        // Allow saving IO details (ioNumber only) even without blocking amount
        // This is useful when Step 3/Requestor Evaluation is in progress but amount hasn't been blocked yet or for linking IO
        if (ioData.ioNumber) {
          const organizedBy = organizedByUserId || null;

          // Check if an IO record already exists for this request and IO number
          // This prevents duplicate 0-amount "provisioned" records when re-saving IO details
          const existingIO = await InternalOrder.findOne({
            where: {
              requestId,
              ioNumber: ioData.ioNumber
            }
          });

          if (existingIO) {
            // Update existing record with latest remark and organizer info if provided
            await existingIO.update({
              ioRemark: ioData.ioRemark || existingIO.ioRemark || '',
              organizedBy: organizedBy || existingIO.organizedBy || undefined,
              organizedAt: new Date(),
            });

            logger.info(`[DealerClaimService] Existing IO record updated for request: ${requestId}`, {
              ioNumber: ioData.ioNumber,
              status: existingIO.status
            });
            return;
          }

          // Create a new Internal Order record if none exists for this IO and request
          await InternalOrder.create({
            requestId,
            ioNumber: ioData.ioNumber,
            ioRemark: ioData.ioRemark || '',
            ioAvailableBalance: ioData.availableBalance || 0,
            ioBlockedAmount: 0,
            ioRemainingBalance: ioData.remainingBalance || 0,
            organizedBy: organizedBy || undefined,
            organizedAt: new Date(),
            status: IOStatus.PENDING,
          });

          logger.info(`[DealerClaimService] IO provision record created for request: ${requestId}`, {
            ioNumber: ioData.ioNumber
          });

          return; // Exit early - no SAP blocking needed
        } else {
          throw new Error('Blocked amount must be greater than 0, or ioNumber must be provided');
        }
      }

      // Validate IO number with SAP
      const ioValidation = await sapIntegrationService.validateIONumber(ioData.ioNumber);

      if (!ioValidation.isValid) {
        throw new Error(`Invalid IO number: ${ioValidation.error || 'IO number not found in SAP'}`);
      }

      // Block budget in SAP
      const request = await WorkflowRequest.findByPk(requestId);
      const requestNumber = request ? ((request as any).requestNumber || (request as any).request_number) : 'UNKNOWN';

      logger.info(`[DealerClaimService] Blocking budget in SAP:`, {
        requestId,
        requestNumber,
        ioNumber: ioData.ioNumber,
        amountToBlock: blockedAmount,
        availableBalance: ioData.availableBalance || ioValidation.availableBalance,
      });

      const blockResult = await sapIntegrationService.blockBudget(
        ioData.ioNumber,
        blockedAmount,
        requestNumber,
        `Budget block for claim request ${requestNumber}`
      );

      if (!blockResult.success) {
        throw new Error(`Failed to block budget in SAP: ${blockResult.error}`);
      }

      const sapReturnedBlockedAmount = blockResult.blockedAmount;
      const sapDocumentNumber = blockResult.blockId || undefined;
      const availableBalance = parseFloat((ioData.availableBalance || ioValidation.availableBalance).toFixed(2));

      // Use the amount we REQUESTED for calculation, unless SAP blocked significantly different amount
      const amountDifference = Math.abs(sapReturnedBlockedAmount - blockedAmount);
      const useSapAmount = amountDifference > 1.0;
      const finalBlockedAmount = useSapAmount ? sapReturnedBlockedAmount : blockedAmount;

      // Calculate remaining balance
      const calculatedRemainingBalance = parseFloat((availableBalance - finalBlockedAmount).toFixed(2));
      const sapRemainingBalance = blockResult.remainingBalance ? parseFloat(blockResult.remainingBalance.toFixed(2)) : 0;
      const sapValueIsValid = sapRemainingBalance > 0 &&
        sapRemainingBalance <= availableBalance &&
        Math.abs(sapRemainingBalance - calculatedRemainingBalance) < 1;

      const remainingBalance = sapValueIsValid ? sapRemainingBalance : calculatedRemainingBalance;
      const finalRemainingBalance = parseFloat(Math.max(0, remainingBalance).toFixed(2));

      // Create new Internal Order record for this block operation (supporting multiple blocks)
      const organizedBy = organizedByUserId || null;
      await InternalOrder.create({
        requestId,
        ioNumber: ioData.ioNumber,
        ioRemark: ioData.ioRemark || '',
        ioAvailableBalance: availableBalance,
        ioBlockedAmount: finalBlockedAmount,
        ioRemainingBalance: finalRemainingBalance,
        organizedBy: organizedBy || undefined,
        organizedAt: new Date(),
        sapDocumentNumber: sapDocumentNumber || undefined,
        status: IOStatus.BLOCKED,
      });

      // Update budget tracking with TOTAL blocked amount from all records
      const allInternalOrders = await InternalOrder.findAll({ where: { requestId, status: IOStatus.BLOCKED } });
      const totalBlockedAmount = allInternalOrders.reduce((sum, io) => sum + Number(io.ioBlockedAmount || 0), 0);

      await ClaimBudgetTracking.upsert({
        requestId,
        ioBlockedAmount: totalBlockedAmount,
        ioBlockedAt: new Date(),
        budgetStatus: BudgetStatus.BLOCKED,
        currency: 'INR',
      });

      logger.info(`[DealerClaimService] Budget block recorded for request: ${requestId}`, {
        ioNumber: ioData.ioNumber,
        blockedAmount: finalBlockedAmount,
        totalBlockedAmount,
        sapDocumentNumber,
        finalRemainingBalance
      });

      // Save IO history after successful blocking
      // Find the Department Lead IO Approval level (Step 3)
      const ioApprovalLevel = await ApprovalLevel.findOne({
        where: {
          requestId,
          levelName: 'Department Lead IO Approval'
        }
      });

      // Fallback: try to find by levelNumber 3
      const ioLevel = ioApprovalLevel || await ApprovalLevel.findOne({
        where: { requestId, levelNumber: 3 }
      });

      // Get user ID for history - use organizedBy if it's a UUID, otherwise try to find user
      let ioHistoryUserId: string | null = null;
      if (ioLevel) {
        if (organizedBy) {
          // Check if organizedBy is a valid UUID
          const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
          if (uuidRegex.test(organizedBy)) {
            ioHistoryUserId = organizedBy;
          } else {
            // Try to find user by email or name
            const user = await User.findOne({
              where: { email: organizedBy }
            });
            ioHistoryUserId = user?.userId || null;
          }
        }

        // Fallback to initiator if no user found
        if (!ioHistoryUserId) {
          const request = await WorkflowRequest.findByPk(requestId);
          ioHistoryUserId = (request as any)?.initiatorId || null;
        }
      }

      // Save IO history AFTER budget tracking update succeeds (only if ioLevel exists)
      if (ioLevel && ioHistoryUserId) {
        try {
          await this.saveIOHistory(
            requestId,
            ioLevel.levelId,
            ioLevel.levelNumber,
            `IO Blocked: ₹${finalBlockedAmount.toFixed(2)} blocked in SAP`,
            ioHistoryUserId
          );
        } catch (snapshotError) {
          // Log error but don't fail the IO blocking - snapshot is for audit, not critical
          logger.error(`[DealerClaimService] Failed to save IO history snapshot (non-critical):`, snapshotError);
        }
      } else if (ioLevel && !ioHistoryUserId) {
        logger.warn(`[DealerClaimService] No user ID available for IO history, skipping history save`);
      }

      logger.info(`[DealerClaimService] IO blocked for request: ${requestId}`, {
        ioNumber: ioData.ioNumber,
        blockedAmount: finalBlockedAmount,
        availableBalance,
        remainingBalance: finalRemainingBalance
      });
    } catch (error) {
      logger.error('[DealerClaimService] Error blocking IO:', error);
      throw error;
    }
  }

  /**
   * Update e-invoice details (Step 7)
   * Generates e-invoice via DMS integration
   */
  async updateEInvoiceDetails(
    requestId: string,
    invoiceData?: {
      eInvoiceNumber?: string;
      eInvoiceDate?: Date;
      dmsNumber?: string;
      amount?: number;
      description?: string;
    }
  ): Promise<void> {
    try {
      const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
      if (!claimDetails) {
        throw new Error('Claim details not found');
      }

      const budgetTracking = await ClaimBudgetTracking.findOne({ where: { requestId } });
      const completionDetails = await DealerCompletionDetails.findOne({ where: { requestId } });
      const internalOrder = await InternalOrder.findOne({ where: { requestId } });
      const claimInvoice = await ClaimInvoice.findOne({ where: { requestId } });

      const request = await WorkflowRequest.findByPk(requestId);
      if (!request) {
        throw new Error('Workflow request not found');
      }

      const workflowType = (request as any).workflowType;
      if (workflowType !== 'CLAIM_MANAGEMENT') {
        throw new Error('This endpoint is only for claim management workflows');
      }

      const requestNumber = request ? ((request as any).requestNumber || (request as any).request_number) : 'UNKNOWN';

      // If invoice data not provided, generate via PWC E-Invoice service
      if (!invoiceData?.eInvoiceNumber) {
        const proposalDetails = await DealerProposalDetails.findOne({ where: { requestId } });
        const invoiceAmount = invoiceData?.amount
          || proposalDetails?.totalEstimatedBudget
          || budgetTracking?.proposalEstimatedBudget
          || budgetTracking?.initialEstimatedBudget
          || 0;

        // Generate custom invoice number based on specific format: INDC + DealerCode + AB + Sequence
        // Format: INDC[DealerCode]AB[Sequence] (e.g., INDC004597AB0001)
        logger.info(`[DealerClaimService] Generating custom invoice number for dealer: ${claimDetails.dealerCode}`);
        const customInvoiceNumber = await this.generateCustomInvoiceNumber(claimDetails.dealerCode);
        logger.info(`[DealerClaimService] Generated custom invoice number: ${customInvoiceNumber} for request: ${requestId}`);

        const invoiceResult = await pwcIntegrationService.generateSignedInvoice(requestId, invoiceAmount, customInvoiceNumber);

        logger.info(`[DealerClaimService] PWC Generation Result: Success=${invoiceResult.success}, AckNo=${invoiceResult.ackNo}, SignedInv present=${!!invoiceResult.signedInvoice}`);

        if (!invoiceResult.success) {
          throw new Error(`Failed to generate signed e-invoice via PWC: ${invoiceResult.error}`);
        }

        await ClaimInvoice.upsert({
          requestId,
          invoiceNumber: customInvoiceNumber, // Use custom invoice number as primary identifier
          invoiceDate: invoiceResult.ackDate instanceof Date ? invoiceResult.ackDate : (invoiceResult.ackDate ? new Date(invoiceResult.ackDate) : new Date()),
          irn: invoiceResult.irn,
          ackNo: invoiceResult.ackNo,
          ackDate: invoiceResult.ackDate instanceof Date ? invoiceResult.ackDate : (invoiceResult.ackDate ? new Date(invoiceResult.ackDate) : null),
          signedInvoice: invoiceResult.signedInvoice,
          qrCode: invoiceResult.qrCode,
          qrImage: invoiceResult.qrImage,
          pwcResponse: invoiceResult.rawResponse,
          irpResponse: invoiceResult.irpResponse,
          amount: invoiceAmount,
          taxableValue: invoiceResult.totalAssAmt,
          igstTotal: invoiceResult.totalIgstAmt,
          cgstTotal: invoiceResult.totalCgstAmt,
          sgstTotal: invoiceResult.totalSgstAmt,
          status: 'GENERATED',
          generatedAt: new Date(),
          description: invoiceData?.description || `PWC Signed Invoice for claim request ${requestNumber}`,
        });

        logger.info(`[DealerClaimService] Signed Invoice generated via PWC for request: ${requestId}`, {
          ackNo: invoiceResult.ackNo,
          irn: invoiceResult.irn
        });
      } else {
        // Manual entry - just update the fields
        await ClaimInvoice.upsert({
          requestId,
          invoiceNumber: invoiceData.eInvoiceNumber,
          invoiceDate: invoiceData.eInvoiceDate || new Date(),
          dmsNumber: invoiceData.dmsNumber,
          amount: Number(invoiceData.amount) || 0,
          status: 'UPDATED',
          generatedAt: new Date(),
          description: invoiceData.description,
        });

        logger.info(`[DealerClaimService] E-Invoice details manually updated for request: ${requestId}`);
      }

      // Generate CSV for WFM system (INCOMING\WFM_MAIN\DLR_INC_CLAIMS)
      await this.pushWFMCSV(requestId).catch((err: Error) => {
        logger.error('[DealerClaimService] Initial WFM push failed:', err);
      });

      // Generate PDF Invoice
      try {
        const { pdfService } = require('./pdf.service');
        await pdfService.generateInvoicePdf(requestId);
      } catch (error) {
        logger.error(`[DealerClaimService] Failed to generate PDF for request ${requestId}:`, error);
        // Don't throw, we still want to proceed with auto-approval
      }

      // Check if Requestor Claim Approval is approved - if not, approve it first
      // Find dynamically by levelName (handles step shifts due to additional approvers)
      const approvalLevels = await ApprovalLevel.findAll({
        where: { requestId },
        order: [['levelNumber', 'ASC']]
      });

      let requestorClaimLevel = approvalLevels.find((level: any) => {
        const levelName = (level.levelName || '').toLowerCase();
        return levelName.includes('requestor') &&
          (levelName.includes('claim') || levelName.includes('approval'));
      });

      // Fallback: try to find by levelNumber 5 (new position after removing system steps)
      // But only if no match found by name (handles edge cases)
      if (!requestorClaimLevel) {
        requestorClaimLevel = approvalLevels.find((level: any) => level.levelNumber === 5);
      }

      // Validate that we're at the Requestor Claim Approval step before allowing E-Invoice generation
      if (requestorClaimLevel && request.currentLevel !== requestorClaimLevel.levelNumber) {
        throw new Error(`Cannot generate E-Invoice. Request is currently at step ${request.currentLevel}, but Requestor Claim Approval is at step ${requestorClaimLevel.levelNumber}. Please complete all previous steps first.`);
      }

      // E-Invoice Generation is successful - auto-approve the Requestor Claim Approval step
      if (requestorClaimLevel && requestorClaimLevel.status !== 'APPROVED') {
        const approvalService = this.getApprovalService();
        await approvalService.approveLevel(
          requestorClaimLevel.levelId,
          { action: 'APPROVE', comments: 'Auto-approved after successful E-Invoice generation' },
          'system'
        );
        logger.info(`[DealerClaimService] Step "${requestorClaimLevel.levelName}" auto-approved after E-Invoice generation for request ${requestId}`);
      }

      // Log E-Invoice generation as activity
      await activityService.log({
        requestId,
        type: 'status_change',
        user: { userId: 'system', name: 'System Auto-Process' },
        timestamp: new Date().toISOString(),
        action: 'E-Invoice Generated',
        details: `E-Invoice generated via PWC integration for request ${requestNumber}. Step "${requestorClaimLevel?.levelName || 'Requestor Claim Approval'}" auto-approved.`,
      });
    } catch (error) {
      logger.error('[DealerClaimService] Error updating e-invoice details:', error);
      throw error;
    }
  }

  /**
   * Log E-Invoice Generation as activity (no longer an approval step)
   * This method logs the e-invoice generation activity when invoice is generated via PWC service
   */
  async logEInvoiceGenerationActivity(requestId: string, invoiceNumber?: string): Promise<void> {
    try {
      logger.info(`[DealerClaimService] Logging E-Invoice Generation activity for request ${requestId}`);

      const request = await WorkflowRequest.findByPk(requestId);
      if (!request) {
        throw new Error(`Workflow request ${requestId} not found`);
      }

      const workflowType = (request as any).workflowType;
      if (workflowType !== 'CLAIM_MANAGEMENT') {
        logger.warn(`[DealerClaimService] Skipping E-Invoice activity logging - not a claim management workflow (type: ${workflowType})`);
        return;
      }

      const requestNumber = (request as any).requestNumber || (request as any).request_number || 'UNKNOWN';
      const claimInvoice = await ClaimInvoice.findOne({ where: { requestId } });
      const finalInvoiceNumber = invoiceNumber || claimInvoice?.invoiceNumber || 'N/A';

      // Log E-Invoice Generation as activity
      await activityService.log({
        requestId,
        type: 'status_change',
        user: { userId: 'system', name: 'System Auto-Process' },
        timestamp: new Date().toISOString(),
        action: 'E-Invoice Generated',
        details: `E-Invoice generated via PWC. Invoice Number: ${finalInvoiceNumber}. Request: ${requestNumber}`,
      });

      logger.info(`[DealerClaimService] E-Invoice Generation activity logged for request ${requestId} (Invoice: ${finalInvoiceNumber})`);
    } catch (error) {
      logger.error(`[DealerClaimService] Error logging E-Invoice Generation activity for request ${requestId}:`, error);
      // Don't throw - activity logging is not critical
    }
  }

  /**
   * Update credit note details (Step 8)
   * Generates credit note via DMS integration
   */
  async updateCreditNoteDetails(
    requestId: string,
    creditNoteData?: {
      creditNoteNumber?: string;
      creditNoteDate?: Date;
      creditNoteAmount?: number;
      reason?: string;
      description?: string;
    }
  ): Promise<void> {
    try {
      const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
      if (!claimDetails) {
        throw new Error('Claim details not found');
      }

      const budgetTracking = await ClaimBudgetTracking.findOne({ where: { requestId } });
      const completionDetails = await DealerCompletionDetails.findOne({ where: { requestId } });
      const claimInvoice = await ClaimInvoice.findOne({ where: { requestId } });

      const request = await WorkflowRequest.findByPk(requestId);
      const requestNumber = request ? ((request as any).requestNumber || (request as any).request_number) : 'UNKNOWN';

      // If credit note data not provided, generate via DMS
      if (!creditNoteData?.creditNoteNumber) {
        const creditNoteAmount = creditNoteData?.creditNoteAmount
          || budgetTracking?.closedExpenses
          || completionDetails?.totalClosedExpenses
          || 0;

        // Only generate via DMS if invoice exists, otherwise allow manual entry
        if (claimInvoice?.invoiceNumber) {
          const creditNoteResult = await dmsIntegrationService.generateCreditNote({
            requestNumber,
            eInvoiceNumber: claimInvoice.invoiceNumber,
            dealerCode: claimDetails.dealerCode,
            dealerName: claimDetails.dealerName,
            amount: creditNoteAmount,
            reason: creditNoteData?.reason || 'Claim settlement',
            description: creditNoteData?.description || `Credit note for claim request ${requestNumber}`,
          });

          if (!creditNoteResult.success) {
            throw new Error(`Failed to generate credit note: ${creditNoteResult.error}`);
          }

          await ClaimCreditNote.upsert({
            requestId,
            invoiceId: claimInvoice.invoiceId,
            creditNoteNumber: creditNoteResult.creditNoteNumber,
            creditNoteDate: creditNoteResult.creditNoteDate || new Date(),
            creditNoteAmount: Number(creditNoteResult.creditNoteAmount) || 0,
            status: 'GENERATED',
            confirmedAt: new Date(),
            reason: creditNoteData?.reason || 'Claim settlement',
            description: creditNoteData?.description || `Credit note for claim request ${requestNumber}`,
          });

          logger.info(`[DealerClaimService] Credit note generated via DMS for request: ${requestId}`, {
            creditNoteNumber: creditNoteResult.creditNoteNumber,
            creditNoteAmount: creditNoteResult.creditNoteAmount
          });
        } else {
          // No invoice exists - create credit note manually without invoice link
          await ClaimCreditNote.upsert({
            requestId,
            invoiceId: undefined, // No invoice linked
            creditNoteNumber: undefined, // Will be set manually later
            creditNoteDate: creditNoteData?.creditNoteDate || new Date(),
            creditNoteAmount: creditNoteAmount,
            status: 'PENDING',
            reason: creditNoteData?.reason || 'Claim settlement',
            description: creditNoteData?.description || `Credit note for claim request ${requestNumber} (no invoice)`,
          });

          logger.info(`[DealerClaimService] Credit note created without invoice for request: ${requestId}`);
        }
      } else {
        // Manual entry - just update the fields
        await ClaimCreditNote.upsert({
          requestId,
          invoiceId: claimInvoice?.invoiceId || undefined, // Allow undefined if no invoice
          creditNoteNumber: creditNoteData.creditNoteNumber,
          creditNoteDate: creditNoteData.creditNoteDate || new Date(),
          creditNoteAmount: Number(creditNoteData.creditNoteAmount) || 0,
          status: 'UPDATED',
          confirmedAt: new Date(),
          reason: creditNoteData?.reason,
          description: creditNoteData?.description,
        });

        logger.info(`[DealerClaimService] Credit note details manually updated for request: ${requestId}`);
      }
    } catch (error) {
      logger.error('[DealerClaimService] Error updating credit note details:', error);
      throw error;
    }
  }

  /**
   * Genetate Custom Invoice Number
   * Format: INDC - DEALER CODE - AB (For FY) - sequence nos.
   * Sample: INDC004597AB0001
   */
  private async generateCustomInvoiceNumber(dealerCode: string): Promise<string> {
    const fyCode = 'AB'; // Hardcoded FY code as per requirement
    // Ensure dealer code is padded/truncated if needed to fit length constraints, but requirement says "004597" which is 6 digits.
    // Assuming dealerCode is already correct length or we use it as is.
    const cleanDealerCode = (dealerCode || '000000').trim();

    const prefix = `INDC${cleanDealerCode}${fyCode}`;

    // Find last invoice with this prefix
    const lastInvoice = await ClaimInvoice.findOne({
      where: {
        invoiceNumber: {
          [Op.like]: `${prefix}%`
        }
      },
      order: [
        [sequelize.fn('LENGTH', sequelize.col('invoice_number')), 'DESC'],
        ['invoice_number', 'DESC']
      ]
    });

    let sequence = 1;
    if (lastInvoice && lastInvoice.invoiceNumber) {
      // Extract the sequence part (last 4 digits)
      const lastSeqStr = lastInvoice.invoiceNumber.replace(prefix, '');
      const lastSeq = parseInt(lastSeqStr, 10);
      if (!isNaN(lastSeq)) {
        sequence = lastSeq + 1;
      }
    }

    // Pad sequence to 4 digits
    const sequenceStr = sequence.toString().padStart(4, '0');

    return `${prefix}${sequenceStr}`;
  }

  /**
   * Send credit note to dealer and auto-approve Step 8
   * This method sends the credit note to the dealer via email/notification and auto-approves Step 8
   */
  async sendCreditNoteToDealer(requestId: string, userId: string): Promise<void> {
    try {
      logger.info(`[DealerClaimService] Sending credit note to dealer for request ${requestId}`);

      // Get credit note details
      const creditNote = await ClaimCreditNote.findOne({
        where: { requestId }
      });

      if (!creditNote) {
        throw new Error('Credit note not found. Please ensure credit note is generated before sending to dealer.');
      }

      // Get claim details for dealer information
      const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
      if (!claimDetails) {
        throw new Error('Claim details not found');
      }

      // Get workflow request
      const request = await WorkflowRequest.findByPk(requestId);
      if (!request) {
        throw new Error('Workflow request not found');
      }

      const workflowType = (request as any).workflowType;
      if (workflowType !== 'CLAIM_MANAGEMENT') {
        throw new Error('This operation is only available for claim management workflows');
      }

      // Credit Note Confirmation is now an activity log only, not an approval step
      const requestNumber = (request as any).requestNumber || (request as any).request_number || 'UNKNOWN';

      // Update credit note status to CONFIRMED
      await creditNote.update({
        status: 'CONFIRMED',
        confirmedAt: new Date(),
        confirmedBy: userId,
      });

      // Log Credit Note Confirmation as activity (no approval step needed)
      await activityService.log({
        requestId,
        type: 'status_change',
        user: { userId: userId, name: 'Finance Team' },
        timestamp: new Date().toISOString(),
        action: 'Credit Note Confirmed and Sent',
        details: `Credit note sent to dealer. Credit Note Number: ${creditNote.creditNoteNumber || 'N/A'}. Credit Note Amount: ₹${creditNote.creditNoteAmount || 0}. Request: ${requestNumber}`,
      });

      // Send notification to dealer (you can implement email service here)
      logger.info(`[DealerClaimService] Credit note sent to dealer`, {
        requestId,
        creditNoteNumber: creditNote.creditNoteNumber,
        dealerEmail: claimDetails.dealerEmail,
        dealerName: claimDetails.dealerName,
      });


    } catch (error) {
      logger.error('[DealerClaimService] Error sending credit note to dealer:', error);
      throw error;
    }
  }

  /**
   * Process Activity Creation (now activity log only, not an approval step)
   * Creates activity confirmation and sends emails to dealer, requestor, and department lead
   * Logs activity instead of creating/approving approval level
   */
  async processActivityCreation(requestId: string): Promise<void> {
    try {
      logger.info(`[DealerClaimService] Processing Activity Creation for request ${requestId}`);

      // Get workflow request
      const request = await WorkflowRequest.findByPk(requestId);
      if (!request) {
        throw new Error(`Workflow request ${requestId} not found`);
      }

      // Verify this is a claim management workflow
      const workflowType = (request as any).workflowType;
      if (workflowType !== 'CLAIM_MANAGEMENT') {
        logger.warn(`[DealerClaimService] Skipping Activity Creation - not a claim management workflow (type: ${workflowType})`);
        return;
      }

      // Get claim details
      const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
      if (!claimDetails) {
        throw new Error(`Claim details not found for request ${requestId}`);
      }

      // Get participants for email notifications
      const initiator = await User.findByPk((request as any).initiatorId);
      const dealerUser = claimDetails.dealerEmail
        ? await User.findOne({ where: { email: claimDetails.dealerEmail } })
        : null;

      // Get department lead dynamically (by levelName, not hardcoded step number)
      let deptLeadLevel = await ApprovalLevel.findOne({
        where: {
          requestId,
          levelName: 'Department Lead Approval'
        }
      });

      // Fallback: try to find by levelNumber 3 (for backwards compatibility)
      if (!deptLeadLevel) {
        deptLeadLevel = await ApprovalLevel.findOne({
          where: {
            requestId,
            levelNumber: 3
          }
        });
      }
      const departmentLead = deptLeadLevel?.approverId
        ? await User.findByPk(deptLeadLevel.approverId)
        : null;

      const requestNumber = (request as any).requestNumber || (request as any).request_number || 'UNKNOWN';
      const activityName = claimDetails.activityName || 'Activity';
      const activityType = claimDetails.activityType || 'N/A';

      // Prepare email recipients
      const emailRecipients: string[] = [];
      const userIdsForNotification: string[] = [];

      // Add initiator
      if (initiator) {
        emailRecipients.push(initiator.email);
        userIdsForNotification.push(initiator.userId);
      }

      // Add dealer
      if (dealerUser) {
        emailRecipients.push(dealerUser.email);
        userIdsForNotification.push(dealerUser.userId);
      } else if (claimDetails.dealerEmail) {
        emailRecipients.push(claimDetails.dealerEmail);
      }

      // Add department lead
      if (departmentLead) {
        emailRecipients.push(departmentLead.email);
        userIdsForNotification.push(departmentLead.userId);
      }

      // Send activity confirmation emails
      const emailSubject = `Activity Created: ${activityName} - ${requestNumber}`;
      const emailBody = `Activity "${activityName}" (${activityType}) has been created successfully for request ${requestNumber}. IO confirmation to be made.`;

      // Send notifications to users in the system with proper metadata
      if (userIdsForNotification.length > 0) {
        // Prepare metadata for activity created email template
        const activityData = {
          activityName: activityName,
          activityType: activityType,
          activityDate: claimDetails.activityDate,
          location: claimDetails.location || 'Not specified',
          dealerName: claimDetails.dealerName || 'Dealer',
          dealerCode: claimDetails.dealerCode,
          initiatorName: initiator ? (initiator.displayName || initiator.email) : 'Initiator',
          departmentLeadName: departmentLead ? (departmentLead.displayName || departmentLead.email) : undefined,
          ioNumber: undefined, // IO number will be added later when IO is created
          nextSteps: 'IO confirmation to be made. Dealer will proceed with activity execution and submit completion documents.'
        };

        await notificationService.sendToUsers(userIdsForNotification, {
          title: emailSubject,
          body: emailBody,
          requestId,
          requestNumber,
          url: `/request/${requestNumber}`,
          type: 'activity_created',
          priority: 'MEDIUM',
          actionRequired: false,
          metadata: {
            activityData: activityData
          }
        });
      }

      // Log Activity Creation as activity (no approval level needed)
      await activityService.log({
        requestId,
        type: 'status_change',
        user: { userId: 'system', name: 'System Auto-Process' },
        timestamp: new Date().toISOString(),
        action: 'Activity Created',
        details: `Activity "${activityName}" created. Activity confirmation email auto-triggered to dealer, requestor, and department lead. IO confirmation to be made.`,
      });

      logger.info(`[DealerClaimService] Activity Creation logged as activity for request ${requestId}. Activity creation completed.`);
    } catch (error) {
      logger.error(`[DealerClaimService] Error processing Step 4 activity creation for request ${requestId}:`, error);
      throw error;
    }
  }

  /**
   * Snapshot current claim state for version history before revisions
   */
  /**
   * Save proposal version history (Step 1)
   */
  async saveProposalHistory(
    requestId: string,
    approvalLevelId: string,
    levelNumber: number,
    changeReason: string,
    userId: string
  ): Promise<void> {
    try {
      const proposalDetails = await DealerProposalDetails.findOne({ where: { requestId } });
      if (!proposalDetails) {
        logger.warn(`[DealerClaimService] No proposal found for request ${requestId}, skipping history`);
        return;
      }

      const costItems = await DealerProposalCostItem.findAll({
        where: { proposalId: (proposalDetails as any).proposalId || (proposalDetails as any).proposal_id }
      });

      // Get level name from approval level
      const level = await ApprovalLevel.findByPk(approvalLevelId);
      const levelName = level?.levelName || undefined;

      // Get next version for this level (match by levelName for consistency)
      const lastVersion = await DealerClaimHistory.findOne({
        where: levelName ? {
          requestId,
          levelName,
          snapshotType: SnapshotType.PROPOSAL
        } : {
          requestId,
          levelNumber,
          snapshotType: SnapshotType.PROPOSAL
        },
        order: [['version', 'DESC']]
      });
      const nextVersion = lastVersion ? lastVersion.version + 1 : 1;

      // Store all proposal data in JSONB
      // Handle expectedCompletionDate - it might be a Date object, string, or null
      let expectedCompletionDateStr = null;
      if (proposalDetails.expectedCompletionDate) {
        if (proposalDetails.expectedCompletionDate instanceof Date) {
          expectedCompletionDateStr = proposalDetails.expectedCompletionDate.toISOString();
        } else if (typeof proposalDetails.expectedCompletionDate === 'string') {
          expectedCompletionDateStr = proposalDetails.expectedCompletionDate;
        }
      }

      // Fetch supporting documents
      const supportingDocs = await Document.findAll({
        where: {
          requestId,
          category: 'SUPPORTING',
          isDeleted: false
        },
        order: [['uploadedAt', 'DESC']]
      });

      const snapshotData = {
        documentUrl: proposalDetails.proposalDocumentUrl,
        totalBudget: Number(proposalDetails.totalEstimatedBudget || 0),
        comments: proposalDetails.dealerComments,
        expectedCompletionDate: expectedCompletionDateStr,
        costItems: costItems.map(i => ({
          description: i.itemDescription,
          amount: Number(i.amount || 0),
          quantity: Number(i.quantity || 1),
          hsnCode: i.hsnCode || '',
          gstRate: Number(i.gstRate || 0),
          gstAmt: Number(i.gstAmt || 0),
          cgstRate: Number(i.cgstRate || 0),
          cgstAmt: Number(i.cgstAmt || 0),
          sgstRate: Number(i.sgstRate || 0),
          sgstAmt: Number(i.sgstAmt || 0),
          igstRate: Number(i.igstRate || 0),
          igstAmt: Number(i.igstAmt || 0),
          utgstRate: Number(i.utgstRate || 0),
          utgstAmt: Number(i.utgstAmt || 0),
          cessRate: Number(i.cessRate || 0),
          cessAmt: Number(i.cessAmt || 0),
          totalAmt: Number(i.totalAmt || 0),
          isService: !!i.isService,
          order: i.itemOrder
        })),
        otherDocuments: supportingDocs.map(doc => ({
          documentId: doc.documentId,
          fileName: doc.fileName,
          originalFileName: doc.originalFileName,
          storageUrl: doc.storageUrl,
          uploadedAt: doc.uploadedAt
        }))
      };

      await DealerClaimHistory.create({
        requestId,
        approvalLevelId,
        levelNumber,
        levelName,
        version: nextVersion,
        snapshotType: SnapshotType.PROPOSAL,
        snapshotData,
        changeReason,
        changedBy: userId
      });

      logger.info(`[DealerClaimService] Saved proposal history (v${nextVersion}) for level ${levelNumber}, request ${requestId}`);
    } catch (error) {
      logger.error(`[DealerClaimService] Error saving proposal history for request ${requestId}:`, error);
    }
  }

  /**
   * Save completion version history (Step 4/5)
   */
  async saveCompletionHistory(
    requestId: string,
    approvalLevelId: string,
    levelNumber: number,
    changeReason: string,
    userId: string
  ): Promise<void> {
    try {
      const completionDetails = await DealerCompletionDetails.findOne({ where: { requestId } });
      if (!completionDetails) {
        logger.warn(`[DealerClaimService] No completion found for request ${requestId}, skipping history`);
        return;
      }

      const expenses = await DealerCompletionExpense.findAll({ where: { requestId } });

      // Get level name from approval level
      const level = await ApprovalLevel.findByPk(approvalLevelId);
      const levelName = level?.levelName || undefined;

      // Get next version for this level (match by levelName for consistency)
      const lastVersion = await DealerClaimHistory.findOne({
        where: levelName ? {
          requestId,
          levelName,
          snapshotType: SnapshotType.COMPLETION
        } : {
          requestId,
          levelNumber,
          snapshotType: SnapshotType.COMPLETION
        },
        order: [['version', 'DESC']]
      });
      const nextVersion = lastVersion ? lastVersion.version + 1 : 1;

      // Fetch supporting documents for completion
      const supportingDocs = await Document.findAll({
        where: {
          requestId,
          category: 'SUPPORTING',
          isDeleted: false
        },
        order: [['uploadedAt', 'DESC']]
      });

      // Store all completion data in JSONB
      const snapshotData = {
        documentUrl: (completionDetails as any).completionDocumentUrl || null,
        totalExpenses: Number(completionDetails.totalClosedExpenses || 0),
        comments: (completionDetails as any).completionDescription || null,
        expenses: expenses.map(e => ({
          description: e.description,
          amount: Number(e.amount || 0)
        })),
        otherDocuments: supportingDocs.map(doc => ({
          documentId: doc.documentId,
          fileName: doc.fileName,
          originalFileName: doc.originalFileName,
          storageUrl: doc.storageUrl,
          uploadedAt: doc.uploadedAt
        }))
      };

      await DealerClaimHistory.create({
        requestId,
        approvalLevelId,
        levelNumber,
        levelName,
        version: nextVersion,
        snapshotType: SnapshotType.COMPLETION,
        snapshotData,
        changeReason,
        changedBy: userId
      });

      logger.info(`[DealerClaimService] Saved completion history (v${nextVersion}) for level ${levelNumber}, request ${requestId}`);
    } catch (error) {
      logger.error(`[DealerClaimService] Error saving completion history for request ${requestId}:`, error);
    }
  }

  /**
   * Save internal order version history
   */
  async saveIOHistory(
    requestId: string,
    approvalLevelId: string,
    levelNumber: number,
    changeReason: string,
    userId: string
  ): Promise<void> {
    try {
      const internalOrder = await InternalOrder.findOne({ where: { requestId } });
      if (!internalOrder || !internalOrder.ioBlockedAmount || internalOrder.ioBlockedAmount <= 0) {
        logger.warn(`[DealerClaimService] No IO block found for request ${requestId}, skipping history`);
        return;
      }

      // Get level name from approval level
      const level = await ApprovalLevel.findByPk(approvalLevelId);
      const levelName = level?.levelName || undefined;

      // Get next version for this level (match by levelName for consistency)
      const lastVersion = await DealerClaimHistory.findOne({
        where: levelName ? {
          requestId,
          levelName,
          snapshotType: SnapshotType.INTERNAL_ORDER
        } : {
          requestId,
          levelNumber,
          snapshotType: SnapshotType.INTERNAL_ORDER
        },
        order: [['version', 'DESC']]
      });
      const nextVersion = lastVersion ? lastVersion.version + 1 : 1;

      // Store all IO data in JSONB
      const snapshotData = {
        ioNumber: internalOrder.ioNumber,
        blockedAmount: Number(internalOrder.ioBlockedAmount || 0),
        availableBalance: Number(internalOrder.ioAvailableBalance || 0),
        remainingBalance: Number(internalOrder.ioRemainingBalance || 0),
        sapDocumentNumber: internalOrder.sapDocumentNumber
      };

      await DealerClaimHistory.create({
        requestId,
        approvalLevelId,
        levelNumber,
        levelName,
        version: nextVersion,
        snapshotType: SnapshotType.INTERNAL_ORDER,
        snapshotData,
        changeReason,
        changedBy: userId
      });

      logger.info(`[DealerClaimService] Saved IO history (v${nextVersion}) for level ${levelNumber}, request ${requestId}`);
    } catch (error) {
      logger.error(`[DealerClaimService] Error saving IO history for request ${requestId}:`, error);
    }
  }

  /**
   * Save approval version history (for approver actions)
   */
  async saveApprovalHistory(
    requestId: string,
    approvalLevelId: string,
    levelNumber: number,
    action: 'APPROVE' | 'REJECT',
    comments: string,
    rejectionReason: string | undefined,
    userId: string
  ): Promise<void> {
    try {
      const level = await ApprovalLevel.findByPk(approvalLevelId);
      if (!level) {
        logger.warn(`[DealerClaimService] No approval level found for ${approvalLevelId}, skipping history`);
        return;
      }

      // Get next version for this level (match by levelName for consistency)
      const lastVersion = await DealerClaimHistory.findOne({
        where: level.levelName ? {
          requestId,
          levelName: level.levelName,
          snapshotType: SnapshotType.APPROVE
        } : {
          requestId,
          levelNumber,
          snapshotType: SnapshotType.APPROVE
        },
        order: [['version', 'DESC']]
      });
      const nextVersion = lastVersion ? lastVersion.version + 1 : 1;

      // Store approval data in JSONB
      const snapshotData = {
        action,
        comments: comments || undefined,
        rejectionReason: rejectionReason || undefined,
        approverName: level.approverName,
        approverEmail: level.approverEmail,
        levelName: level.levelName
      };

      // Build changeReason - will be updated later if moving to next level
      // For now, just include the basic approval/rejection info
      let changeReason = action === 'APPROVE'
        ? `Approved by ${level.approverName || level.approverEmail}`
        : `Rejected by ${level.approverName || level.approverEmail}`;

      if (action === 'REJECT' && (rejectionReason || comments)) {
        changeReason += `. Reason: ${rejectionReason || comments}`;
      }

      await DealerClaimHistory.create({
        requestId,
        approvalLevelId,
        levelNumber,
        levelName: level.levelName || undefined,
        version: nextVersion,
        snapshotType: SnapshotType.APPROVE,
        snapshotData,
        changeReason,
        changedBy: userId
      });

      logger.info(`[DealerClaimService] Saved approval history (v${nextVersion}) for level ${levelNumber}, request ${requestId}`);
    } catch (error) {
      logger.error(`[DealerClaimService] Error saving approval history for request ${requestId}:`, error);
    }
  }

  /**
   * Save workflow-level version history (for actions that move workflow forward/backward)
   */
  async saveWorkflowHistory(
    requestId: string,
    changeReason: string,
    userId: string,
    approvalLevelId?: string,
    levelNumber?: number,
    levelName?: string,
    approvalComment?: string
  ): Promise<void> {
    try {
      const wf = await WorkflowRequest.findByPk(requestId);
      if (!wf) return;

      // Get next version for workflow-level snapshots PER LEVEL
      // Each level should have its own version numbering starting from 1
      // Filter by levelName or levelNumber to get versions for this specific level
      const lastVersion = await DealerClaimHistory.findOne({
        where: levelName ? {
          requestId,
          levelName,
          snapshotType: SnapshotType.WORKFLOW
        } : levelNumber !== undefined ? {
          requestId,
          levelNumber,
          snapshotType: SnapshotType.WORKFLOW
        } : {
          requestId,
          snapshotType: SnapshotType.WORKFLOW
        },
        order: [['version', 'DESC']]
      });
      const nextVersion = lastVersion ? lastVersion.version + 1 : 1;

      // Store workflow data in JSONB
      // Include level information for version tracking and comparison
      // Include approval comment if provided (for approval actions)
      const snapshotData: any = {
        status: wf.status,
        currentLevel: wf.currentLevel,
        // Include level info in snapshotData for completeness and version tracking
        approvalLevelId: approvalLevelId || undefined,
        levelNumber: levelNumber || undefined,
        levelName: levelName || undefined
      };

      // Add approval comment to snapshotData if provided
      if (approvalComment) {
        snapshotData.comments = approvalComment;
      }

      await DealerClaimHistory.create({
        requestId,
        approvalLevelId: approvalLevelId || undefined,
        levelNumber: levelNumber || undefined,
        levelName: levelName || undefined,
        version: nextVersion,
        snapshotType: SnapshotType.WORKFLOW,
        snapshotData,
        changeReason,
        changedBy: userId
      });

      logger.info(`[DealerClaimService] Saved workflow history (v${nextVersion}) for request ${requestId}, level ${levelNumber || 'N/A'}`);
    } catch (error) {
      logger.error(`[DealerClaimService] Error saving workflow history for request ${requestId}:`, error);
    }
  }

  /**
   * Create or activate initiator action level when request is rejected
   * This allows initiator to take action (REVISE, CANCEL, REOPEN) directly from the step card
   */
  async createOrActivateInitiatorLevel(
    requestId: string,
    userId: string
  ): Promise<ApprovalLevel | null> {
    try {
      const wf = await WorkflowRequest.findByPk(requestId);
      if (!wf) return null;

      // Check if initiator level already exists
      let initiatorLevel = await ApprovalLevel.findOne({
        where: {
          requestId,
          levelName: 'Initiator Action'
        }
      });

      if (initiatorLevel) {
        // Activate existing level
        await initiatorLevel.update({
          status: ApprovalStatus.IN_PROGRESS,
          levelStartTime: new Date(),
          tatStartTime: new Date(),
          approverId: wf.initiatorId
        });
        return initiatorLevel;
      }

      // Create new initiator level
      // Find the highest level number to place it after
      const maxLevel = await ApprovalLevel.findOne({
        where: { requestId },
        order: [['levelNumber', 'DESC']]
      });
      const nextLevelNumber = maxLevel ? maxLevel.levelNumber + 1 : 0;

      // Get initiator user details
      const initiatorUser = await User.findByPk(wf.initiatorId);
      if (!initiatorUser) {
        throw new Error('Initiator user not found');
      }

      initiatorLevel = await ApprovalLevel.create({
        requestId,
        levelNumber: nextLevelNumber,
        levelName: 'Initiator Action',
        approverId: wf.initiatorId,
        approverEmail: initiatorUser.email || '',
        approverName: initiatorUser.displayName || initiatorUser.email || 'Initiator',
        status: ApprovalStatus.IN_PROGRESS,
        levelStartTime: new Date(),
        tatStartTime: new Date(),
        tatHours: 0, // No TAT for initiator action
        elapsedHours: 0,
        remainingHours: 0,
        tatPercentageUsed: 0,
        isFinalApprover: false
      } as any);

      logger.info(`[DealerClaimService] Created/activated initiator level for request ${requestId}`);
      return initiatorLevel;
    } catch (error) {
      logger.error(`[DealerClaimService] Error creating/activating initiator level:`, error);
      return null;
    }
  }

  /**
   * @deprecated - Removed complex snapshot method. Snapshots are now taken at step execution.
   */
  async saveCompleteRevisionSnapshot_DEPRECATED(
    requestId: string,
    changeReason: string,
    userId: string
  ): Promise<void> {
    try {
      logger.info(`[DealerClaimService] Capturing complete revision snapshot for request ${requestId}`);

      // 1. Capture current proposal snapshot (if exists)
      const proposalDetails = await DealerProposalDetails.findOne({ where: { requestId } });
      if (proposalDetails) {
        const costItems = await DealerProposalCostItem.findAll({
          where: { proposalId: (proposalDetails as any).proposalId || (proposalDetails as any).proposal_id }
        });

        // Find dealer proposal level
        const dealerLevel = await ApprovalLevel.findOne({
          where: {
            requestId,
            levelName: 'Dealer Proposal Submission'
          }
        }) || await ApprovalLevel.findOne({
          where: { requestId, levelNumber: 1 }
        });

        if (dealerLevel) {
          const proposalSnapshotData = {
            documentUrl: proposalDetails.proposalDocumentUrl,
            totalBudget: Number(proposalDetails.totalEstimatedBudget || 0),
            comments: proposalDetails.dealerComments,
            expectedCompletionDate: proposalDetails.expectedCompletionDate ? proposalDetails.expectedCompletionDate.toISOString() : null,
            costItems: costItems.map(i => ({
              description: i.itemDescription,
              amount: Number(i.amount || 0),
              order: i.itemOrder
            }))
          };

          // Get next version for this level
          const lastProposalVersion = await DealerClaimHistory.findOne({
            where: {
              requestId,
              levelName: dealerLevel.levelName || undefined,
              snapshotType: SnapshotType.PROPOSAL
            },
            order: [['version', 'DESC']]
          });
          const nextProposalVersion = lastProposalVersion ? lastProposalVersion.version + 1 : 1;

          await DealerClaimHistory.create({
            requestId,
            approvalLevelId: dealerLevel.levelId,
            levelNumber: dealerLevel.levelNumber,
            levelName: dealerLevel.levelName || undefined,
            version: nextProposalVersion,
            snapshotType: SnapshotType.PROPOSAL,
            snapshotData: proposalSnapshotData,
            changeReason: `${changeReason} - Pre-revision snapshot`,
            changedBy: userId
          });

          logger.info(`[DealerClaimService] Captured proposal snapshot (v${nextProposalVersion}) for revision`);
        }
      }

      // 2. Capture current completion snapshot (if exists)
      const completionDetails = await DealerCompletionDetails.findOne({ where: { requestId } });
      if (completionDetails) {
        const expenses = await DealerCompletionExpense.findAll({
          where: { completionId: (completionDetails as any).completionId || (completionDetails as any).completion_id }
        });

        // Find completion level
        const completionLevel = await ApprovalLevel.findOne({
          where: {
            requestId,
            levelName: 'Dealer Completion Documents'
          }
        }) || await ApprovalLevel.findOne({
          where: { requestId, levelNumber: 4 }
        });

        if (completionLevel) {
          const completionSnapshotData = {
            documentUrl: (completionDetails as any).completionDocumentUrl || null,
            totalExpenses: Number(completionDetails.totalClosedExpenses || 0),
            comments: (completionDetails as any).completionDescription || null,
            expenses: expenses.map(e => ({
              description: e.description,
              amount: Number(e.amount || 0)
            }))
          };

          // Get next version for this level
          const lastCompletionVersion = await DealerClaimHistory.findOne({
            where: {
              requestId,
              levelName: completionLevel.levelName || undefined,
              snapshotType: SnapshotType.COMPLETION
            },
            order: [['version', 'DESC']]
          });
          const nextCompletionVersion = lastCompletionVersion ? lastCompletionVersion.version + 1 : 1;

          await DealerClaimHistory.create({
            requestId,
            approvalLevelId: completionLevel.levelId,
            levelNumber: completionLevel.levelNumber,
            levelName: completionLevel.levelName || undefined,
            version: nextCompletionVersion,
            snapshotType: SnapshotType.COMPLETION,
            snapshotData: completionSnapshotData,
            changeReason: `${changeReason} - Pre-revision snapshot`,
            changedBy: userId
          });

          logger.info(`[DealerClaimService] Captured completion snapshot (v${nextCompletionVersion}) for revision`);
        }
      }

      // 3. Capture current IO snapshot (if exists)
      const internalOrder = await InternalOrder.findOne({ where: { requestId } });
      if (internalOrder && internalOrder.ioBlockedAmount && internalOrder.ioBlockedAmount > 0) {
        const ioLevel = await ApprovalLevel.findOne({
          where: {
            requestId,
            levelName: 'Department Lead IO Approval'
          }
        }) || await ApprovalLevel.findOne({
          where: { requestId, levelNumber: 3 }
        });

        if (ioLevel) {
          const ioSnapshotData = {
            ioNumber: internalOrder.ioNumber,
            blockedAmount: Number(internalOrder.ioBlockedAmount || 0),
            availableBalance: Number(internalOrder.ioAvailableBalance || 0),
            remainingBalance: Number(internalOrder.ioRemainingBalance || 0),
            sapDocumentNumber: internalOrder.sapDocumentNumber
          };

          // Get next version for this level
          const lastIOVersion = await DealerClaimHistory.findOne({
            where: {
              requestId,
              levelName: ioLevel.levelName || undefined,
              snapshotType: SnapshotType.INTERNAL_ORDER
            },
            order: [['version', 'DESC']]
          });
          const nextIOVersion = lastIOVersion ? lastIOVersion.version + 1 : 1;

          await DealerClaimHistory.create({
            requestId,
            approvalLevelId: ioLevel.levelId,
            levelNumber: ioLevel.levelNumber,
            levelName: ioLevel.levelName || undefined,
            version: nextIOVersion,
            snapshotType: SnapshotType.INTERNAL_ORDER,
            snapshotData: ioSnapshotData,
            changeReason: `${changeReason} - Pre-revision snapshot`,
            changedBy: userId
          });

          logger.info(`[DealerClaimService] Captured IO snapshot (v${nextIOVersion}) for revision`);
        }
      }

      // 4. Capture ALL approval comments from all levels (so approvers can see their previous comments)
      const allLevels = await ApprovalLevel.findAll({
        where: { requestId },
        order: [['levelNumber', 'ASC']]
      });

      for (const level of allLevels) {
        // Only capture if level has been acted upon (has comments or action date)
        if (level.comments || level.actionDate || level.status === ApprovalStatus.APPROVED || level.status === ApprovalStatus.REJECTED) {
          const approver = level.approverId ? await User.findByPk(level.approverId) : null;

          const approvalSnapshotData = {
            action: level.status === ApprovalStatus.APPROVED ? 'APPROVE' : level.status === ApprovalStatus.REJECTED ? 'REJECT' : 'PENDING',
            comments: level.comments || undefined,
            rejectionReason: level.status === ApprovalStatus.REJECTED ? (level.comments || undefined) : undefined,
            approverName: approver?.displayName || approver?.email || undefined,
            approverEmail: approver?.email || undefined,
            levelName: level.levelName || undefined
          };

          // Get next version for this level's approval snapshot
          const lastApprovalVersion = await DealerClaimHistory.findOne({
            where: {
              requestId,
              levelName: level.levelName || undefined,
              snapshotType: SnapshotType.APPROVE
            },
            order: [['version', 'DESC']]
          });
          const nextApprovalVersion = lastApprovalVersion ? lastApprovalVersion.version + 1 : 1;

          await DealerClaimHistory.create({
            requestId,
            approvalLevelId: level.levelId,
            levelNumber: level.levelNumber,
            levelName: level.levelName || undefined,
            version: nextApprovalVersion,
            snapshotType: SnapshotType.APPROVE,
            snapshotData: approvalSnapshotData,
            changeReason: `${changeReason} - Pre-revision approval snapshot`,
            changedBy: userId
          });

          logger.info(`[DealerClaimService] Captured approval snapshot (v${nextApprovalVersion}) for level ${level.levelNumber} (${level.levelName})`);
        }
      }

      // 5. Save workflow-level snapshot
      const wf = await WorkflowRequest.findByPk(requestId);
      if (wf) {
        const lastWorkflowVersion = await DealerClaimHistory.findOne({
          where: {
            requestId,
            snapshotType: SnapshotType.WORKFLOW
          },
          order: [['version', 'DESC']]
        });
        const nextWorkflowVersion = lastWorkflowVersion ? lastWorkflowVersion.version + 1 : 1;

        await DealerClaimHistory.create({
          requestId,
          version: nextWorkflowVersion,
          snapshotType: SnapshotType.WORKFLOW,
          snapshotData: {
            status: wf.status,
            currentLevel: wf.currentLevel
          },
          changeReason: `${changeReason} - Pre-revision workflow snapshot`,
          changedBy: userId
        });

        logger.info(`[DealerClaimService] Captured workflow snapshot (v${nextWorkflowVersion}) for revision`);
      }

      logger.info(`[DealerClaimService] Complete revision snapshot captured for request ${requestId}`);
    } catch (error) {
      logger.error(`[DealerClaimService] Error saving complete revision snapshot for request ${requestId}:`, error);
      // Don't throw - we want to continue even if snapshot fails
    }
  }

  /**
   * Handle initiator actions when a request is in RETURNED status
   */
  async handleInitiatorAction(
    requestId: string,
    userId: string,
    action: 'REOPEN' | 'DISCUSS' | 'REVISE' | 'CANCEL',
    data?: { reason: string }
  ): Promise<void> {
    const wf = await WorkflowRequest.findByPk(requestId);
    if (!wf) throw new Error('Request not found');

    // Check if the current user is the initiator
    if (wf.initiatorId !== userId) {
      throw new Error('Only the initiator can perform actions on a rejected/returned request');
    }

    // A returned request is REJECTED but has NO closureDate
    if (wf.status !== WorkflowStatus.REJECTED || wf.closureDate) {
      throw new Error(`Request is in ${wf.status} status (Closed: ${!!wf.closureDate}), expected an open REJECTED state to perform this action`);
    }

    const initiator = await User.findByPk(userId);
    const initiatorName = initiator?.displayName || initiator?.email || 'Initiator';
    const now = new Date();

    switch (action) {
      case 'CANCEL': {
        // Format change reason to include the comment if provided
        const changeReason = data?.reason && data.reason.trim()
          ? `Request Cancelled: ${data.reason.trim()}`
          : 'Request Cancelled';

        // Find current level for workflow history
        const currentLevel = await ApprovalLevel.findOne({
          where: { requestId, levelNumber: wf.currentLevel || 1 }
        });

        await wf.update({
          status: WorkflowStatus.CLOSED,
          closureDate: now
        });

        await activityService.log({
          requestId,
          type: 'status_change',
          user: { userId, name: initiatorName },
          timestamp: now.toISOString(),
          action: 'Request Cancelled',
          details: data?.reason && data.reason.trim()
            ? `Request was cancelled by initiator. Reason: ${data.reason.trim()}`
            : 'Request was cancelled by initiator.'
        });
        break;
      }

      case 'REOPEN': {
        // Format change reason to include the comment if provided
        const changeReason = data?.reason && data.reason.trim()
          ? `Request Reopened: ${data.reason.trim()}`
          : 'Request Reopened';

        // Find Department Lead level dynamically (handles step shifts)
        const approvalsReopen = await ApprovalLevel.findAll({ where: { requestId } });
        const deptLeadLevel = approvalsReopen.find(l => {
          const name = (l.levelName || '').toLowerCase();
          return name.includes('department lead') || name.includes('dept lead') || l.levelNumber === 3;
        });

        if (!deptLeadLevel) {
          throw new Error('Department Lead approval level not found for this request');
        }

        const deptLeadLevelNumber = deptLeadLevel.levelNumber;

        // Move back to Department Lead Approval level FIRST
        await wf.update({
          status: WorkflowStatus.PENDING,
          currentLevel: deptLeadLevelNumber
        });

        // Capture workflow snapshot AFTER workflow update succeeds
        try {
          await this.saveWorkflowHistory(
            requestId,
            `Reopened and moved to Department Lead level (${deptLeadLevelNumber}) - ${changeReason}`,
            userId,
            deptLeadLevel.levelId,
            deptLeadLevelNumber,
            deptLeadLevel.levelName || undefined
          );
        } catch (snapshotError) {
          // Log error but don't fail the reopen - snapshot is for audit, not essential
          logger.error(`[DealerClaimService] Failed to save workflow history snapshot (non-critical):`, snapshotError);
        }

        // Reset the found level status to IN_PROGRESS so Dept Lead can approve again
        await deptLeadLevel.update({
          status: ApprovalStatus.IN_PROGRESS,
          levelStartTime: now,
          tatStartTime: now,
          actionDate: undefined,
          comments: undefined
        });

        await activityService.log({
          requestId,
          type: 'approval',
          user: { userId, name: initiatorName },
          timestamp: now.toISOString(),
          action: 'Request Reopened',
          details: data?.reason && data.reason.trim()
            ? `Initiator reopened the request for Department Lead approval. Reason: ${data.reason.trim()}`
            : 'Initiator reopened the request for Department Lead approval.'
        });

        if (deptLeadLevel.approverId) {
          await notificationService.sendToUsers([deptLeadLevel.approverId], {
            title: `Request Reopened: ${wf.requestNumber}`,
            body: `Initiator has reopened the request "${wf.title}" after revision/discussion.`,
            requestNumber: wf.requestNumber,
            requestId: wf.requestId,
            url: `/request/${wf.requestNumber}`,
            type: 'assignment',
            priority: 'HIGH',
            actionRequired: true
          });
        }
        break;
      }

      case 'DISCUSS': {
        // Format change reason to include the comment if provided
        const changeReason = data?.reason && data.reason.trim()
          ? `Discussion Requested: ${data.reason.trim()}`
          : 'Discussion Requested';

        // Find Dealer level dynamically
        const approvalsDiscuss = await ApprovalLevel.findAll({ where: { requestId } });
        const dealerLevelDiscuss = approvalsDiscuss.find(l => {
          const name = (l.levelName || '').toLowerCase();
          return name.includes('dealer proposal') || l.levelNumber === 1;
        });

        // Note: DISCUSS action doesn't change workflow state, so no snapshot needed
        // The action is logged in activity log only

        await activityService.log({
          requestId,
          type: 'status_change',
          user: { userId, name: initiatorName },
          timestamp: now.toISOString(),
          action: 'Discuss with Dealer',
          details: data?.reason && data.reason.trim()
            ? `Initiator indicated they will discuss with the dealer. Reason: ${data.reason.trim()}`
            : 'Initiator indicated they will discuss with the dealer.'
        });

        if (dealerLevelDiscuss?.approverId) {
          await notificationService.sendToUsers([dealerLevelDiscuss.approverId], {
            title: `Discussion Requested: ${wf.requestNumber}`,
            body: `The initiator of request "${wf.title}" wants to discuss the proposal with you.`,
            requestNumber: wf.requestNumber,
            requestId: wf.requestId,
            url: `/request/${wf.requestNumber}`,
            type: 'info',
            priority: 'MEDIUM'
          });
        }
        break;
      }

      case 'REVISE': {
        // Format change reason
        const changeReason = data?.reason && data.reason.trim()
          ? `Revision Requested: ${data.reason.trim()}`
          : 'Revision Requested';

        // Find current level and previous level
        const allLevels = await ApprovalLevel.findAll({
          where: { requestId },
          order: [['levelNumber', 'ASC']]
        });

        const currentLevelNumber = wf.currentLevel || 1;
        const currentLevel = allLevels.find(l => l.levelNumber === currentLevelNumber);

        if (!currentLevel) {
          throw new Error('Current approval level not found');
        }

        // Find previous level (the one before current)
        const previousLevel = allLevels.find(l => l.levelNumber < currentLevelNumber);

        if (!previousLevel) {
          throw new Error('No previous level found to revise to');
        }

        // Move back to previous level FIRST
        await wf.update({
          status: WorkflowStatus.PENDING,
          currentLevel: previousLevel.levelNumber
        });

        // Capture workflow snapshot AFTER workflow update succeeds
        try {
          await this.saveWorkflowHistory(
            requestId,
            `Moved back to previous level (${previousLevel.levelNumber}) - ${changeReason}`,
            userId,
            previousLevel.levelId,
            previousLevel.levelNumber,
            previousLevel.levelName || undefined
          );
        } catch (snapshotError) {
          // Log error but don't fail the revise - snapshot is for audit, not essential
          logger.error(`[DealerClaimService] Failed to save workflow history snapshot (non-critical):`, snapshotError);
        }

        // Reset current level to PENDING
        await currentLevel.update({
          status: ApprovalStatus.PENDING,
          actionDate: undefined,
          levelStartTime: undefined,
          levelEndTime: undefined,
          tatStartTime: undefined,
          elapsedHours: 0,
          tatPercentageUsed: 0,
          comments: undefined
        });

        // Activate previous level
        await previousLevel.update({
          status: ApprovalStatus.IN_PROGRESS,
          levelStartTime: now,
          tatStartTime: now,
          comments: changeReason, // Save revision reason as comment
          actionDate: undefined,
          levelEndTime: undefined,
          elapsedHours: 0,
          tatPercentageUsed: 0
        });

        await activityService.log({
          requestId,
          type: 'assignment',
          user: { userId, name: initiatorName },
          timestamp: now.toISOString(),
          action: 'Revision Requested',
          details: data?.reason && data.reason.trim()
            ? `Initiator requested revision. Moving back to previous step. Reason: ${data.reason.trim()}`
            : 'Initiator requested revision. Moving back to previous step.'
        });

        // Notify the approver of the previous level
        if (previousLevel.approverId) {
          await notificationService.sendToUsers([previousLevel.approverId], {
            title: `Revision Required: ${wf.requestNumber}`,
            body: `Initiator has requested a revision for request "${wf.title}". The request has been moved back to your level.`,
            requestNumber: wf.requestNumber,
            requestId: wf.requestId,
            url: `/request/${wf.requestNumber}`,
            type: 'assignment',
            priority: 'HIGH',
            actionRequired: true
          });
        }
        break;
      }
    }

    const { emitToRequestRoom } = await import('../realtime/socket');
    emitToRequestRoom(requestId, 'request:updated', {
      requestId,
      requestNumber: wf.requestNumber,
      action: `INITIATOR_${action}`,
      timestamp: now.toISOString()
    });
  }

  async getHistory(requestId: string): Promise<any[]> {
    const history = await DealerClaimHistory.findAll({
      where: { requestId },
      order: [['version', 'DESC']],
      include: [
        {
          model: User,
          as: 'changer',
          attributes: ['userId', 'displayName', 'email']
        }
      ]
    });

    // Map to plain objects and sort otherDocuments in snapshots
    return history.map(item => {
      const plain = item.get({ plain: true });
      if (plain.snapshotData && plain.snapshotData.otherDocuments && Array.isArray(plain.snapshotData.otherDocuments)) {
        plain.snapshotData.otherDocuments.sort((a: any, b: any) => {
          const dateA = a.uploadedAt ? new Date(a.uploadedAt).getTime() : 0;
          const dateB = b.uploadedAt ? new Date(b.uploadedAt).getTime() : 0;
          return dateB - dateA;
        });
      }
      return plain;
    });
  }

  /**
   * Push CSV to WFM folder and track status
   * This is used by both auto-trigger and manual re-trigger
   */
  async pushWFMCSV(requestId: string): Promise<void> {
    const invoice = await ClaimInvoice.findOne({ where: { requestId } });
    if (!invoice) {
        throw new Error('Invoice not found');
    }

    try {
        const [invoiceItems, claimDetails, internalOrder] = await Promise.all([
            ClaimInvoiceItem.findAll({ where: { requestId } }),
            DealerClaimDetails.findOne({ where: { requestId } }),
            InternalOrder.findOne({ where: { requestId } })
        ]);

        if (!claimDetails) {
            throw new Error('Dealer claim details not found');
        }

        const requestNumber = (await WorkflowRequest.findByPk(requestId))?.requestNumber || 'UNKNOWN';

        if (invoiceItems.length > 0) {
            let sapRefNo = '';
            let isNonGst = false;
            
            if (claimDetails.activityType) {
              const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
              sapRefNo = activity?.sapRefNo || '';
              const taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST');
              isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
            }

            const csvData = invoiceItems.map((item: any) => {
                if (isNonGst) {
                  return {
                    TRNS_UNIQ_NO: item.transactionCode || '',
                    CLAIM_NUMBER: requestNumber,
                    INV_NUMBER: invoice.invoiceNumber || '',
                    DEALER_CODE: padDealerCode(claimDetails.dealerCode),
                    IO_NUMBER: internalOrder?.ioNumber || '',
                    CLAIM_DOC_TYP: sapRefNo,
                    CLAIM_TYPE: claimDetails.activityType,
                    CLAIM_DATE: (() => {
                      const d = new Date(invoice.invoiceDate || new Date());
                      return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
                    })(),
                    CLAIM_AMT: item.assAmt
                  };
                }

                return buildWfmClaimCsvRow({
                  item,
                  requestNumber,
                  invoiceNumber: invoice.invoiceNumber || '',
                  invoiceDate: (invoice.invoiceDate as Date) || new Date(),
                  dealerCode: claimDetails.dealerCode,
                  ioNumber: internalOrder?.ioNumber || '',
                  claimDocTyp: sapRefNo,
                  claimType: claimDetails.activityType || '',
                });
            });
            
            await wfmFileService.generateIncomingClaimCSV(csvData, `CN_${padDealerCode(claimDetails.dealerCode)}_${requestNumber}.csv`, isNonGst);
            
            await invoice.update({
                wfmPushStatus: 'SUCCESS',
                wfmPushError: null
            });

            logger.info(`[DealerClaimService] WFM CSV successfully pushed for request ${requestNumber}`);
        } else {
            logger.warn(`[DealerClaimService] No invoice items found for WFM push: ${requestNumber}`);
        }
    } catch (error: any) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error pushing to WFM';
        await invoice.update({
            wfmPushStatus: 'FAILED',
            wfmPushError: errorMessage
        });
        throw error;
    }
  }
}

