import { Request, Response } from 'express';
import db from '../../database/models/index.js';
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, State, Region, Zone, SecurityDeposit, FddAssignment, FddReport, OnboardingDocument, Worknote, StageApprovalAction, DealerCode, Dealer, RequestParticipant, QuestionnaireResponse, QuestionnaireQuestion, QuestionnaireOption, Questionnaire, User } = db;
import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS, ROLES } from '../../common/config/constants.js';
import { v4 as uuidv4 } from 'uuid';
import { Op } from 'sequelize';
import { AuthRequest } from '../../types/express.types.js';

import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } from '../../common/utils/email.service.js';
import { syncLocationManagers } from '../master/syncHierarchy.service.js';
import { WorkflowService } from '../../services/WorkflowService.js';
import { WorkflowIntegrityService } from '../../services/WorkflowIntegrityService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js';
import { pickApplicationAuditContext, safeAuditLogCreate } from '../../services/applicationAuditLog.service.js';
import { isAutoAssignmentEnabled } from '../../services/AutoAssignmentConfigService.js';
import { NotificationService } from '../../services/NotificationService.js';
import {
    formatDueDateDaysFromNow,
    DEFAULT_PROSPECT_DOCUMENT_CHECKLIST,
    DEFAULT_ARCHITECTURE_SITE_INPUTS
} from '../../constants/onboarding-email-defaults.js';

const { DocumentStageConfig } = db;

// Helper to find district by name and state name combination
const findDistrictByName = async (districtName: string, stateName?: string) => {
    if (!districtName) return null;
    
    return await District.findOne({
        where: { name: { [Op.iLike]: districtName.trim() } },
        include: stateName ? [{
            model: State,
            as: 'state',
            where: { name: { [Op.iLike]: stateName.trim() } }
        }] : []
    });
};

const toTitleCase = (str: string) => {
    if (!str) return str;
    return str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
};

export const submitApplication = async (req: AuthRequest, res: Response) => {
    try {
        const {
            opportunityId,
            applicantName, email, phone, businessType, locationType,
            preferredLocation, city, state, experienceYears, investmentCapacity,
            age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, constitutionType
        } = req.body;

        // Check for duplicate application for SAME location
        const existingApp = await Application.findOne({ 
            where: { 
                email,
                city: city || null,
                preferredLocation: preferredLocation || null,
                overallStatus: { [Op.ne]: 'Rejected' } // Don't block if previous was rejected
            } 
        });

        if (existingApp) {
            return res.status(400).json({ 
                success: false, 
                message: 'An active application for this location already exists with this email address.' 
            });
        }

        const applicationId = await NomenclatureService.generateApplicationId();
        let districtId = null;
        // Normalize incoming ID sources for robustness
        const incomingLocationId = req.body.locationId || req.body.districtId;
        
        // Primary Mapping: Resolve district by Name (State + District combination)
        // This is robust for external sources where ID mapping is difficult.
        if (req.body.district) {
            const districtRecord: any = await findDistrictByName(req.body.district, req.body.state);
            if (districtRecord) {
                districtId = districtRecord.id;
            }
        }

        // Secondary Fallback: If ID is explicitly provided
        if (!districtId && incomingLocationId) {
            const selectedDistrict = await District.findByPk(incomingLocationId);
            if (selectedDistrict) {
                districtId = selectedDistrict.id;
            }
        }

        let activeOpportunityId = null;

        // CASE 1: Explicit Opportunity ID provided (Trust but verify active status)
        if (opportunityId) {
            const explicitOpp = await Opportunity.findOne({
                where: {
                    id: opportunityId,
                    status: 'active',
                    [Op.or]: [
                        { openTo: null },
                        { openTo: { [Op.gte]: new Date() } }
                    ]
                }
            });
            if (explicitOpp) {
                activeOpportunityId = explicitOpp.id;
                if (!districtId) districtId = explicitOpp.districtId;
            }
        }

        // CASE 2: Auto-discover active opportunity for the resolved district
        if (!activeOpportunityId && districtId) {
            const opportunity = await Opportunity.findOne({
                where: {
                    districtId,
                    status: 'active',
                    [Op.or]: [
                        { openTo: null },
                        { openTo: { [Op.gte]: new Date() } }
                    ]
                }
            });
            if (opportunity) {
                activeOpportunityId = opportunity.id;
            }
        }

        const isOpportunityAvailable = !!activeOpportunityId;

        const application = await Application.create({
            opportunityId: activeOpportunityId,
            applicationId,
            applicantName,
            email,
            phone,
            businessType,
            preferredLocation,
            city,
            state,
            experienceYears,
            investmentCapacity,
            age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType, 
            constitutionType: constitutionType || 'Proprietorship',
            currentStage: APPLICATION_STAGES.DD,
            overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
            progressPercentage: isOpportunityAvailable ? 10 : 0,
            districtId,
            score: 0,
            documents: [],
            timeline: []
        });

        // Use WorkflowService for initial status and progress sync
        await WorkflowService.transitionApplication(application, application.overallStatus, req.user?.id || null, {
            reason: 'Initial Submission',
            stage: application.currentStage
        });

        // Ensure district/region/zone manager pointers are fresh, then auto-map evaluators
        // so Application Details shows RBM/ZBH (and other mapped roles) immediately.
        if (districtId) {
            await syncLocationManagers(districtId);
            await assignStageEvaluators(application.id);
        }

        // Send Email (Async)
        const displayApplicantName = toTitleCase(applicantName);
        const displayLocation = toTitleCase(city || preferredLocation);

        if (isOpportunityAvailable) {
            sendOpportunityEmail(email, displayApplicantName, displayLocation, applicationId)
                .then(() => {
                    db.AuditLog.create({
                        userId: req.user?.id || null,
                        action: 'EMAIL_SENT',
                        entityType: 'application',
                        entityId: application.id,
                        newData: { email, type: 'OPPORTUNITY', location: displayLocation }
                    }).catch((err: any) => console.error('AuditLog error for email:', err));
                })
                .catch((err: any) => console.error('Error sending opportunity email', err));
        } else {
            sendNonOpportunityEmail(email, displayApplicantName, displayLocation)
                .then(() => {
                    db.AuditLog.create({
                        userId: req.user?.id || null,
                        action: 'EMAIL_SENT',
                        entityType: 'application',
                        entityId: application.id,
                        newData: { email, type: 'NON_OPPORTUNITY', location: displayLocation }
                    }).catch((err: any) => console.error('AuditLog error for email:', err));
                })
                .catch((err: any) => console.error('Error sending non-opportunity email', err));
        }

        await AuditLog.create({
            userId: req.user?.id,
            action: AUDIT_ACTIONS.CREATED,
            entityType: 'application',
            entityId: application.id
        });

        // SRS §6.1.3 — WhatsApp acknowledgement to applicant on submission (alongside email)
        // The email is handled above; now send WhatsApp if phone is available
        if (phone) {
            NotificationService.notify(null, email, {
                title: isOpportunityAvailable
                    ? `Your Royal Enfield Dealership Application: ${applicationId}`
                    : `We received your interest: ${applicationId}`,
                message: isOpportunityAvailable
                    ? `Hi ${displayApplicantName}, your application for ${displayLocation} has been received. We will contact you soon.`
                    : `Hi ${displayApplicantName}, we have noted your interest for ${displayLocation}. We'll reach out when an opportunity arises.`,
                channels: ['whatsapp'],
                templateCode: isOpportunityAvailable ? 'OPPORTUNITY' : 'NON_OPPORTUNITY',
                placeholders: {
                    applicantName: displayApplicantName,
                    location: displayLocation,
                    applicationId,
                    link: isOpportunityAvailable
                        ? `${getFrontendBaseUrl()}/questionnaire/${applicationId}`
                        : undefined,
                    ctaLabel: isOpportunityAvailable ? 'Complete Questionnaire' : undefined,
                    phone
                }
            }).catch((err: any) => console.error('[Onboarding] WhatsApp ack failed:', err));
        }

        res.status(201).json({
            success: true,
            message: 'Application submitted successfully',
            data: application
        });
    } catch (error) {
        console.error('Submit application error:', error);
        res.status(500).json({ success: false, message: 'Error submitting application' });
    }
};

export const getApplications = async (req: AuthRequest, res: Response) => {
    try {
        const whereClause: any = {};

        // Determine if user has national-level visibility
        const nationalRoles = [
            ROLES.NBH, 
            ROLES.DD_HEAD, 
            ROLES.DD_LEAD, 
            ROLES.FINANCE, 
            ROLES.DD_ADMIN, 
            ROLES.LEGAL_ADMIN, 
            ROLES.SUPER_ADMIN,
            ROLES.CCO,
            ROLES.CEO,
            'Admin' // Keep legacy support if any
        ];
        const userRole = String(req.user?.roleCode || '').trim();
        const isNationalUser = nationalRoles.some(r => String(r).trim().toLowerCase() === userRole.toLowerCase());
        const isProspectiveDealer = userRole.toLowerCase() === 'prospective dealer';

        if (isProspectiveDealer) {
            whereClause.phone = (req.user as any).phone || req.user?.email;
        } else {
            // REVOCATION CHECK: Fetch specific participant status for this user
            const participantRecords = await db.RequestParticipant.findAll({
                where: { userId: req.user?.id, requestType: 'application' },
                attributes: ['requestId', 'metadata']
            });

            const activeAppIds = participantRecords
                .filter((p: any) => !p.metadata?.revokedAt)
                .map((p: any) => p.requestId);
                
            const revokedAppIds = participantRecords
                .filter((p: any) => p.metadata?.revokedAt)
                .map((p: any) => p.requestId);

            if (!isNationalUser) {
                // Non-national users: ONLY see applications where they are an ACTIVE participant
                whereClause.id = { [Op.in]: activeAppIds };
            } else if (revokedAppIds.length > 0 && userRole.toLowerCase() !== 'super admin') {
                // National users (except Super Admin): See all applications EXCEPT those where they were explicitly revoked
                whereClause.id = { [Op.notIn]: revokedAppIds };
            }
        }

        // Apply Filters
        const { fromDate, toDate, search, status, location, state, isShortlisted, ddLeadShortlisted, assignedTo } = req.query;
        
        // 1. Date Filters (createdAt range)
        if (fromDate || toDate) {
            const dateClause: any = {};
            if (fromDate && fromDate !== 'undefined') {
                const start = new Date(fromDate as string);
                if (!isNaN(start.getTime())) {
                    start.setHours(0, 0, 0, 0);
                    dateClause[Op.gte] = start;
                }
            }
            if (toDate && toDate !== 'undefined') {
                const end = new Date(toDate as string);
                if (!isNaN(end.getTime())) {
                    end.setHours(23, 59, 59, 999);
                    dateClause[Op.lte] = end;
                }
            }
            if (Object.keys(dateClause).length > 0) {
                whereClause.createdAt = dateClause;
            }
        }

        if (search) {
            whereClause[Op.or] = [
                { applicantName: { [Op.iLike]: `%${search}%` } },
                { applicationId: { [Op.iLike]: `%${search}%` } },
                { email: { [Op.iLike]: `%${search}%` } },
                { phone: { [Op.iLike]: `%${search}%` } }
            ];
        }

        const applyStatusFilter = (val: any) => {
            if (!val || val === 'all') return;
            if (Array.isArray(val)) {
                whereClause.overallStatus = { [Op.in]: val };
            } else if (typeof val === 'string' && val.includes(',')) {
                whereClause.overallStatus = { [Op.in]: val.split(',') };
            } else {
                whereClause.overallStatus = val;
            }
        };

        // Pipeline Logic - Forced strict filtering by lifecycle stage
        // 3. Status Grouping Logic (Prospects vs Leads vs Workflow)
        const isShortlistedStr = String(isShortlisted ?? '').toLowerCase();
        const ddLeadShortlistedStr = String(ddLeadShortlisted ?? '').toLowerCase();

        // Use a conditions array to prevent Op.or overwrites
        const conditions: any[] = [];

        if (isShortlistedStr === 'false') {
            // Non-Opportunities (New Leads) MUST be 'Submitted', NOT shortlisted, and NOT linked to an opportunity
            whereClause.overallStatus = 'Submitted';
            whereClause.isShortlisted = { [Op.ne]: true };
            whereClause.ddLeadShortlisted = { [Op.ne]: true };
            whereClause.opportunityId = null; // Strictly lead-gen records only
        } else if (isShortlistedStr === 'true' && ddLeadShortlistedStr !== 'true') {
            // Opportunities (Prospects): include anything explicitly shortlisted OR in an opportunity status
            conditions.push({
                [Op.or]: [
                    { isShortlisted: true },
                    { overallStatus: { [Op.in]: ['Questionnaire Pending', 'Questionnaire Completed', 'Shortlisted'] } }
                ]
            });
            // However, must NOT be shortlisted by DD Lead yet (that moves them to Workflow)
            whereClause.ddLeadShortlisted = { [Op.ne]: true };

            if (status && status !== 'all') {
                applyStatusFilter(status);
            } else if (!whereClause.overallStatus) {
                whereClause.overallStatus = { [Op.in]: ['Questionnaire Pending', 'Questionnaire Completed', 'Shortlisted'] };
            }
        } else if (ddLeadShortlistedStr === 'true') {
            // Workflow strictly shows shortlisted by DD Lead
            whereClause.ddLeadShortlisted = true;
            applyStatusFilter(status);
        } else {
            // 'All Requests' or other general views
            applyStatusFilter(status);
        }

        if (conditions.length > 0) {
            whereClause[Op.and] = [...(whereClause[Op.and] || []), ...conditions];
        }

        if (location && location !== 'all') {
            whereClause.preferredLocation = location;
        }
        if (state && state !== 'all') {
            whereClause.state = state;
        }
        if (assignedTo) {
            whereClause.assignedTo = assignedTo;
        }

        // Apply Pagination
        const page = parseInt(req.query.page as string) || 1;
        const limit = parseInt(req.query.limit as string) || 10;
        const offset = (page - 1) * limit;

        const { count, rows: applications } = await Application.findAndCountAll({
            where: whereClause,
            include: [
                { model: Opportunity, as: 'opportunity', attributes: ['opportunityType', 'city', 'id'] },
                { model: SecurityDeposit, as: 'securityDeposits' }
            ],
            order: [['createdAt', 'DESC']],
            limit,
            offset,
            distinct: true,
            col: 'id'
        });

        // Get additional stats for the header in parallel to reduce response time
        const [uniqueLocationsCount, withExperienceCount] = await Promise.all([
            Application.count({
                where: whereClause,
                distinct: true,
                col: 'preferredLocation'
            }),
            Application.count({
                where: {
                    ...whereClause,
                    experienceYears: { [Op.gt]: 0 }
                }
            })
        ]);

        const stats = {
            total: count,
            uniqueLocations: uniqueLocationsCount,
            withExperience: withExperienceCount
        };

        res.json({ 
            success: true, 
            data: applications,
            meta: {
                total: count,
                totalPages: Math.ceil(count / limit),
                currentPage: page,
                limit,
                stats
            }
        });
    } catch (error) {
        console.error('Get applications error:', error);
        res.status(500).json({ success: false, message: 'Error fetching applications' });
    }
};

export const getApplicationById = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const targetId = id as string;

        const where: any = {};
        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        
        if (isUUID) {
            where[Op.or] = [{ id: targetId }, { applicationId: targetId }];
        } else {
            where.applicationId = targetId;
        }

        // PROACTIVE INTEGRITY CHECK: Ensure application isn't stalled before returning
        await WorkflowIntegrityService.synchronizeApplicationState(targetId);

        const application = await Application.findOne({
            where,
            include: [
                { model: ApplicationStatusHistory, as: 'statusHistory', separate: true, order: [['createdAt', 'DESC']] },
                { model: ApplicationProgress, as: 'progressTracking', separate: true, order: [['stageOrder', 'ASC']] },
                { 
                    model: SecurityDeposit, 
                    as: 'securityDeposits',
                    include: [{ model: User, as: 'verifier', attributes: ['fullName'] }] 
                },
                {
                    model: QuestionnaireResponse,
                    as: 'questionnaireResponses',
                    separate: true,
                    include: [
                        {
                            model: QuestionnaireQuestion,
                            as: 'question',
                            include: [{ model: QuestionnaireOption, as: 'questionOptions' }]
                        }
                    ]
                },
                { 
                    model: RequestParticipant, 
                    as: 'participants', 
                    separate: true,
                    include: [{ model: User, as: 'user', attributes: ['id', ['fullName', 'name'], 'email', ['roleCode', 'role']] }] 
                },
                { 
                    model: OnboardingDocument, 
                    as: 'uploadedDocuments', 
                    separate: true,
                    include: [{ model: User, as: 'uploader', attributes: ['fullName', 'roleCode'] }],
                    order: [['createdAt', 'DESC']]
                },
                { model: StageApprovalAction, as: 'stageApprovals', separate: true },
                { model: DealerCode, as: 'dealerCode' },
                { model: Dealer, as: 'dealer' },
                {
                    model: FddAssignment,
                    as: 'fddAssignments',
                    include: [
                        {
                            model: FddReport,
                            as: 'reports',
                            include: [{ model: OnboardingDocument, as: 'reportDocument' }]
                        }
                    ]
                },
                { 
                    model: Worknote, 
                    as: 'worknotes', 
                    separate: true,
                    include: [{ model: User, as: 'author', attributes: ['id', 'fullName', 'email', 'roleCode'] }],
                    order: [['createdAt', 'DESC']]
                }
            ]
        });

        if (!application) {
            return res.status(404).json({ success: false, message: 'Application not found' });
        }

        // Security Check for FDD: If user is FDD, only return restricted data
        if (req.user?.roleCode === 'FDD') {
            const isParticipant = await db.RequestParticipant.findOne({
                where: { requestId: application.id, userId: req.user.id, requestType: 'application' }
            });

            if (!isParticipant) {
                return res.status(403).json({ success: false, message: 'Access denied. You are not assigned to this application.' });
            }

            // Strip sensitive internal data for FDD
            const restrictedData = application.toJSON();
            delete (restrictedData as any).questionnaireResponses;
            delete (restrictedData as any).stageApprovals;
            delete (restrictedData as any).score;

            // FDD should only see relevant documents for security
            // FDD should only see relevant documents for security 
            // OR documents they uploaded themselves
            const fddRelevantDocs = [
                'GST Certificate', 'PAN Card', 'Bank Statement', 'Cancelled Check', 
                'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 
                'Property Documents', 'Rental Agreement', 'Firm Registration', 'CIBIL Report', 
                'FDD Final Audit Report', 'FDD Audit Report', 'Income Tax Returns (ITR)', 'Business Valuation Report'
            ];
            
            if (restrictedData.uploadedDocuments) {
                restrictedData.uploadedDocuments = (restrictedData.uploadedDocuments as any[]).filter(
                    (doc: any) => fddRelevantDocs.includes(doc.documentType) || (req.user && doc.uploadedBy === req.user.id)
                );
            }
            
            return res.json({ success: true, data: restrictedData });
        }

        // Security Check: Ensure prospective dealer controls data ownership and document privacy
        if (req.user?.roleCode === 'Prospective Dealer') {
            const userEmail = req.user.email;
            const userPhone = (req.user as any).phone;
            
            // Helper to normalize phone for comparison (last 10 digits)
            const normalize = (p: string) => p ? String(p).replace(/[^0-9]/g, '').slice(-10) : '';
            const normalizedAppPhone = normalize(application.phone);
            const normalizedUserPhone = normalize(userPhone);

            const hasAccess = 
                (application.email && userEmail && application.email.toLowerCase() === userEmail.toLowerCase()) || 
                (normalizedAppPhone && normalizedUserPhone && normalizedAppPhone === normalizedUserPhone);
            
            if (!hasAccess) {
                return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' });
            }

            // FILTER: Prospect should ONLY see documents they uploaded
            const restrictedData = application.toJSON();
            if (restrictedData.uploadedDocuments) {
                restrictedData.uploadedDocuments = (restrictedData.uploadedDocuments as any[]).filter(
                    (doc: any) => doc.uploadedBy === req.user?.id || (doc.uploadedBy === null && doc.applicationId === application.id)
                );
            }
            return res.json({ success: true, data: restrictedData });
        }

        res.json({ success: true, data: application });
    } catch (error) {
        console.error('Get application error:', error);
        res.status(500).json({ success: false, message: 'Error fetching application' });
    }
};

export const updateApplicationStatus = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const targetId = id as string;
        const { status, stage, reason } = req.body;

        // Resolve application by ID (UUID) or Registeration Number (applicationId)
        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });

        if (!application) return res.status(404).json({ success: false, message: 'Application not found' });

        await WorkflowService.transitionApplication(application, status, req.user?.id || null, {
            reason: reason || 'Manual Status Update',
            stage: stage
        });

        res.json({ success: true, message: 'Application status updated successfully' });
    } catch (error) {
        console.error('Update application status error:', error);
        res.status(500).json({ success: false, message: 'Error updating application status' });
    }
};

export const uploadDocuments = async (req: any, res: Response) => {
    try {
        const { id } = req.params;
        const { documentType, stage } = req.body;
        const file = req.file;

        if (!file) {
            return res.status(400).json({ success: false, message: 'No file uploaded' });
        }

        if (!documentType) {
            return res.status(400).json({ success: false, message: 'Document type is required' });
        }

        const targetId = id as string;
        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });

        if (!application) {
            return res.status(404).json({ success: false, message: 'Application not found' });
        }

        // Create Document Record
        const newDoc = await db.OnboardingDocument.create({
            applicationId: application.id,
            documentType,
            stage: stage || null,
            fileName: file.originalname,
            filePath: file.path, 
            fileSize: file.size,
            mimeType: file.mimetype,
            uploadedBy: req.user?.roleCode === 'Prospective Dealer' ? null : req.user?.id,
            status: 'active'
        });

        // Update architecture document date if relevant
        if (['Architecture Blueprint', 'Site Plan'].includes(documentType)) {
            await application.update({ architectureDocumentDate: new Date() });
        }

        // Handle EOR Checklist Automatic Linking & Compliance
        const eorItems = [
            { itemType: 'Sales', description: 'Sales Standards' },
            { itemType: 'Service', description: 'Service & Spares' },
            { itemType: 'IT', description: 'DMS infra' },
            { itemType: 'Training', description: 'Manpower Training' },
            { itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' },
            { itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' },
            { itemType: 'Finance', description: 'Inventory Funding' },
            { itemType: 'IT', description: 'Virtual code availability' },
            { itemType: 'Finance', description: 'Vendor payments' },
            { itemType: 'Marketing', description: 'Details for website submission' },
            { itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' },
            { itemType: 'IT', description: 'Auto ordering' }
        ];

        const eorDescriptions = eorItems.map(i => i.description);

        if (eorDescriptions.includes(documentType)) {
            // Find or Create Checklist
            const [checklist, created] = await db.EorChecklist.findOrCreate({
                where: { applicationId: application.id },
                defaults: { status: 'In Progress' }
            });

            // If newly created or no items exist, seed them
            const existingItemCount = await db.EorChecklistItem.count({ where: { checklistId: checklist.id } });
            if (created || existingItemCount === 0) {
                const itemsData = eorItems.map(item => ({
                    ...item,
                    checklistId: checklist.id,
                    isCompliant: false
                }));
                await db.EorChecklistItem.bulkCreate(itemsData);
            }

            console.log(`[debug] EOR Checklist found/created: ${checklist.id} for Application: ${application.id}`);
            
            const [updatedCount] = await db.EorChecklistItem.update(
                { proofDocumentId: newDoc.id, isCompliant: false },
                { 
                    where: { 
                        checklistId: checklist.id, 
                        description: { [Op.iLike]: documentType.trim() }
                    } 
                }
            );

            console.log(`[debug] EOR items updated: ${updatedCount} for type: ${documentType}`);
        }

        const prospectiveUpload = req.user?.roleCode === 'Prospective Dealer' || !req.user?.id;
        await safeAuditLogCreate({
            userId: req.user?.id || null,
            action: AUDIT_ACTIONS.DOCUMENT_UPLOADED,
            entityType: 'application',
            entityId: application.id,
            newData: {
                documentId: newDoc.id,
                documentType,
                fileName: file.originalname,
                docStage: stage || null,
                prospectiveUpload,
                uploaderRole: req.user?.roleCode || 'Prospective (session)',
                eorChecklistLinked: eorDescriptions.includes(documentType),
                context: pickApplicationAuditContext(application),
            },
        });

        if (application.email) {
            const portalBase = getFrontendBaseUrl();
            await NotificationService.notify(null, application.email, {
                title: `Document received — ${application.applicationId}`,
                message: `We received your upload: ${documentType}.`,
                channels: ['email'],
                templateCode: 'DOCUMENT_RECEIVED_ACKNOWLEDGEMENT',
                placeholders: {
                    applicantName: application.applicantName || 'Applicant',
                    applicationId: application.applicationId,
                    documentCategory: stage || 'Onboarding',
                    documentName: documentType,
                    receivedOn: new Date().toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }),
                    link: getProspectPortalUrl(application.id),
                    ctaLabel: 'View uploads'
                }
            }).catch((mailErr: any) => console.error('[uploadDocuments] acknowledgement email:', mailErr));
        }

        res.status(201).json({
            success: true,
            message: 'Document uploaded successfully',
            data: newDoc
        });
    } catch (error) {
        console.error('Upload document error:', error);
        res.status(500).json({ success: false, message: 'Error uploading document' });
    }
};

export const getApplicationDocuments = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;

        const targetId = id as string;
        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);

        // Resolve ID to primary key if it's an appId string
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });

        if (!application) {
            return res.status(404).json({ success: false, message: 'Application not found' });
        }

        const whereClause: any = {
            applicationId: application.id,
            status: 'active'
        };

        // ENFORCE PRIVACY: Prospect should ONLY see documents they uploaded
        if (req.user?.roleCode === 'Prospective Dealer') {
            whereClause[Op.or] = [
                { uploadedBy: req.user?.id || null },
                { uploadedBy: null }
            ];
        }

        const documents = await db.OnboardingDocument.findAll({
            where: whereClause,
            include: [
                { model: db.User, as: 'uploader', attributes: ['fullName'] }
            ],
            order: [['createdAt', 'DESC']]
        });

        res.json({ success: true, data: documents });
    } catch (error) {
        console.error('Get documents error:', error);
        res.status(500).json({ success: false, message: 'Error fetching documents' });
    }
};

export const bulkShortlist = async (req: AuthRequest, res: Response) => {
    try {
        const { applicationIds, assignedTo, remarks } = req.body;

        if (!applicationIds || !Array.isArray(applicationIds) || applicationIds.length === 0) {
            return res.status(400).json({ success: false, message: 'No applications selected' });
        }

        // assignedTo is now optional as auto-assignment is handled via location
        const assignedToArr = Array.isArray(assignedTo) ? assignedTo : [];
        const primaryManualAssigneeId = assignedToArr.length > 0 ? assignedToArr[0] : null;

        // Update Applications sequentially via WorkflowService for consistency
        for (const appId of applicationIds) {
            const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(appId);
            const application = await Application.findOne({
                where: isUUID ? { [Op.or]: [{ id: appId }, { applicationId: appId }] } : { applicationId: appId }
            });
            
            if (application) {
                let resolvedAssigneeId = primaryManualAssigneeId;

                // If no manual assignee provided, auto-resolve from District mapping
                if (!resolvedAssigneeId && application.districtId) {
                    const district = await db.District.findByPk(application.districtId);
                    if (district) {
                        // Prioritize DD-AM as per user request
                        resolvedAssigneeId = district.ddAmId || district.zmId || null;
                    }
                }

                await application.update({
                    ddLeadShortlisted: true,
                    isShortlisted: true,
                    assignedTo: resolvedAssigneeId,
                    updatedAt: new Date(),
                });

                await WorkflowService.transitionApplication(application, APPLICATION_STATUS.SHORTLISTED, req.user?.id || null, {
                    reason: remarks || 'Bulk Shortlist',
                    progressPercentage: 30
                });

                // Send Shortlist Email
                sendShortlistedEmail(
                    application.email,
                    application.applicantName,
                    application.preferredLocation || application.city,
                    application.applicationId
                ).catch(err => console.error('Failed to send shortlist email:', err));

                const portalBase = getFrontendBaseUrl();
                const applicantUser = await User.findOne({
                    where: { email: application.email },
                    attributes: ['id', 'mobileNumber']
                });
                const prospectPhone = (applicantUser as any)?.mobileNumber || application.phone || '';
                await NotificationService.notify(applicantUser?.id ?? null, application.email, {
                    title: `Documents required — ${application.applicationId}`,
                    message: 'Please upload the requested documents to continue your dealership evaluation.',
                    channels: prospectPhone ? ['email', 'whatsapp'] : ['email'],
                    templateCode: 'PROSPECT_DOCUMENT_REQUEST',
                    placeholders: {
                        applicantName: application.applicantName || 'Applicant',
                        location: application.preferredLocation || application.city || 'your proposed location',
                        applicationId: application.applicationId,
                        dueDate: formatDueDateDaysFromNow(7),
                        documentList: DEFAULT_PROSPECT_DOCUMENT_CHECKLIST,
                        link: getProspectPortalUrl(application.id),
                        ctaLabel: 'Upload documents',
                        phone: String(prospectPhone || ''),
                        // Show the "Congratulations on being shortlisted" line only at the
                        // genuine shortlist moment. Later document-request triggers (the
                        // admin's "Request Documents" action) omit this flag so the email
                        // reads as a neutral follow-up.
                        isShortlistContext: true
                    }
                }).catch((err: any) => console.error('Failed to send prospect document request email:', err));

                // Add manual assignees as participants if provided
                if (assignedToArr.length > 0) {
                    for (const userId of assignedToArr) {
                        await db.RequestParticipant.findOrCreate({
                            where: { requestId: application.id, requestType: 'application', userId, participantType: 'assignee' },
                            defaults: { joinedMethod: 'auto' }
                        });
                    }
                }

                // TRIGGER AUTO-ASSIGNMENT for all stages based on location
                await assignStageEvaluators(application.id);
            }
        }

        res.json({
            success: true,
            message: `Successfully shortlisted ${applicationIds.length} application(s). Assignments processed automatically based on location.`
        });
    } catch (error) {
        console.error('Bulk shortlist error:', error);
        res.status(500).json({ success: false, message: 'Error processing shortlist' });
    }
};

/**
/**
 * Helper to assign default evaluators for all 3 interview levels based on location
 */
/**
 * Helper to assign default evaluators for all 3 interview levels, LOI, and LOA based on location
 */
const assignStageEvaluators = async (appIdOrId: string) => {
    try {
        if (!await isAutoAssignmentEnabled('ONBOARDING')) {
            console.log(`[debug] Auto-assignment disabled for ONBOARDING. Skipping for App: ${appIdOrId}`);
            return;
        }
        console.log(`[debug] Starting stage evaluator assignment for App: ${appIdOrId}`);
        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(appIdOrId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: appIdOrId }, { applicationId: appIdOrId }] } : { applicationId: appIdOrId },
            include: [
                {
                    model: District,
                    as: 'district',
                    include: [
                        { model: Region, as: 'region' },
                        { model: Zone, as: 'zone' }
                    ]
                }
            ]
        });

        if (!application) {
            console.log(`[debug] Application ${appIdOrId} not found`);
            return;
        }

        if (!application.district) {
            console.log(`[debug] Application ${appIdOrId} has NO district linked. Skipping auto-assign.`);
            return;
        }

        const district = application.district;
        const region = district.region;
        const zone = district.zone;

        console.log(`[debug] Mapping for District: ${district.name}, Region: ${region?.name}, Zone: ${zone?.name}`);

        const evaluatorMappings: any = {
            1: [], // Level 1 Interview: DD-ZM + RBM
            2: [], // Level 2 Interview: DD Lead + ZBH
            3: [], // Level 3 Interview: NBH + DD Head
            'LOI_APPROVAL': [], // LOI: Finance, DD Head, NBH
            'LOA_APPROVAL': []  // LOA: DD Head, NBH
        };

        // --- INTERVIEWS ---

        // --- GROUND STAKEHOLDERS (Area / District Level) ---
        // SRS §9.3.4: DD-AM is responsible for ground opportunity identification and field validation in onboarding.
        // Falls back to ASM if DD-AM is not assigned for this district.
        if (district.ddAmId) {
            evaluatorMappings['DD'] = [{ id: district.ddAmId, role: 'DD-AM' }];
        } else if (district.asmId) {
            evaluatorMappings['DD'] = [{ id: district.asmId, role: 'ASM' }];
        }

        // Level 1: DD-ZM (District manager) + RBM (Region manager)
        if (district.zmId) evaluatorMappings[1].push({ id: district.zmId, role: 'DD-ZM' });
        if (region && region.rbmId) evaluatorMappings[1].push({ id: region.rbmId, role: 'RBM' });

        // Level 2: ZBH (Zone manager) + DD Lead (National Singleton)
        if (zone && zone.zbhId) evaluatorMappings[2].push({ id: zone.zbhId, role: 'ZBH' });
        
        const ddLead = await db.User.findOne({
            where: { roleCode: 'DD Lead', status: 'active' },
            include: [{
                model: db.UserRole,
                as: 'userRoles',
                where: { isActive: true } // Removed zoneId filter
            }]
        });
        if (ddLead) evaluatorMappings[2].push({ id: ddLead.id, role: 'DD Lead' });

        // Level 3: NBH + DD Head (National Level Roles)
        const level3Roles = ['NBH', 'DD Head'];
        for (const roleCode of level3Roles) {
            const user = await db.User.findOne({ where: { roleCode, status: 'active' } });
            if (user) evaluatorMappings[3].push({ id: user.id, role: roleCode });
        }

        // --- LOI & LOA ---
        
        // National roles for LOI / LOA
        const nationalRoles = ['NBH', 'DD Head', 'Finance'];
        const nationalUsers: Record<string, string> = {};
        for (const r of nationalRoles) {
            const user = await db.User.findOne({ where: { roleCode: r, status: 'active' } });
            if (user) nationalUsers[r] = user.id;
        }

        // LOI: DD Head, NBH (Finance removed per user requirement)
        if (nationalUsers['DD Head']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['DD Head'], role: 'DD Head' });
        if (nationalUsers['NBH']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['NBH'], role: 'NBH' });

        // LOA: DD Head, NBH
        if (nationalUsers['DD Head']) evaluatorMappings['LOA_APPROVAL'].push({ id: nationalUsers['DD Head'], role: 'DD Head' });
        if (nationalUsers['NBH']) evaluatorMappings['LOA_APPROVAL'].push({ id: nationalUsers['NBH'], role: 'NBH' });

        // Persistence logic: Store in RequestParticipant with metadata
        // Persistence logic: Consolidate all stage/interview assignments into the same user record
        // to prevent duplication in the participants list.
        const userAssignments: Record<string, { stages: any[], roles: string[] }> = {};
        
        const allStages = ['DD', 1, 2, 3, 'LOI_APPROVAL', 'LOA_APPROVAL'];
        for (const stage of allStages) {
            const assignments = evaluatorMappings[stage];
            for (const assign of assignments) {
                const { id: userId, role } = assign;
                if (!userAssignments[userId]) {
                    userAssignments[userId] = { stages: [], roles: [] };
                }
                userAssignments[userId].stages.push(stage);
                userAssignments[userId].roles.push(role);
            }
        }

        for (const [userId, assignment] of Object.entries(userAssignments)) {
            const primaryStage = assignment.stages[0];
            const primaryRole = assignment.roles[0];
            
            const [participant, created] = await db.RequestParticipant.findOrCreate({
                where: {
                    requestId: application.id,
                    requestType: 'application',
                    userId: userId
                },
                defaults: {
                    participantType: 'contributor',
                    joinedMethod: 'auto',
                    metadata: {
                        interviewLevel: typeof primaryStage === 'number' ? primaryStage : null,
                        stageCode: typeof primaryStage === 'string' ? primaryStage : null,
                        role: primaryRole,
                        allAssignments: assignment.stages,
                        autoMapped: true
                    }
                }
            });

            if (!created) {
                await participant.update({
                    joinedMethod: 'auto', // Ensure it's marked as auto if it wasn't
                    metadata: {
                        ...(participant.metadata || {}),
                        interviewLevel: typeof primaryStage === 'number' ? primaryStage : null,
                        stageCode: typeof primaryStage === 'string' ? primaryStage : null,
                        role: primaryRole,
                        allAssignments: assignment.stages,
                        autoMapped: true
                    }
                });
            }
        }

        // --- Cleanup stale auto-assignments ---
        const currentParticipantIds = Object.keys(userAssignments);
        await db.RequestParticipant.destroy({
            where: {
                requestId: application.id,
                requestType: 'application',
                joinedMethod: 'auto',
                userId: { [db.Sequelize.Op.notIn]: currentParticipantIds }
            }
        });
    } catch (error) {
        console.error(`Error assigning stage evaluators for application ${appIdOrId}:`, error);
    }
};


export const retriggerEvaluators = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const targetId = id as string;
        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });
        if (!application) return res.status(404).json({ success: false, message: 'Application not found' });

        // Remove existing auto-mapped participants (Interviews, LOI, LOA)
        // Using a more robust Postgres-compatible JSON path check
        await db.RequestParticipant.destroy({
            where: {
                requestId: application.id,
                requestType: 'application',
                joinedMethod: 'auto',
                [Op.and]: [
                    db.sequelize.literal(`"metadata"->>'autoMapped' = 'true'`)
                ]
            }
        });

        // Sync district data before re-assignment to ensure fresh manager mapping
        if (application.districtId) {
            await syncLocationManagers(application.districtId);
        }

        await assignStageEvaluators(id as string);

        res.json({ success: true, message: 'All stage evaluators (Interviews, LOI, LOA) have been re-assigned successfully.' });
    } catch (error) {
        console.error('Retrigger evaluators error:', error);
        res.status(500).json({ success: false, message: 'Error re-triggering evaluator assignment' });
    }
};

export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const targetId = id as string;
        const { userId, assignedTo, remarks } = req.body;
        const targetUserId = userId || assignedTo;

        if (!targetUserId) {
            return res.status(400).json({ success: false, message: 'Architecture team member (userId) is required' });
        }

        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });
        if (!application) return res.status(404).json({ success: false, message: 'Application not found' });

        await application.update({
            architectureAssignedTo: targetUserId,
            architectureStatus: 'IN_PROGRESS',
            architectureAssignedDate: new Date(),
            updatedAt: new Date()
        });

        await WorkflowService.transitionApplication(application, APPLICATION_STATUS.ARCHITECTURE_TEAM_ASSIGNED, req.user?.id || null, {
            reason: remarks || 'Architecture team assigned'
        });

        // Add as participant
        await db.RequestParticipant.findOrCreate({
            where: {
                requestId: application.id,
                requestType: 'application',
                userId: targetUserId,
                participantType: 'architecture'
            },
            defaults: { joinedMethod: 'auto' }
        });

        const architect = await User.findByPk(targetUserId, { attributes: ['fullName'] });
        const portalBaseArch = getFrontendBaseUrl();
        if (application.email) {
            await NotificationService.notify(null, application.email, {
                title: `Architecture & site inputs — ${application.applicationId}`,
                message: 'Please share site inputs for your dealership layout.',
                channels: ['email'],
                templateCode: 'ARCHITECTURAL_PLAN_REQUEST',
                placeholders: {
                    applicantName: application.applicantName || 'Applicant',
                    applicationId: application.applicationId,
                    architectName: architect?.fullName || 'Royal Enfield Architecture',
                    dueDate: formatDueDateDaysFromNow(10),
                    inputsList: DEFAULT_ARCHITECTURE_SITE_INPUTS,
                    link: getProspectPortalUrl(application.id),
                    ctaLabel: 'Provide site inputs'
                }
            }).catch((e: any) => console.error('[Architecture] Applicant email failed:', e));
        }

        res.json({ success: true, message: 'Architecture team assigned successfully' });
    } catch (error) {
        console.error('Assign architecture team error:', error);
        res.status(500).json({ success: false, message: 'Error assigning architecture team' });
    }
};

export const updateArchitectureStatus = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const targetId = id as string;
        const { status, remarks } = req.body;

        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });
        if (!application) return res.status(404).json({ success: false, message: 'Application not found' });

        const updateData: any = {
            architectureStatus: status,
            updatedAt: new Date()
        };

        // Sync overall status if architecture is completed
        const targetOverallStatus = status === 'COMPLETED' 
            ? APPLICATION_STATUS.ARCHITECTURE_TEAM_COMPLETION 
            : APPLICATION_STATUS.ARCHITECTURE_TEAM_ASSIGNED;

        await application.update({
            architectureStatus: status,
            architectureCompletionDate: status === 'COMPLETED' ? new Date() : application.architectureCompletionDate,
            updatedAt: new Date()
        });

        await WorkflowService.transitionApplication(application, targetOverallStatus, req.user?.id || null, {
            reason: remarks || `Architecture status updated to ${status}`
        });

        res.json({ success: true, message: 'Architecture status updated successfully' });
    } catch (error) {
        console.error('Update architecture status error:', error);
        res.status(500).json({ success: false, message: 'Error updating architecture status' });
    }
};

import { ExternalMocksService } from '../../common/utils/externalMocks.service.js';
import { getFrontendBaseUrl, getProspectPortalUrl } from '../../common/utils/frontendUrl.js';

export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params; // applicationId
        const targetId = id as string;

        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });
        if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
        
        // Strict Workflow Validation: Dealer Code Generation requires LOI Issued status
        if (application.overallStatus !== APPLICATION_STATUS.LOI_ISSUED && application.overallStatus !== APPLICATION_STATUS.DEALER_CODE_GENERATION) {
            return res.status(400).json({
                success: false,
                message: `Cannot generate dealer codes. The application must be in 'LOI Issued' status (Current: ${application.overallStatus}).`
            });
        }
        
        // Validation: Check for mandatory fields before triggering Dealer Code Generation
        const mandatoryFields = [
            { key: 'panNumber', label: 'PAN Number' },
            { key: 'gstNumber', label: 'GST Number' },
            { key: 'bankName', label: 'Bank Name' },
            { key: 'accountNumber', label: 'Account Number' },
            { key: 'ifscCode', label: 'IFSC Code' }
        ];

        const missingFields = mandatoryFields
            .filter(f => !application[f.key as keyof typeof application])
            .map(f => f.label);

        if (missingFields.length > 0) {
            return res.status(400).json({ 
                success: false, 
                message: `Cannot generate dealer codes. Missing mandatory fields: ${missingFields.join(', ')}. Please update application details first.`
            });
        }

        // Trigger Mock SAP Integration
        const { data: sapData } = await ExternalMocksService.mockGenerateSapCodes(application.applicationId);

        // Save Dealer Codes
        await db.DealerCode.create({
            dealerCode: sapData.salesCode, // Use sales code as primary dealer code
            applicationId: application.id,
            salesCode: sapData.salesCode,
            serviceCode: sapData.serviceCode,
            gmaCode: sapData.gmaCode,
            gearCode: sapData.gearCode,
            sapMasterId: sapData.sapMasterId,
            status: 'Active',
            generatedBy: req.user?.id
        });
        
        // Create Final Security Deposit record (Blocker for LOA)
        await db.SecurityDeposit.findOrCreate({
            where: { applicationId: application.id, depositType: 'FIRST_FILL' },
            defaults: {
                amount: 1500000, // 15 Lakhs Final
                status: 'Pending'
            }
        });

        await WorkflowService.transitionApplication(application, APPLICATION_STATUS.DEALER_CODE_GENERATION, req.user?.id || null, {
            reason: 'SAP Dealer Codes Generated',
            progressPercentage: 80
        });

        res.json({
            success: true,
            message: 'SAP Dealer Codes generated successfully (Mock)',
            data: sapData
        });
    } catch (error) {
        console.error('Generate dealer code error:', error);
        res.status(500).json({ success: false, message: 'Error generating dealer codes' });
    }
};
// Fetch Metadata for Document Management (Modules & Stages)
export const getDocumentConfigMetadata = async (_req: AuthRequest, res: Response) => {
    try {
        const { MODULE_LIST, STAGES_MAP } = await import('../../common/config/constants.js');
        res.json({
            success: true,
            data: {
                modules: MODULE_LIST,
                stages: STAGES_MAP
            }
        });
    } catch (error) {
        res.status(500).json({ success: false, message: 'Error fetching metadata' });
    }
};

// Fetch Document Configurations based on Role and Stage
export const getDocumentConfigs = async (req: AuthRequest, res: Response) => {
    try {        const { stageCode, search, page = 1, limit = 10, roleFilter, module = 'ONBOARDING' } = req.query;
        const roleCode = (roleFilter as string) || req.user?.role;
        
        const where: any = { module };
        if (stageCode) {
            where.stageCode = { [Op.or]: [stageCode, 'General'] };
        }


        if (search) {
            where[Op.or] = [
                { documentType: { [Op.iLike]: `%${search}%` } },
                { stageCode: { [Op.iLike]: `%${search}%` } }
            ];
        }

        const offset = (Number(page) - 1) * Number(limit);
        
        const { rows: configs, count } = await DocumentStageConfig.findAndCountAll({
            where,
            order: [['stageCode', 'DESC'], ['documentType', 'ASC']],
            limit: Number(limit),
            offset: Number(offset)
        });
        
        // Manual role filtering because it's a JSON field
        // Note: For admin search, we might want to skip this
        let filteredConfigs = configs;
        if (roleCode && !req.query.isAdminView) {
            filteredConfigs = configs.filter((c: any) => {
                const allowedRoles = c.allowedRoles || [];
                return allowedRoles.length === 0 || allowedRoles.includes(roleCode);
            });
        }
        
        return res.json({ 
            success: true, 
            data: filteredConfigs,
            pagination: {
                total: count,
                page: Number(page),
                limit: Number(limit),
                pages: Math.ceil(count / Number(limit))
            }
        });
    } catch (error) {
        console.error('Failed to fetch document configs:', error);
        return res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

export const createDocumentConfig = async (req: AuthRequest, res: Response) => {
    try {
        const config = await DocumentStageConfig.create(req.body);
        return res.json({ success: true, data: config });
    } catch (error) {
        console.error('Failed to create document config:', error);
        return res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

export const updateDocumentConfig = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const config = await DocumentStageConfig.findByPk(id);
        if (!config) return res.status(404).json({ success: false, message: 'Config not found' });
        
        await config.update(req.body);
        return res.json({ success: true, data: config });
    } catch (error) {
        console.error('Failed to update document config:', error);
        return res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

export const deleteDocumentConfig = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const config = await DocumentStageConfig.findByPk(id);
        if (!config) return res.status(404).json({ success: false, message: 'Config not found' });
        
        await config.destroy();
        return res.json({ success: true, message: 'Deleted successfully' });
    } catch (error) {
        console.error('Failed to delete document config:', error);
        return res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

export const updateApplication = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const targetId = id as string;

        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });
        if (!application) return res.status(404).json({ success: false, message: 'Application not found' });

        await application.update(req.body);
        return res.json({ success: true, message: 'Application updated successfully', data: application });
    } catch (error) {
        console.error('Failed to update application:', error);
        return res.status(500).json({ success: false, message: 'Internal server error' });
    }
};
export const exportApplicationResponses = async (req: AuthRequest, res: Response) => {
    try {
        const { applicationIds } = req.query;
        if (!applicationIds) return res.status(400).json({ success: false, message: 'No applications selected' });

        const ids = (applicationIds as string).split(',');

        // 1. Fetch Applications with their identification details
        const applications = await Application.findAll({
            where: { 
                id: { [Op.in]: ids },
                overallStatus: { [Op.ne]: APPLICATION_STATUS.QUESTIONNAIRE_PENDING }
            },
            attributes: ['id', 'applicationId', 'applicantName', 'preferredLocation', 'email', 'phone']
        });

        if (applications.length === 0 && ids.length > 0) {
            return res.status(400).json({ 
                success: false, 
                message: 'No applications found with completed questionnaires among the selected records.' 
            });
        }

        // 2. Fetch all unique questions involved in these applications
        // For consistency, let's get all questions from the active questionnaire
        const activeQuestionnaire = await Questionnaire.findOne({ where: { isActive: true }, order: [['version', 'DESC']] });
        if (!activeQuestionnaire) return res.status(404).json({ success: false, message: 'Active questionnaire not found' });

        const allQuestions = await QuestionnaireQuestion.findAll({
            where: { questionnaireId: activeQuestionnaire.id },
            order: [['order', 'ASC']]
        });

        // 3. Fetch all responses for these applications
        const responses = await QuestionnaireResponse.findAll({
            where: { applicationId: { [Op.in]: ids } },
            attributes: ['applicationId', 'questionId', 'responseValue']
        });

        // 4. Map responses for quick lookup: applicationid -> questionid -> responseValue
        const responsesMap: any = {};
        responses.forEach((r: any) => {
            if (!responsesMap[r.applicationId]) responsesMap[r.applicationId] = {};
            responsesMap[r.applicationId][r.questionId] = r.responseValue;
        });

        // 5. Construct rows
        const rows = applications.map((app: any) => {
            const data: any = {
                'Application ID': app.applicationId,
                'Applicant Name': app.applicantName,
                'Email': app.email,
                'Phone': app.phone,
                'Preferred Location': app.preferredLocation
            };

            allQuestions.forEach((q: any) => {
                data[q.questionText] = responsesMap[app.id]?.[q.id] || 'Not Answered';
            });

            return data;
        });

        res.json({ success: true, data: rows });
    } catch (error: any) {
        console.error('Export error:', error);
        res.status(500).json({ success: false, message: 'Error exporting data' });
    }
};

/**
 * Handle the scenario when an applicant has applied for a location where 
 * the opportunity was unavailable (Non-Opportunity Application) but now 
 * an opportunity has opened up.
 */
export const convertToOpportunity = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const { opportunityId, remarks } = req.body;

        // 1. Resolve Application
        const application = await Application.findByPk(id);
        if (!application) {
            return res.status(404).json({ success: false, message: 'Application not found' });
        }

        // 2. Guardians: Ensure it's not already an opportunity application
        if (application.opportunityId) {
            return res.status(400).json({ 
                success: false, 
                message: 'Application is already linked to an opportunity.' 
            });
        }

        let targetOpportunityId = opportunityId;

        // 3. Auto-discovery fallback: If no ID provided, look for active opportunity in same district
        if (!targetOpportunityId && application.districtId) {
            const activeOpp = await Opportunity.findOne({
                where: {
                    districtId: application.districtId,
                    status: 'active',
                    [Op.or]: [
                        { openTo: null },
                        { openTo: { [Op.gte]: new Date() } }
                    ]
                }
            });
            if (activeOpp) {
                targetOpportunityId = activeOpp.id;
            }
        }

        if (!targetOpportunityId) {
            return res.status(400).json({ 
                success: false, 
                message: 'No active opportunity found for this location. Please create an opportunity for this district first.' 
            });
        }

        // 4. Update Application Record
        await application.update({
            opportunityId: targetOpportunityId,
            isShortlisted: true,
            updatedAt: new Date()
        });

        // 5. Trigger Workflow Transition (Moves to Questionnaire Pending)
        // This handles status history, audit logs, and progress percentage sync (10%)
        await WorkflowService.transitionApplication(
            application, 
            APPLICATION_STATUS.QUESTIONNAIRE_PENDING, 
            req.user?.id || null, 
            {
                reason: 'Application processing initiated.',
                source: 'Lead Conversion',
                skipNotification: true
            }
        );

        // 6. Trigger Opportunity Email (Contains portal link + questionnaire invitation)
        const displayApplicantName = toTitleCase(application.applicantName);
        const displayLocation = toTitleCase(application.city || application.preferredLocation || 'Proposed Location');

        sendOpportunityEmail(application.email, displayApplicantName, displayLocation, application.applicationId)
            .then(() => {
                db.AuditLog.create({
                    userId: req.user?.id || null,
                    action: 'EMAIL_SENT',
                    entityType: 'application',
                    entityId: application.id,
                    newData: { 
                        email: application.email, 
                        type: 'OPPORTUNITY_CONVERSION', 
                        status: 'SUCCESS'
                    }
                }).catch((err: any) => console.error('[onboarding] Conversion email audit failed:', err));
            })
            .catch((err: any) => console.error('[onboarding] Failed to send conversion email:', err));

        // 7. Log specialized Audit record
        await AuditLog.create({
            userId: req.user?.id,
            action: AUDIT_ACTIONS.UPDATED,
            entityType: 'application',
            entityId: application.id,
            newData: { 
                action: 'CONVERT_TO_OPPORTUNITY',
                opportunityId: targetOpportunityId,
                remarks: remarks || 'N/A'
            }
        });

        res.json({
            success: true,
            message: 'Application successfully converted. Questionnaire link sent to applicant.',
            data: application
        });

    } catch (error) {
        console.error('Convert to opportunity error:', error);
        res.status(500).json({ success: false, message: 'Error converting application' });
    }
};

/**
 * Bulk conversion of lead-generation applications to active opportunities.
 */
export const bulkConvertToOpportunity = async (req: AuthRequest, res: Response) => {
    try {
        const { ids, remarks } = req.body;

        if (!ids || !Array.isArray(ids) || ids.length === 0) {
            return res.status(400).json({ success: false, message: 'No application IDs provided' });
        }

        const results = {
            success: 0,
            failed: 0,
            errors: [] as string[]
        };

        for (const id of ids) {
            try {
                const application = await Application.findByPk(id);
                if (!application) {
                    results.failed++;
                    results.errors.push(`Application ${id} not found.`);
                    continue;
                }

                if (application.opportunityId) {
                    results.failed++;
                    results.errors.push(`Application ${application.applicationId} is already linked to an opportunity.`);
                    continue;
                }

                let targetOpportunityId = null;

                // Auto-discovery logic (matches the single conversion logic)
                if (application.districtId) {
                    const activeOpp = await Opportunity.findOne({
                        where: {
                            districtId: application.districtId,
                            status: 'active',
                            [Op.or]: [
                                { openTo: null },
                                { openTo: { [Op.gte]: new Date() } }
                            ]
                        }
                    });
                    if (activeOpp) {
                        targetOpportunityId = activeOpp.id;
                    }
                }

                if (!targetOpportunityId) {
                    results.failed++;
                    results.errors.push(`No active opportunity found for ${application.applicantName} (${application.preferredLocation}).`);
                    continue;
                }

                // Update Application
                await application.update({
                    opportunityId: targetOpportunityId,
                    isShortlisted: true,
                    updatedAt: new Date()
                });

                // Transition Workflow
                await WorkflowService.transitionApplication(
                    application, 
                    APPLICATION_STATUS.QUESTIONNAIRE_PENDING, 
                    req.user?.id || null, 
                    {
                        reason: 'Application processing initiated.',
                        source: 'Lead Conversion',
                        skipNotification: true
                    }
                );

                // Send Email Notification
                const displayApplicantName = toTitleCase(application.applicantName);
                const displayLocation = toTitleCase(application.city || application.preferredLocation || 'Proposed Location');

                sendOpportunityEmail(application.email, displayApplicantName, displayLocation, application.applicationId)
                    .catch((err: any) => console.error(`[bulk-convert] Mail failed for ${id}:`, err));

                results.success++;
            } catch (err: any) {
                results.failed++;
                results.errors.push(`Error converting ID ${id}: ${err.message}`);
            }
        }

        res.json({
            success: true,
            message: `Batch processed ${ids.length} records. ${results.success} succeeded, ${results.failed} failed.`,
            data: results
        });

    } catch (error) {
        console.error('Bulk convert error:', error);
        res.status(500).json({ success: false, message: 'Internal error during batch conversion' });
    }
};

export const sendBulkReminders = async (req: AuthRequest, res: Response) => {
    try {
        const { applicationIds } = req.body;
        if (!applicationIds || !Array.isArray(applicationIds)) {
            return res.status(400).json({ success: false, message: 'Invalid application IDs' });
        }

        const applications = await Application.findAll({
            where: { id: { [Op.in]: applicationIds } }
        });

        const { QuestionnaireReminderService } = await import('../../services/QuestionnaireReminderService.js');
        for (const app of applications) {
            await QuestionnaireReminderService.sendReminderForApplication(app, 'manual');
        }

        res.json({ success: true, message: `Reminders sent to ${applications.length} applicants` });
    } catch (error) {
        console.error('Send bulk reminders error:', error);
        res.status(500).json({ success: false, message: 'Error sending reminders' });
    }
};

export const sendBulkDocumentReminders = async (req: AuthRequest, res: Response) => {
    try {
        const { applicationIds, pendingDocuments, dueDate } = req.body;
        if (!applicationIds || !Array.isArray(applicationIds) || applicationIds.length === 0) {
            return res.status(400).json({ success: false, message: 'applicationIds array is required' });
        }

        const applications = await Application.findAll({
            where: { id: { [Op.in]: applicationIds } }
        });

        for (const app of applications) {
            await NotificationService.sendDocumentSubmissionReminder(app.email, app.phone, app.applicantName, {
                applicationId: app.applicationId,
                pendingDocuments: pendingDocuments || undefined,
                dueDate: dueDate || formatDueDateDaysFromNow(7),
                link: getProspectPortalUrl(app.id)
            });
            await safeAuditLogCreate({
                userId: req.user?.id || null,
                action: 'REMINDER_SENT',
                entityType: 'application',
                entityId: app.id,
                newData: {
                    template: 'DOCUMENT_SUBMISSION_REMINDER',
                    sentAt: new Date(),
                    context: pickApplicationAuditContext(app)
                }
            });
        }

        res.json({ success: true, message: `Document reminders sent to ${applications.length} applicant(s)` });
    } catch (error) {
        console.error('Send bulk document reminders error:', error);
        res.status(500).json({ success: false, message: 'Error sending document reminders' });
    }
};

export const sendBulkLoiAckReminders = async (req: AuthRequest, res: Response) => {
    try {
        const { applicationIds, dueDate } = req.body;
        if (!applicationIds || !Array.isArray(applicationIds) || applicationIds.length === 0) {
            return res.status(400).json({ success: false, message: 'applicationIds array is required' });
        }

        const applications = await Application.findAll({
            where: { id: { [Op.in]: applicationIds } }
        });

        let sent = 0;
        for (const app of applications) {
            const lr = await db.LoiRequest.findOne({
                where: { applicationId: app.id, status: 'Approved' }
            });
            if (!lr) continue;

            const latestGen = await db.LoiDocumentGenerated.findOne({
                where: { requestId: lr.id },
                order: [['generatedAt', 'DESC']]
            });
            if (!latestGen) continue;

            const ackCount = await db.LoiAcknowledgement.count({ where: { loiDocId: latestGen.id } });
            if (ackCount > 0) continue;

            await NotificationService.sendLoiAcknowledgementReminder(app.email, app.phone, app.applicantName, {
                applicationId: app.applicationId,
                dueDate: dueDate || formatDueDateDaysFromNow(7),
                link: getProspectPortalUrl(application.id)
            });
            sent++;
        }

        res.json({ success: true, message: `LOI acknowledgement reminders sent: ${sent} of ${applications.length} selected` });
    } catch (error) {
        console.error('Send bulk LOI ack reminders error:', error);
        res.status(500).json({ success: false, message: 'Error sending LOI acknowledgement reminders' });
    }
};

export const rejectOnboardingDocument = async (req: AuthRequest, res: Response) => {
    try {
        const allowed: string[] = [ROLES.DD_ADMIN, ROLES.DD_LEAD, ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN];
        if (!req.user?.roleCode || !allowed.includes(req.user.roleCode)) {
            return res.status(403).json({ success: false, message: 'Forbidden: insufficient role to reject documents' });
        }

        const { id, documentId } = req.params;
        const { rejectionReason, dueDate } = req.body as { rejectionReason?: string; dueDate?: string };
        if (!rejectionReason || String(rejectionReason).trim().length === 0) {
            return res.status(400).json({ success: false, message: 'rejectionReason is required' });
        }

        const targetId = id as string;
        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });
        if (!application) {
            return res.status(404).json({ success: false, message: 'Application not found' });
        }

        const doc = await OnboardingDocument.findOne({
            where: { id: documentId, applicationId: application.id }
        });
        if (!doc) {
            return res.status(404).json({ success: false, message: 'Document not found' });
        }

        await doc.update({ status: 'rejected' });

        if (application.email) {
            const portalBase = getFrontendBaseUrl();
            await NotificationService.notify(null, application.email, {
                title: `Document re-upload required — ${application.applicationId}`,
                message: `A document was not accepted: ${doc.documentType}.`,
                channels: application.phone ? ['email', 'whatsapp'] : ['email'],
                templateCode: 'DOCUMENT_REJECTED_RESUBMIT',
                placeholders: {
                    applicantName: application.applicantName || 'Applicant',
                    applicationId: application.applicationId,
                    documentName: doc.documentType,
                    rejectionReason: String(rejectionReason).trim(),
                    dueDate: dueDate || formatDueDateDaysFromNow(7),
                    link: getProspectPortalUrl(application.id),
                    ctaLabel: 'Re-upload document',
                    phone: application.phone || ''
                }
            }).catch((e: any) => console.error('[rejectOnboardingDocument] email failed:', e));
        }

        await safeAuditLogCreate({
            userId: req.user?.id || null,
            action: AUDIT_ACTIONS.DOCUMENT_REJECTED,
            entityType: 'application',
            entityId: application.id,
            newData: {
                documentId: doc.id,
                documentRejected: true,
                reason: rejectionReason,
                context: pickApplicationAuditContext(application)
            }
        });

        res.json({ success: true, message: 'Document marked as rejected and applicant notified' });
    } catch (error) {
        console.error('Reject onboarding document error:', error);
        res.status(500).json({ success: false, message: 'Error rejecting document' });
    }
};

/**
 * Request documents from prospect — gated to post-LOI-approval stages and admin/leadership roles.
 *
 * The admin picks one or more `documentTypes` (typically from the master DocumentStageConfig list).
 * The endpoint:
 *  1. Skips any types already uploaded for this application.
 *  2. Groups the remaining types by category (LOI / Statutory / Architecture / FDD / Other)
 *     based on each DocumentStageConfig's `stageCode`.
 *  3. Sends ONE email per non-empty category, reusing the existing applicant-facing templates
 *     (PROSPECT_DOCUMENT_REQUEST, STATUTORY_DOCUMENT_REQUEST, ARCHITECTURAL_PLAN_REQUEST,
 *     FDD_DOCUMENT_REQUEST). Each email carries only the items in that category.
 *  4. Logs an audit entry summarising what was requested, skipped, and per-category email status.
 *
 * Body: { documentTypes: string[]; dueDays?: number; customMessage?: string }
 */
export const requestProspectDocuments = async (req: AuthRequest, res: Response) => {
    try {
        const allowed: string[] = [ROLES.DD_ADMIN, ROLES.SUPER_ADMIN, ROLES.DD_LEAD, ROLES.DD_HEAD];
        if (!req.user?.roleCode || !allowed.includes(req.user.roleCode)) {
            return res.status(403).json({
                success: false,
                message: 'Forbidden: only DD Admin, DD Lead, DD Head, or Super Admin can request documents'
            });
        }

        const { id } = req.params;
        const { documentTypes, dueDays, customMessage } = req.body as {
            documentTypes?: string[];
            dueDays?: number;
            customMessage?: string;
        };

        if (!Array.isArray(documentTypes) || documentTypes.length === 0) {
            return res.status(400).json({ success: false, message: 'documentTypes (non-empty array) is required' });
        }

        const targetId = id as string;
        const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
        const application = await Application.findOne({
            where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
        });
        if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
        if (!application.email) {
            return res.status(400).json({ success: false, message: 'Applicant email not on file; cannot send document request' });
        }

        // Stage gate: only after LOI Approval (Security Deposit corridor onward).
        const postLoiStatuses = [
            APPLICATION_STATUS.SECURITY_DETAILS, // 'Security Deposit' canonical
            'Security Details',                  // legacy label still possible on older rows
            APPLICATION_STATUS.PAYMENT_PENDING,
            APPLICATION_STATUS.LOI_ISSUANCE_PENDING,
            APPLICATION_STATUS.LOI_ISSUED,
            APPLICATION_STATUS.DEALER_CODE_GENERATION,
            APPLICATION_STATUS.ARCHITECTURE_TEAM_ASSIGNED,
            APPLICATION_STATUS.ARCHITECTURE_DOCUMENT_UPLOAD,
            APPLICATION_STATUS.ARCHITECTURE_TEAM_COMPLETION,
            APPLICATION_STATUS.STATUTORY_GST,
            APPLICATION_STATUS.STATUTORY_PAN,
            APPLICATION_STATUS.STATUTORY_NODAL,
            APPLICATION_STATUS.STATUTORY_CHECK,
            APPLICATION_STATUS.STATUTORY_PARTNERSHIP,
            APPLICATION_STATUS.STATUTORY_FIRM_REG,
            APPLICATION_STATUS.STATUTORY_VIRTUAL_CODE,
            APPLICATION_STATUS.STATUTORY_DOMAIN,
            APPLICATION_STATUS.STATUTORY_MSD,
            APPLICATION_STATUS.STATUTORY_LOI_ACK,
            APPLICATION_STATUS.LOA_PENDING,
        ];
        if (!postLoiStatuses.includes(application.overallStatus as any)) {
            return res.status(400).json({
                success: false,
                message: `Document requests are only available after LOI Approval. Current status: ${application.overallStatus}.`
            });
        }

        // Drop types already uploaded — no point re-requesting them.
        const uploadedDocs = await OnboardingDocument.findAll({
            where: { applicationId: application.id, documentType: { [Op.in]: documentTypes } },
            attributes: ['documentType']
        });
        const uploadedTypes = new Set<string>(uploadedDocs.map((d: any) => d.documentType));
        const pendingTypes = documentTypes.filter(t => !uploadedTypes.has(t));
        if (pendingTypes.length === 0) {
            return res.status(400).json({
                success: false,
                message: 'All selected documents are already uploaded. Nothing to request.',
                data: { skippedAlreadyUploaded: Array.from(uploadedTypes) }
            });
        }

        // Categorize by DocumentStageConfig.stageCode so we can pick the right template per group.
        const configs = await DocumentStageConfig.findAll({
            where: { documentType: { [Op.in]: pendingTypes }, module: 'ONBOARDING' }
        });
        const configByType = new Map<string, any>();
        for (const c of configs) configByType.set((c as any).documentType, c);

        const categorize = (stageCode: string | null | undefined): 'LOI' | 'Statutory' | 'Architecture' | 'FDD' | 'Other' => {
            const s = String(stageCode || '').toLowerCase();
            if (s.startsWith('loi')) return 'LOI';
            if (s.startsWith('statutory')) return 'Statutory';
            if (s.startsWith('architecture')) return 'Architecture';
            if (s.startsWith('fdd')) return 'FDD';
            return 'Other';
        };

        const groups: Record<'LOI' | 'Statutory' | 'Architecture' | 'FDD' | 'Other', string[]> = {
            LOI: [], Statutory: [], Architecture: [], FDD: [], Other: []
        };
        for (const docType of pendingTypes) {
            const cfg = configByType.get(docType);
            groups[categorize(cfg?.stageCode)].push(docType);
        }

        const portalBase = getFrontendBaseUrl();
        const applicantAcct = await User.findOne({
            where: { email: application.email },
            attributes: ['id', 'mobileNumber']
        });
        const phone = (applicantAcct as any)?.mobileNumber || application.phone || '';
        // Send prospect to their own portal — internal `/applications/:id` is admin-only.
        const link = getProspectPortalUrl(application.id);
        const dueDaysResolved = typeof dueDays === 'number' && dueDays > 0 ? dueDays : 14;
        const formatList = (list: string[]) => list.map((d, i) => `${i + 1}. ${d}`).join('\n');

        const sendBuckets: Array<{
            category: 'LOI' | 'Statutory' | 'Architecture' | 'FDD' | 'Other';
            template: string;
            listKey: string;
            extraPlaceholders?: Record<string, string>;
        }> = [
            { category: 'LOI', template: 'PROSPECT_DOCUMENT_REQUEST', listKey: 'documentList' },
            { category: 'Statutory', template: 'STATUTORY_DOCUMENT_REQUEST', listKey: 'statutoryList' },
            { category: 'Architecture', template: 'ARCHITECTURAL_PLAN_REQUEST', listKey: 'inputsList',
              extraPlaceholders: { architectName: 'Royal Enfield Architecture Team' } },
            { category: 'FDD', template: 'FDD_DOCUMENT_REQUEST', listKey: 'documentChecklist',
              extraPlaceholders: { fddPartnerName: 'Assigned FDD Partner' } },
            // Generic fallback for un-categorised types — reuses the same shortlist-era template
            // since it already supports a `documentList` checklist field.
            { category: 'Other', template: 'PROSPECT_DOCUMENT_REQUEST', listKey: 'documentList' },
        ];

        const emailResults: Array<{ category: string; status: 'sent' | 'failed'; docCount: number; items: string[]; error?: string }> = [];

        for (const bucket of sendBuckets) {
            const items = groups[bucket.category];
            if (items.length === 0) continue;
            try {
                await NotificationService.notify(applicantAcct?.id ?? null, application.email, {
                    title: `Documents required — ${application.applicationId}`,
                    message: customMessage || `Please upload the requested ${bucket.category} documents on the Dealer Portal.`,
                    channels: phone ? ['email', 'whatsapp'] : ['email'],
                    templateCode: bucket.template,
                    placeholders: {
                        applicantName: application.applicantName || 'Applicant',
                        applicationId: application.applicationId,
                        location: application.preferredLocation || application.city || '',
                        dueDate: formatDueDateDaysFromNow(dueDaysResolved),
                        [bucket.listKey]: formatList(items),
                        link,
                        ctaLabel: 'Upload documents',
                        phone: phone || '',
                        ...(bucket.extraPlaceholders || {})
                    }
                });
                emailResults.push({ category: bucket.category, status: 'sent', docCount: items.length, items });
            } catch (e: any) {
                console.error(`[requestProspectDocuments] ${bucket.category} email failed:`, e);
                emailResults.push({
                    category: bucket.category,
                    status: 'failed',
                    docCount: items.length,
                    items,
                    error: e?.message
                });
            }
        }

        await safeAuditLogCreate({
            userId: req.user?.id || null,
            action: 'DOCUMENT_REQUEST_SENT',
            entityType: 'application',
            entityId: application.id,
            newData: {
                requestedDocumentTypes: pendingTypes,
                skippedAlreadyUploaded: Array.from(uploadedTypes),
                emailResults,
                requestedByRole: req.user?.roleCode,
                dueDays: dueDaysResolved,
                customMessage: customMessage || null,
                context: pickApplicationAuditContext(application)
            }
        });

        return res.json({
            success: true,
            message: 'Document request emails dispatched',
            data: {
                requested: pendingTypes,
                skippedAlreadyUploaded: Array.from(uploadedTypes),
                emailsSent: emailResults
            }
        });
    } catch (error) {
        console.error('Request prospect documents error:', error);
        return res.status(500).json({ success: false, message: 'Error requesting prospect documents' });
    }
};
