import { NotificationLogDao } from "../dao/notificationLog.dao";
import { NotificationDao } from "../dao/notification.dao";
import { sequelize } from "../models";
import { AppError } from "../../utils/errors";
import { Common } from "../../utils/common";
import Models from "../models";
import { Op } from "sequelize";

export class NotificationLogService {
    private userId: number | null;
    private notificationLogDao: NotificationLogDao;

    constructor(
        options: {
            language: string;
            scope: string[] | null;
            accountId: number | null;
            userId: number | null;
            config: userConfig | null;
        } = {
                language: process.env.DEFAULT_LANGUAGE_CODE!,
                scope: null,
                config: null,
                userId: null,
                accountId: null,
            }
    ) {
        this.userId = options.userId ?? null;
        this.notificationLogDao = new NotificationLogDao(options);
    }

    // Resolves entity data from replacements.data based on replacements.type.
    // For type 'lead': looks up Lead (businessName) and DepartmentContent (departmentName).
    // For type 'business': looks up BusinessContent (businessName) and DepartmentContent (departmentName).
    private static async enrichLogs(logs: NotificationLogObjectInterface[]): Promise<NotificationLogObjectInterface[]> {
        const leadLogs = logs.filter(l => l.replacements?.type === 'lead');
        const businessLogs = logs.filter(l => l.replacements?.type === 'business');
        const seaqaLogs = logs.filter(l => l.replacements?.type === 'seaqa');

        if (leadLogs.length === 0 && businessLogs.length === 0 && seaqaLogs.length === 0) {
            return logs.map(l => ({ ...l, resolvedData: null }));
        }

        const leadIds = [...new Set(leadLogs.map(l => l.replacements?.data?.leadId).filter(Boolean))];
        const businessIds = [...new Set(businessLogs.map(l => l.replacements?.data?.businessId).filter(Boolean))];
        const deptIds = [...new Set([...leadLogs, ...businessLogs].map(l => l.replacements?.data?.departmentId).filter(Boolean))];
        const answerIds = [...new Set(seaqaLogs.map(l => l.replacements?.data?.answerId).filter(Boolean))];
        const questionIds = [...new Set(seaqaLogs.map(l => (!l.replacements?.data?.title || !l.replacements?.data?.code) ? l.replacements?.data?.questionId : null).filter(Boolean))];

        const defaultLang = await Models.Language.findOne({ attributes: ['id'], where: { code: process.env.DEFAULT_LANGUAGE_CODE } });

        const [leads, businessContents, answers, questions] = await Promise.all([
            leadIds.length > 0 ? Models.Lead.findAll({ attributes: ['id', 'businessName'], where: { id: { [Op.in]: leadIds } } }) : Promise.resolve([]),
            businessIds.length > 0 && defaultLang ? Models.BusinessContent.findAll({ attributes: ['businessId', 'businessName'], where: { businessId: { [Op.in]: businessIds }, languageId: (defaultLang as any).id } }) : Promise.resolve([]),
            answerIds.length > 0 ? Models.SeaQAAnswer.findAll({ 
                attributes: ['id'], 
                where: { id: { [Op.in]: answerIds } },
                include: [
                    { model: Models.SeaQAAnswerContent, as: 'content', required: false, include: [{ model: Models.Language, as: 'language', where: { code: process.env.DEFAULT_LANGUAGE_CODE } }] },
                    { model: Models.SeaQAAnswerContent, as: 'defaultContent', required: false }
                ]
            }) : Promise.resolve([]),
            questionIds.length > 0 ? Models.SeaQAQuestion.findAll({
                attributes: ['id', 'code'],
                where: { id: { [Op.in]: questionIds } },
                include: [
                    { model: Models.SeaQAQuestionContent, as: 'content', required: false, include: [{ model: Models.Language, as: 'language', where: { code: process.env.DEFAULT_LANGUAGE_CODE } }] },
                    { model: Models.SeaQAQuestionContent, as: 'defaultContent', required: false }
                ]
            }) : Promise.resolve([])
        ]);

        let deptContents: any[] = [];
        if (deptIds.length > 0 && defaultLang) {
            deptContents = await Models.DepartmentContent.findAll({
                attributes: ['departmentId', 'name'],
                where: { departmentId: { [Op.in]: deptIds }, languageId: (defaultLang as any).id }
            });
        }

        const leadMap = new Map((leads as any[]).map((l: any) => [+l.id, l.businessName]));
        const businessMap = new Map((businessContents as any[]).map((b: any) => [+b.businessId, b.businessName]));
        const deptMap = new Map(deptContents.map((d: any) => [+d.departmentId, d.name]));
        const answerMap = new Map((answers as any[]).map((a: any) => [+a.id, a.content?.body || a.defaultContent?.body || '']));
        const questionMap = new Map((questions as any[]).map((q: any) => [+q.id, { code: q.code, title: q.content?.title || q.defaultContent?.title || 'Unknown Question' }]));

        return logs.map(log => {
            let data = (log.replacements as any)?.data || {};
            
            if (log.replacements?.type === 'seaqa') {
                if (data.questionId && (!data.title || !data.code)) {
                    const qData = questionMap.get(+data.questionId);
                    if (qData) {
                        data = { ...data, title: data.title || qData.title, code: data.code || qData.code };
                        (log.replacements as any).data = data;
                    }
                }
                
                return {
                    ...log,
                    resolvedData: {
                        answer: data.answerId ? (answerMap.get(+data.answerId) ?? null) : null
                    }
                };
            }

            if (log.replacements?.type === 'lead') {
                return {
                    ...log,
                    resolvedData: {
                        businessName: leadMap.get(+data.leadId) ?? null,
                        departmentName: deptMap.get(+data.departmentId) ?? null
                    }
                };
            }
            if (log.replacements?.type === 'business') {
                return {
                    ...log,
                    resolvedData: {
                        businessName: businessMap.get(+data.businessId) ?? null,
                        departmentName: deptMap.get(+data.departmentId) ?? null
                    }
                };
            }
            return { ...log, resolvedData: null };
        });
    }

    // Create a notification log entry and emit to user's socket connections.
    // Call this from any service when a notification should be delivered to a user.
    static emit = async (data: NotificationLogEmitServiceInput): Promise<NotificationLogObjectInterface> => {
        const { userId, notificationId, replacements } = data;
        const fromUserId = data.fromUserId ?? null;
        if (userId && fromUserId && userId === fromUserId) {
            return {
                id: 0,
                userId,
                notificationId,
                fromUserId,
                replacements,
                viewed: true,
                read: true,
                createdAt: new Date(),
                updatedAt: new Date(),
            } as any;
        }
        const transaction = await sequelize.transaction();
        try {
            const dao = new NotificationLogDao({
                language: process.env.DEFAULT_LANGUAGE_CODE!,
                scope: null,
                config: null,
                userId,
                accountId: null,
            });

            const createInput: NotificationLogCreateDaoInput = { userId, notificationId, fromUserId: data.fromUserId ?? null, replacements };
            const log = await dao.create(createInput, { transaction });
            await transaction.commit();

            const [enrichedLog] = await NotificationLogService.enrichLogs([log]);

            // Emit directly to each of the user's active socket connections
            const activeSockets = await Models.Socket.findAll({
                attributes: ['socketId'],
                where: { userId, status: 1 }
            });
            if (activeSockets.length > 0) {
                const io = (globalThis as any).io;
                activeSockets.forEach((s: any) => {
                    io?.to(s.socketId).emit('notification', enrichedLog);
                });
            }

            return enrichedLog;
        } catch (err) {
            await transaction.rollback();
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // Emit by notification code instead of ID — resolves the code to an ID then delegates to emit.
    static emitByCode = async (data: NotificationLogEmitByCodeServiceInput): Promise<NotificationLogObjectInterface> => {
        try {
            const notificationDao = new NotificationDao({
                language: process.env.DEFAULT_LANGUAGE_CODE!,
                scope: null,
                config: null,
                userId: null,
                accountId: null,
            });
            const notification = await notificationDao.getNotificationByCode({ code: data.notificationCode, expanded: false });
            return await NotificationLogService.emit({
                userId: data.userId,
                notificationId: notification.id,
                fromUserId: data.fromUserId ?? null,
                replacements: data.replacements,
            });
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // List notification logs for the authenticated user with optional viewed/read filters.
    getList = async (data: NotificationLogGetListServiceInput): Promise<NotificationLogPaginatedList> => {
        try {
            if (!this.userId) {
                throw new AppError(401, 'UNAUTHORIZED_REQUEST', {});
            }
            const { listRequest } = data;
            const getListInput: NotificationLogGetListDaoInput = {
                userId: this.userId,
                listRequest,
            };
            const result = await this.notificationLogDao.getList(getListInput);
            const enrichedRows = await NotificationLogService.enrichLogs(result.rows);
            const totalPages = Common.getTotalPages(result.count, listRequest.perPage);
            return {
                data: enrichedRows,
                page: listRequest.page,
                perPage: listRequest.perPage,
                totalRecords: result.count,
                totalPages,
            } as unknown as NotificationLogPaginatedList;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    getUnreadCount = async (): Promise<number> => {
        try {
            if (!this.userId) {
                throw new AppError(401, 'UNAUTHORIZED_REQUEST', {});
            }
            return await this.notificationLogDao.getUnreadCount(this.userId);
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // Mark a notification log as viewed. Only the owning user can update their own log.
    markViewed = async (data: NotificationLogMarkViewedServiceInput): Promise<NotificationLogObjectInterface> => {
        const transaction = await sequelize.transaction();
        try {
            if (!this.userId) {
                throw new AppError(401, 'UNAUTHORIZED_REQUEST', {});
            }
            const log = await this.notificationLogDao.getById({ id: data.id }, { transaction });
            if (log.userId !== this.userId) {
                throw new AppError(403, 'PERMISSION_DENIED', {});
            }
            await this.notificationLogDao.markViewed({ id: data.id, userId: this.userId }, { transaction });
            const updated = await this.notificationLogDao.getById({ id: data.id }, { transaction });
            await transaction.commit();
            const [enriched] = await NotificationLogService.enrichLogs([updated]);
            return enriched;
        } catch (err) {
            await transaction.rollback();
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // Mark a notification log as read. Only the owning user can update their own log.
    markRead = async (data: NotificationLogMarkReadServiceInput): Promise<NotificationLogObjectInterface> => {
        const transaction = await sequelize.transaction();
        try {
            if (!this.userId) {
                throw new AppError(401, 'UNAUTHORIZED_REQUEST', {});
            }
            const log = await this.notificationLogDao.getById({ id: data.id }, { transaction });
            if (log.userId !== this.userId) {
                throw new AppError(403, 'PERMISSION_DENIED', {});
            }
            await this.notificationLogDao.markRead({ id: data.id, userId: this.userId }, { transaction });
            const updated = await this.notificationLogDao.getById({ id: data.id }, { transaction });
            await transaction.commit();
            const [enriched] = await NotificationLogService.enrichLogs([updated]);
            return enriched;
        } catch (err) {
            await transaction.rollback();
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }
}
