/*
 * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.com).
 *
 * WSO2 Inc. licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.wso2.carbon.is.migration.service.v710.migrator;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.email.mgt.constants.I18nMgtConstants;
import org.wso2.carbon.identity.core.migrate.MigrationClientException;
import org.wso2.carbon.identity.governance.exceptions.notiification.NotificationTemplateManagerServerException;
import org.wso2.carbon.identity.governance.model.NotificationTemplate;
import org.wso2.carbon.identity.governance.service.notification.NotificationChannels;
import org.wso2.carbon.is.migration.service.Migrator;
import org.wso2.carbon.is.migration.service.v710.dao.NotificationDAO;
import org.wso2.carbon.is.migration.util.Utility;
import org.wso2.carbon.user.api.Tenant;
import org.wso2.carbon.utils.CarbonUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLTransientConnectionException;
import java.sql.SQLTransientException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import static org.wso2.carbon.base.MultitenantConstants.SUPER_TENANT_DOMAIN_NAME;
import static org.wso2.carbon.base.MultitenantConstants.SUPER_TENANT_ID;

/**
 * Migration implementation to cleanup system email template data.
 */
public class SystemTemplateMigrator extends Migrator {

    private static final Logger LOG = LoggerFactory.getLogger(SystemTemplateMigrator.class);

    private static final String WAIT_TIME = "waitTime";
    private static final long DEFAULT_WAIT_TIME_IN_MILLIS = 500;
    List<NotificationTemplate> systemDefaultNotificationTemplates;

    private final NotificationDAO notificationDAO = new NotificationDAO();
    long waitTimeInMillis = DEFAULT_WAIT_TIME_IN_MILLIS;

    @Override
    public void dryRun() throws MigrationClientException {

        LOG.info("STM>> System template cleanup (dry run) started.");
        cleanupSystemTemplates(true);
    }

    @Override
    public void migrate() throws MigrationClientException {

        LOG.info("STM>> System template cleanup started.");
        cleanupSystemTemplates(false);
    }

    private void cleanupSystemTemplates(boolean isDryRun) throws MigrationClientException {

        Properties migrationProperties = getMigratorConfig().getParameters();
        if (migrationProperties.containsKey(WAIT_TIME)) {
            waitTimeInMillis = (int) migrationProperties.get(WAIT_TIME);
        }
        LOG.info("STM>> Parameters: Wait time {}ms.", waitTimeInMillis);

        systemDefaultNotificationTemplates = loadSystemDefaultNotificationTemplates();
        LOG.info("STM>> Loaded {} system default templates.", systemDefaultNotificationTemplates.size());

        int successTenantCount = 0;
        int errorTenantCount = 0;
        int inactiveTenantCount = 0;

        Set<Tenant> tenants = getTenants();

        int totalTenantCount = tenants.size();
        LOG.info("STM>> Found {} tenants in the system.", totalTenantCount + 1); // Including super tenant.

        try {
            // Migrate super tenant.
            cleanupSystemTemplatesForTenant(isDryRun, false, SUPER_TENANT_ID, SUPER_TENANT_DOMAIN_NAME);
            successTenantCount++;

            // Migrate other tenants.
            boolean ignoreForInactiveTenants = isIgnoreForInactiveTenants();
            if (ignoreForInactiveTenants) {
                tenants = Utility.getActiveTenants(tenants);
                inactiveTenantCount = totalTenantCount - tenants.size();
                LOG.info("STM>> Excluding {} inactive tenants from the migration.", inactiveTenantCount);
            }
            totalTenantCount = tenants.size() + 1; // Including super tenant.
            LOG.info("STM>> Selected {} tenants for the migration.", totalTenantCount);

            List<Tenant> tenantList = new ArrayList<>(tenants);
            tenantList.sort(Comparator.comparingInt(Tenant::getId));

            int i = 0;
            for (Tenant tenant : tenantList) {
                i++;
                int tenantId = tenant.getId();
                String tenantDomain = tenant.getDomain();

                LOG.info("STM>> Starting migration for {} of {} tenant.", i, totalTenantCount);
                LOG.info("STM>> Migrating tenant: {} with tid: {}.", tenantDomain, tenantId);

                try {
                    cleanupSystemTemplatesForTenant(isDryRun, false, tenantId, tenantDomain);
                    successTenantCount++;
                } catch (MigrationClientException e) {
                    if (!isContinueOnError()) {
                        throw new NotificationTemplateManagerServerException(e.getMessage(), e);
                    }
                    errorTenantCount++;
                    LOG.error("STM>> Error while migrating notification templates in tenant {}.", tenantDomain, e);
                }
                LOG.info("STM>> Notification template migration successfully completed for tenant {}.",
                        tenantDomain);
            }
        } catch (NotificationTemplateManagerServerException e) {
            errorTenantCount++;
            LOG.error("STM>> Error while migrating notification templates.", e);
        }

        if (errorTenantCount == 0) {
            LOG.info("STM>> Notification templates residual data migration successfully completed.");
        } else {
            LOG.info("STM>> Notification templates residual data migration completed with errors in {} tenants.",
                    errorTenantCount);
        }
        LOG.info("STM>> Successfully cleaned up: {} of {} tenants.", successTenantCount, totalTenantCount);
        LOG.info("STM>> Found issues with:  {} of {} tenants.", errorTenantCount, totalTenantCount);
        LOG.info("STM>> Skipped {} inactive tenants.", inactiveTenantCount);
    }

    private void cleanupSystemTemplatesForTenant(boolean isDryRun, boolean isRetry, int tenantId, String tenantDomain)
            throws MigrationClientException {

        try {
            try {
                notificationDAO.deleteSystemDefaultNotificationTemplates(systemDefaultNotificationTemplates,
                        NotificationChannels.EMAIL_CHANNEL.getChannelType(), isDryRun, tenantId);
                notificationDAO.deleteSystemDefaultNotificationTemplates(systemDefaultNotificationTemplates,
                        NotificationChannels.SMS_CHANNEL.getChannelType(), isDryRun, tenantId);
                LOG.info("STM>> System default templates successfully cleanup up in database for tenant {}.", tenantDomain);
            } finally {
                if (waitTimeInMillis > 0) {
                    try {
                        LOG.info("STM>> Waiting {}ms to cool down...", waitTimeInMillis);
                        Thread.sleep(waitTimeInMillis);
                    } catch (InterruptedException e) {
                        LOG.error("STM>> Error while waiting for the next batch.", e);
                    }
                }
            }
        } catch (NotificationTemplateManagerServerException e) {
            // This is ideally needed to be handled by each respective components.
            // Since carbon components doesn't handle transient exceptions across the board, we need to handle it here.
            if (!isRetry && isTransientException(e)) {
                LOG.error("STM>> Transient error occurred while migrating notification templates in tenant {}. ",
                        tenantDomain, e);
                try {
                    LOG.info("STM>> Retrying after waiting for 2 minutes.");
                    Thread.sleep(120*1000);
                } catch (InterruptedException ex) {
                    LOG.error("STM>> Retrying without waiting.", e);
                }
                cleanupSystemTemplatesForTenant(isDryRun, true, tenantId, tenantDomain);
            } else {
                throw new MigrationClientException("Error while migrating tenant: " + tenantDomain, e);
            }
        }
    }

    private static boolean isTransientException(Exception e) {

        Throwable rootCause = ExceptionUtils.getRootCause(e);
        return rootCause instanceof SQLTransientException || rootCause instanceof SQLTransientConnectionException ||
                (rootCause != null && rootCause.getMessage() != null &&
                        rootCause.getMessage().contains("Connection reset by peer"));
    }

    private List<NotificationTemplate> loadSystemDefaultNotificationTemplates() throws MigrationClientException {

        List<NotificationTemplate> defaultTemplates = new ArrayList<>();
        defaultTemplates.addAll(loadDefaultTemplatesFromFile(NotificationChannels.EMAIL_CHANNEL.getChannelType()));
        defaultTemplates.addAll(loadDefaultTemplatesFromFile(NotificationChannels.SMS_CHANNEL.getChannelType()));
        return defaultTemplates;
    }

    /**
     * Get the template attributes of the notification template such as SUBJECT, BODY, EMAIL.
     *
     * @param templateElement OMElement
     * @return Map of template attributes.
     */
    private static Map<String, String> getNotificationTemplateContent(OMElement templateElement) {

        Map<String, String> notificationTemplateContent = new HashMap<>();
        Iterator it = templateElement.getChildElements();
        while (it.hasNext()) {
            OMElement element = (OMElement) it.next();
            String elementName = element.getLocalName();
            String elementText = element.getText();
            if (StringUtils.equalsIgnoreCase(I18nMgtConstants.TEMPLATE_SUBJECT, elementName)) {
                notificationTemplateContent.put(I18nMgtConstants.TEMPLATE_SUBJECT, elementText);
            } else if (StringUtils.equalsIgnoreCase(I18nMgtConstants.TEMPLATE_BODY, elementName)) {
                notificationTemplateContent.put(I18nMgtConstants.TEMPLATE_BODY, elementText);
            } else if (StringUtils.equalsIgnoreCase(I18nMgtConstants.TEMPLATE_FOOTER, elementName)) {
                notificationTemplateContent.put(I18nMgtConstants.TEMPLATE_FOOTER, elementText);
            }
        }
        return notificationTemplateContent;
    }

    /**
     * Loads the default templates from the file for the channel(EMAIL or SMS) and create list of Notification Template.
     *
     * @param notificationChannel Channel of the notification.
     * @return List of NotificationTemplate.
     */
    public List<NotificationTemplate> loadDefaultTemplatesFromFile(String notificationChannel) {

        String configFilePath = buildNotificationTemplateConfigPath(notificationChannel);
        File configFile = new File(configFilePath);
        if (!configFile.exists()) {
            LOG.error("Email Configuration File is not present at: " + configFilePath);
        }

        List<NotificationTemplate> defaultNotificationTemplates = new ArrayList<>();
        XMLStreamReader xmlStreamReader = null;

        try (InputStream inputStream = new FileInputStream(configFile)) {
            xmlStreamReader = XMLInputFactory.newInstance().createXMLStreamReader(inputStream);
            StAXOMBuilder builder = new StAXOMBuilder(xmlStreamReader);

            OMElement documentElement = builder.getDocumentElement();
            Iterator iterator = documentElement.getChildElements();
            while (iterator.hasNext()) {
                OMElement omElement = (OMElement) iterator.next();
                Map<String, String> templateContentMap = getNotificationTemplateContent(omElement);

                // Create notification template model with the template attributes.
                NotificationTemplate notificationTemplate = new NotificationTemplate();
                String templateType = omElement.getAttributeValue(new QName(I18nMgtConstants.TEMPLATE_TYPE));
                String displayName =
                        omElement.getAttributeValue(new QName(I18nMgtConstants.TEMPLATE_TYPE_DISPLAY_NAME));
                if (!templateType.equalsIgnoreCase(displayName)) {
                    LOG.warn("STM>> Template type and display name are not equal for the template: " + displayName);
                    templateType = displayName;
                }
                notificationTemplate.setType(templateType);
                notificationTemplate.setDisplayName(displayName);
                notificationTemplate
                        .setLocale(omElement.getAttributeValue(new QName(I18nMgtConstants.TEMPLATE_LOCALE)));
                notificationTemplate.setBody(templateContentMap.get(I18nMgtConstants.TEMPLATE_BODY));
                notificationTemplate.setNotificationChannel(notificationChannel);

                if (NotificationChannels.EMAIL_CHANNEL.getChannelType().equals(notificationChannel)) {
                    notificationTemplate.setContentType(
                            omElement.getAttributeValue(new QName(I18nMgtConstants.TEMPLATE_CONTENT_TYPE)));
                    notificationTemplate.setFooter(templateContentMap.get(I18nMgtConstants.TEMPLATE_FOOTER));
                    notificationTemplate.setSubject(templateContentMap.get(I18nMgtConstants.TEMPLATE_SUBJECT));
                }
                defaultNotificationTemplates.add(notificationTemplate);
            }
        } catch (XMLStreamException | IOException e) {
            LOG.warn("Error while loading default templates from file.", e);
        } finally {
            try {
                if (xmlStreamReader != null) {
                    xmlStreamReader.close();
                }
            } catch (XMLStreamException e) {
                LOG.error("Error while closing XML stream", e);
            }
        }
        return defaultNotificationTemplates;
    }

    /**
     * Build the file path of the notification template config file according to the given channel.
     *
     * @param notificationChannel Notification channel name (EMAIL,SMS)
     * @return File path of the notification template config file.
     */
    private String buildNotificationTemplateConfigPath(String notificationChannel) {

        if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannel)) {
            return CarbonUtils.getCarbonConfigDirPath() + File.separator +
                    I18nMgtConstants.SMS_CONF_DIRECTORY + File.separator + I18nMgtConstants.SMS_TEMPLAE_ADMIN_CONF_FILE;
        }
        return CarbonUtils.getCarbonConfigDirPath() + File.separator +
                I18nMgtConstants.EMAIL_CONF_DIRECTORY + File.separator + I18nMgtConstants.EMAIL_ADMIN_CONF_FILE;
    }
}
