const moment = require('moment-timezone');
const logging = require('@tryghost/logging');

class EmailEventStorage {
    #db;
    #membersRepository;
    #models;
    #emailSuppressionList;

    constructor({db, models, membersRepository, emailSuppressionList}) {
        this.#db = db;
        this.#models = models;
        this.#membersRepository = membersRepository;
        this.#emailSuppressionList = emailSuppressionList;
    }

    async handleDelivered(event) {
        // To properly handle events that are received out of order (this happens because of polling)
        // only set if delivered_at is null
        await this.#db.knex('email_recipients')
            .where('id', '=', event.emailRecipientId)
            .whereNull('delivered_at')
            .update({
                delivered_at: moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')
            });
    }

    async handleOpened(event) {
        // To properly handle events that are received out of order (this happens because of polling)
        // only set if opened_at is null
        await this.#db.knex('email_recipients')
            .where('id', '=', event.emailRecipientId)
            .whereNull('opened_at')
            .update({
                opened_at: moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')
            });
    }

    async handlePermanentFailed(event) {
        // To properly handle events that are received out of order (this happens because of polling)
        // only set if failed_at is null
        await this.#db.knex('email_recipients')
            .where('id', '=', event.emailRecipientId)
            .whereNull('failed_at')
            .update({
                failed_at: moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')
            });
        await this.saveFailure('permanent', event);
    }

    async handleTemporaryFailed(event) {
        await this.saveFailure('temporary', event);
    }

    /**
     * @private
     * @param {'temporary'|'permanent'} severity
     * @param {import('@tryghost/email-events').EmailTemporaryBouncedEvent|import('@tryghost/email-events').EmailBouncedEvent} event
     * @param {{transacting?: any}} options
     * @returns
     */
    async saveFailure(severity, event, options = {}) {
        if (!event.error) {
            logging.warn(`Missing error information provided for ${severity} failure event with id ${event.id}`);
            return;
        }

        if (!options || !options.transacting) {
            return await this.#models.EmailRecipientFailure.transaction(async (transacting) => {
                await this.saveFailure(severity, event, {transacting});
            });
        }

        // Create a forUpdate transaction
        const existing = await this.#models.EmailRecipientFailure.findOne({
            email_recipient_id: event.emailRecipientId
        }, {...options, require: false, forUpdate: true});

        if (!existing) {
            // Create a new failure
            await this.#models.EmailRecipientFailure.add({
                email_id: event.emailId,
                member_id: event.memberId,
                email_recipient_id: event.emailRecipientId,
                severity,
                message: event.error.message || `Error ${event.error.enhancedCode ?? event.error.code}`,
                code: event.error.code,
                enhanced_code: event.error.enhancedCode,
                failed_at: event.timestamp,
                event_id: event.id
            }, {...options, autoRefresh: false});
        } else {
            if (existing.get('severity') === 'permanent') {
                // Already marked as failed, no need to change anything here
                return;
            }

            if (existing.get('failed_at') > event.timestamp) {
                /// We can get events out of order, so only save the last one
                return;
            }

            // Update the existing failure
            await existing.save({
                severity,
                message: event.error.message || `Error ${event.error.enhancedCode ?? event.error.code}`,
                code: event.error.code,
                enhanced_code: event.error.enhancedCode ?? null,
                failed_at: event.timestamp,
                event_id: event.id
            }, {...options, patch: true, autoRefresh: false});
        }
    }

    async handleUnsubscribed(event) {
        try {
            // Unsubscribe member from the specific newsletter
            const newsletters = await this.findNewslettersToKeep(event);
            await this.#membersRepository.update({newsletters}, {id: event.memberId});

            // Remove member from Mailgun's suppression list
            await this.#emailSuppressionList.removeUnsubscribe(event.email);
        } catch (err) {
            logging.error(err);
        }
    }

    async handleComplained(event) {
        try {
            await this.#models.EmailSpamComplaintEvent.add({
                member_id: event.memberId,
                email_id: event.emailId,
                email_address: event.email
            });
        } catch (err) {
            if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
                logging.error(err);
            }
        }
    }

    async findNewslettersToKeep(event) {
        try {
            const member = await this.#membersRepository.get({email: event.email}, {
                withRelated: ['newsletters']
            });
            const existingNewsletters = member.related('newsletters');

            const email = await this.#models.Email.findOne({id: event.emailId});
            const newsletterToRemove = email.get('newsletter_id');

            return existingNewsletters.models.filter(newsletter => newsletter.id !== newsletterToRemove).map((n) => {
                return {id: n.id};
            });
        } catch (err) {
            logging.error(err);
            return [];
        }
    }
}

module.exports = EmailEventStorage;
