import { Response } from 'express';
import db from '../../database/models/index.js';
const { 
    Worknote, User, WorkNoteTag, WorkNoteAttachment, DocumentVersion, RequestParticipant, Application, AuditLog, 
    OnboardingDocument, RelocationDocument, ResignationDocument, ConstitutionalDocument, TerminationDocument 
} = db;
import { AuthRequest } from '../../types/express.types.js';
import { AUDIT_ACTIONS, REQUEST_TYPES, ROLES } from '../../common/config/constants.js';
import {
    resolveEntityUuidByType,
    requestTypeQueryVariants,
    uuidCandidatesForWorknoteRequestId
} from '../../common/utils/requestResolver.js';
import * as EmailService from '../../common/utils/email.service.js';
import { getIO } from '../../common/utils/socket.js';
import * as InAppPushNotificationService from '../../common/utils/notification.service.js';
import { NotificationService as EmailNotificationService } from '../../services/NotificationService.js';
import logger from '../../common/utils/logger.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';

// --- Helpers ---
const getDocumentModel = (requestType: string) => {
    switch (requestType?.toLowerCase()) {
        case 'relocation': return RelocationDocument;
        case 'resignation': return ResignationDocument;
        case 'constitutional':
        case 'constitutional-change': return ConstitutionalDocument;
        case 'termination': return TerminationDocument;
        case 'onboarding':
        case 'application': return OnboardingDocument;
        default: return OnboardingDocument;
    }
};

const stitchWorknoteAttachments = async (worknotes: any[]) => {
    const notePromises = worknotes.map(async (note: any) => {
        const noteObj = note.toJSON ? note.toJSON() : note;
        if (noteObj.attachments && noteObj.attachments.length > 0) {
            // Group by documentType for batch fetching
            const typeGroups: Record<string, string[]> = {};
            noteObj.attachments.forEach((att: any) => {
                const type = att.documentType || 'onboarding';
                if (!typeGroups[type]) typeGroups[type] = [];
                typeGroups[type].push(att.documentId);
            });

            const attachmentsWithFiles = [];
            for (const [type, ids] of Object.entries(typeGroups)) {
                const DocModel = getDocumentModel(type);
                if (DocModel) {
                    const docs = await (DocModel as any).findAll({ where: { id: ids } });
                    const docsByid = new Map(docs.map((d: any) => [d.id, d.toJSON()]));
                    attachmentsWithFiles.push(...noteObj.attachments
                        .filter((att: any) => (att.documentType || 'onboarding') === type)
                        .map((att: any) => {
                            const doc = docsByid.get(att.documentId) as any;
                            return {
                                ...att,
                                fileName: doc?.fileName || 'Unknown',
                                filePath: doc?.filePath || '',
                                mimeType: doc?.mimeType || 'application/octet-stream',
                                fileSize: doc?.fileSize
                            };
                        })
                    );
                }
            }
            noteObj.attachments = attachmentsWithFiles;
        }
        return noteObj;
    });
    return Promise.all(notePromises);
};

/**
 * Build where-clause for `worknotes.request_id` (UUID column). Must not include human-readable
 * request codes (REL-*, CC-*, etc.) — Postgres rejects casting them to uuid (500 on IN lists).
 */
function worknoteListWhere(rawId: string, resolvedId: string, normalizedType: string) {
    const variants = requestTypeQueryVariants(normalizedType);
    const uuidIds = uuidCandidatesForWorknoteRequestId(rawId, resolvedId);

    if (uuidIds.length === 0) {
        // No UUID to match — return unsatisfiable predicate without invalid uuid casts
        return {
            userId: { [db.Sequelize.Op.is]: null } as any,
            requestType: variants.length > 1 ? { [db.Sequelize.Op.in]: variants } : variants[0]
        };
    }

    const requestIdWhere = uuidIds.length > 1 ? { [db.Sequelize.Op.in]: uuidIds } : uuidIds[0];
    if (variants.length > 1) return { requestId: requestIdWhere, requestType: { [db.Sequelize.Op.in]: variants } };
    return { requestId: requestIdWhere, requestType: variants[0] };
}

// --- Worknotes ---

export const addWorknote = async (req: AuthRequest, res: Response) => {
    try {
        const { requestId, requestType, noteText, noteType, tags, attachmentDocIds } = req.body;
        const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
        logger.info(`Adding worknote for ${normalizedType} ${resolvedId}. Body:`, { noteText, tags, attachmentDocIds });

        // Debug: Log participants
        const participants = await db.RequestParticipant.findAll({
            where: { requestId: resolvedId, requestType: normalizedType },
            include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }]
        });
        const simplifiedParticipants = participants.map((p: any) => ({ id: p.user?.id, name: p.user?.fullName }));
        logger.info(`Participants for ${resolvedId}:`, simplifiedParticipants);

        const worknote = await Worknote.create({
            requestId: resolvedId,
            requestType: normalizedType,
            userId: req.user?.id,
            noteText,
            noteType: noteType || 'General',
            status: 'Active'
        });

        if (tags && tags.length > 0) {
            for (const tag of tags) {
                await WorkNoteTag.create({ noteId: worknote.id, tagName: tag });
            }
        }

        if (attachmentDocIds && attachmentDocIds.length > 0) {
            for (const docId of attachmentDocIds) {
                await WorkNoteAttachment.create({ 
                    noteId: worknote.id, 
                    documentId: docId,
                    documentType: normalizedType || 'onboarding'
                });
            }
        }

        // Add author as participant
        if (req.user?.id && resolvedId && normalizedType) {
            await db.RequestParticipant.findOrCreate({
                where: {
                    requestId: resolvedId,
                    requestType: normalizedType,
                    userId: req.user.id
                },
                defaults: {
                    participantType: 'contributor',
                    joinedMethod: 'worknote'
                }
            });
        }

        // Reload with associations for the response and socket emission
        const fullWorknote = await Worknote.findByPk(worknote.id, {
            include: [
                { model: User, as: 'author', attributes: [['fullName', 'name'], 'email', ['roleCode', 'role']] },
                { model: WorkNoteTag, as: 'tags' },
                { model: WorkNoteAttachment, as: 'attachments' }
            ]
        });

        const [stitchedNote] = await stitchWorknoteAttachments([fullWorknote]);

        // --- Real-time & Notifications ---
        try {
            const io = getIO();
            io.to(resolvedId).emit('new_worknote', stitchedNote);

            // Handle Mentions/Notifications
            const notifiedUserIds = new Set<string>();

            // 1. Check structured mentions in noteText
            logger.info(`Checking worknote for mentions. Text: "${noteText}"`);
            if (noteText && typeof noteText === 'string' && noteText.includes('@')) {
                const mentionRegex = /@\[([^\]]+)\]\(user:([^\)]+)\)/g;
                let match;
                while ((match = mentionRegex.exec(noteText)) !== null) {
                    const userId = match[2];
                    if (userId && userId !== req.user?.id) {
                        notifiedUserIds.add(userId);
                    }
                }
            }

            // 2. Check tags (fallback/robustness)
            if (tags && Array.isArray(tags)) {
                tags.forEach((tag: any) => {
                    // If tag is a UUID-like string and not the current user
                    if (typeof tag === 'string' && tag.length > 20 && tag !== req.user?.id) {
                        notifiedUserIds.add(tag);
                    }
                });
            }

            const portalBase = getFrontendBaseUrl();
            let worknoteLink = `${portalBase}/dashboard`;
            let applicationIdLabel = String(resolvedId).slice(0, 12);
            let dealerNameLabel = 'Record';

            try {
                const appRow = await Application.findByPk(resolvedId, {
                    attributes: ['applicationId', 'applicantName']
                });
                if (appRow) {
                    applicationIdLabel = appRow.applicationId || applicationIdLabel;
                    dealerNameLabel = appRow.applicantName || dealerNameLabel;
                    worknoteLink = `${portalBase}/applications/${resolvedId}?tab=worknotes`;
                }
            } catch {
                /* optional lookup */
            }

            const mentionSnippet =
                noteText && typeof noteText === 'string'
                    ? noteText.replace(/@\[([^\]]+)\]\(user:[^\)]+\)/g, '$1').slice(0, 280)
                    : 'You were mentioned in a work note.';

            // Send Notifications (in-app + push + email template)
            for (const userId of notifiedUserIds) {
                logger.info(`Sending notification to user ID: ${userId}`);
                try {
                    await InAppPushNotificationService.sendNotification({
                        userId,
                        title: 'New Mention',
                        message: `${req.user?.fullName || 'Someone'} mentioned you in a worknote.`,
                        type: 'info',
                        link: `/applications/${resolvedId}?tab=worknotes`
                    });

                    const targetUser = await User.findByPk(userId, {
                        attributes: ['id', 'email', 'fullName']
                    });
                    if (targetUser?.email) {
                        await EmailNotificationService.notify(userId, targetUser.email, {
                            title: 'Worknote mention',
                            message: mentionSnippet,
                            channels: ['email'],
                            templateCode: 'WORKNOTE_NOTIFICATION',
                            placeholders: {
                                userName: targetUser.fullName || 'there',
                                applicationId: applicationIdLabel,
                                dealerName: dealerNameLabel,
                                message: mentionSnippet,
                                link: worknoteLink,
                                ctaLabel: 'View worknotes'
                            }
                        });
                    }
                } catch (notifyErr) {
                    logger.warn(`Failed to send notification to ${userId}:`, notifyErr);
                }
            }

            if (notifiedUserIds.size === 0 && noteText && noteText.includes('@')) {
                logger.warn('Worknote contains "@" but no mentions were successfully identified.');
            }
        } catch (err) {
            console.warn('Real-time notification failed:', err);
        }

        // Audit log for worknote
        await AuditLog.create({
            userId: req.user?.id,
            action: AUDIT_ACTIONS.WORKNOTE_ADDED,
            entityType: 'application',
            entityId: resolvedId,
            newData: { noteType: noteType || 'General', hasAttachments: !!(attachmentDocIds?.length) }
        });

        res.status(201).json({ success: true, message: 'Worknote added', data: stitchedNote });
    } catch (error) {
        console.error('Add worknote error:', error);
        res.status(500).json({ success: false, message: 'Error adding worknote' });
    }
};

export const getWorknotes = async (req: AuthRequest, res: Response) => {
    try {
        const { requestId, requestType } = req.query as any;
        const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
        const where = worknoteListWhere(String(requestId || ''), resolvedId, normalizedType);

        const worknotes = await Worknote.findAll({
            where,
            include: [
                { model: User, as: 'author', attributes: [['fullName', 'name'], 'email', ['roleCode', 'role']] },
                { model: WorkNoteTag, as: 'tags' },
                { model: WorkNoteAttachment, as: 'attachments' }
            ],
            order: [['createdAt', 'DESC']]
        });

        const finalWorknotes = await stitchWorknoteAttachments(worknotes);
        res.json({ success: true, data: finalWorknotes });
    } catch (error) {
        res.status(500).json({ success: false, message: 'Error fetching worknotes' });
    }
};

export const uploadWorknoteAttachment = async (req: any, res: Response) => {
    try {
        const file = req.file;
        const { requestId, requestType } = req.body;
        const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);

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

        const DocModel = getDocumentModel(normalizedType);
        
        let createData: any = {
            documentType: 'Worknote Attachment',
            fileName: file.originalname,
            filePath: file.path,
            mimeType: file.mimetype,
            fileSize: file.size,
            uploadedBy: req.user?.id,
            status: 'active'
        };

        // Assign correct FK based on model (always UUID for self-service modules)
        if (DocModel === RelocationDocument) createData.relocationId = resolvedId;
        else if (DocModel === ResignationDocument) createData.resignationId = resolvedId;
        else if (DocModel === ConstitutionalDocument) createData.constitutionalChangeId = resolvedId;
        else if (DocModel === TerminationDocument) createData.terminationRequestId = resolvedId;
        else createData.applicationId = resolvedId;

        const document = await DocModel.create(createData);

        // Create initial version
        await DocumentVersion.create({
            documentId: document.id,
            documentType: normalizedType || 'onboarding',
            versionNumber: 1,
            filePath: file.path,
            uploadedBy: req.user?.id,
            changeReason: 'Initial Upload'
        });

        // Audit log for attachment upload
        if (resolvedId) {
            await AuditLog.create({
                userId: req.user?.id,
                action: AUDIT_ACTIONS.ATTACHMENT_UPLOADED,
                entityType: normalizedType || 'application',
                entityId: resolvedId,
                newData: { fileName: file.originalname, mimeType: file.mimetype }
            });
        }

        res.status(201).json({
            success: true,
            message: 'Attachment uploaded',
            data: {
                id: document.id,
                fileName: document.fileName,
                filePath: document.filePath,
                mimeType: document.mimeType
            }
        });
    } catch (error) {
        console.error('Upload worknote attachment error:', error);
        res.status(500).json({ success: false, message: 'Error uploading attachment' });
    }
};

// --- Documents ---

export const uploadDocument = async (req: AuthRequest, res: Response) => {
    try {
        const { applicationId, dealerId, docType, fileName, fileUrl, mimeType } = req.body;

        const document = await OnboardingDocument.create({
            applicationId,
            dealerId,
            documentType: docType,
            fileName,
            filePath: fileUrl, // Assuming URL from cloud storage
            mimeType,
            uploadedBy: req.user?.id
        });

        // Create Initial Version
        await DocumentVersion.create({
            documentId: document.id,
            versionNumber: 1,
            filePath: fileUrl,
            uploadedBy: req.user?.id,
            changeReason: 'Initial Upload'
        });

        // Audit log for document upload
        await AuditLog.create({
            userId: req.user?.id,
            action: AUDIT_ACTIONS.DOCUMENT_UPLOADED,
            entityType: 'application',
            entityId: applicationId,
            newData: { docType, fileName }
        });

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

export const uploadNewVersion = async (req: AuthRequest, res: Response) => {
    try {
        const { documentId, fileUrl, changeReason } = req.body;

        const lastVersion = await DocumentVersion.findOne({
            where: { documentId },
            order: [['versionNumber', 'DESC']]
        });
        const nextVersion = (lastVersion?.versionNumber || 0) + 1;

        await DocumentVersion.create({
            documentId,
            versionNumber: nextVersion,
            filePath: fileUrl,
            uploadedBy: req.user?.id,
            changeReason
        });

        // Update main document pointer if needed (usually main doc points to latest or metadata)
        // For simplicity assuming onboarding if not specified
        await OnboardingDocument.update({ filePath: fileUrl }, { where: { id: documentId } });

        res.status(201).json({ success: true, message: 'New version uploaded' });
    } catch (error) {
        res.status(500).json({ success: false, message: 'Error uploading version' });
    }
};
// --- Participants ---

export const addParticipant = async (req: AuthRequest, res: Response) => {
    try {
        const { requestId, requestType, userId, participantType } = req.body;

        const [participant, created] = await RequestParticipant.findOrCreate({
            where: { requestId, requestType, userId },
            defaults: {
                participantType: participantType || 'contributor',
                joinedMethod: 'manual'
            }
        });

        if (!created) {
            await participant.update({ participantType });
        }

        // Notify the user via email
        const user = await User.findByPk(userId);
        const application = await Application.findByPk(requestId);

        if (user && application) {
            await EmailService.sendUserAssignedEmail(
                user.email,
                user.fullName,
                application.applicationId || application.id,
                application.applicantName, // The dealer's name
                participantType || 'participant'
            );
        }

        // Audit log for participant added
        await AuditLog.create({
            userId: req.user?.id,
            action: AUDIT_ACTIONS.PARTICIPANT_ADDED,
            entityType: requestType || 'application',
            entityId: requestId,
            newData: { addedUserId: userId, participantType: participantType || 'contributor', userName: user?.fullName }
        });

        res.status(201).json({ success: true, message: 'Participant added', data: participant });
    } catch (error) {
        console.error('Add participant error:', error);
        res.status(500).json({ success: false, message: 'Error adding participant' });
    }
};

export const removeParticipant = async (req: AuthRequest, res: Response) => {
    try {
        const { id } = req.params;
        const { reason } = req.body || {}; // Safe destructuring

        const participant = await RequestParticipant.findByPk(id);
        if (!participant) {
            return res.status(404).json({ success: false, message: 'Participant not found' });
        }

        // ROLE SECURITY: DD Lead, DD Head, NBH, DD Admin, Super Admin
        const authorizedRoles: string[] = [ROLES.DD_LEAD, ROLES.DD_HEAD, ROLES.NBH, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN];
        if (!authorizedRoles.includes(req.user?.roleCode || '')) {
            return res.status(403).json({ success: false, message: 'Unauthorized: Only designated roles can revoke participants.' });
        }

        // SOFT DELETE: Update metadata instead of destroying record
        const metadata = {
            ...(participant.metadata || {}),
            revokedAt: new Date(),
            revokedBy: req.user?.id,
            revocationReason: reason || 'Manual revocation'
        };

        await participant.update({ metadata });

        // Audit log for participant revoked
        await AuditLog.create({
            userId: req.user?.id,
            action: AUDIT_ACTIONS.PARTICIPANT_REMOVED, // Using existing constant
            entityType: (participant as any).requestType || 'application',
            entityId: (participant as any).requestId,
            newData: { 
                revokedUserId: (participant as any).userId,
                revocationReason: reason || 'Manual revocation'
            }
        });

        res.json({ success: true, message: 'Participant access revoked successfully' });
    } catch (error) {
        console.error('Revoke participant error:', error);
        res.status(500).json({ success: false, message: 'Error revoking participant' });
    }
};
