package com.iqser.red.service.persistence.management.v1.processor.migration.liquibase;

import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;

import liquibase.change.custom.CustomTaskChange;
import liquibase.database.Database;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.CustomChangeException;
import liquibase.exception.ValidationErrors;
import liquibase.resource.ResourceAccessor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@SuppressWarnings("PMD.CloseResource") // we do not want to close the underlying database connection ;)
public class TechnicalNameChange implements CustomTaskChange {

    // there is a bug in our liquibase version which causes custom task changes to be run twice
    // that bug got fixed in liquibase 4.25.1
    // when we upgrade the dependency this can then be removed
    @Setter
    private static boolean skipExecution;


    @Override
    public void execute(Database database) throws CustomChangeException {

        if (skipExecution) {
            return;
        }

        Connection connection;

        try {
            connection = ((JdbcConnection) database.getConnection()).getUnderlyingConnection();

            String[] tables = {"manual_legal_basis_change", "manual_force_redaction", "manual_recategorization", "manual_redaction"};

            HashFunction hashFunction = Hashing.murmur3_128();

            Set<String> allFileIds = collectAllFileIds(connection, tables);

            if (allFileIds.isEmpty()) {
                log.info("No rows updated because no files with manual redactions are present.");
                return;
            }

            Map<String, String> fileIdToDossierTemplateIdMap = buildFileIdToDossierTemplateIdMap(connection, allFileIds);

            Map<String, String> reasonDossierTemplateIdToTechnicalNameMap = buildReasonDossierTemplateIdToTechnicalNameMap(connection);

            int totalRowsUpdatedWithTechnicalName = 0;
            int totalRowsHashed = 0;

            for (String tableName : tables) {
                int[] counts = updateLegalBasisForTable(connection, tableName, hashFunction, fileIdToDossierTemplateIdMap, reasonDossierTemplateIdToTechnicalNameMap);
                int rowsUpdatedWithTechnicalName = counts[0];
                int rowsHashed = counts[1];

                log.info("Table '{}': {} rows updated with technical_name, {} rows hashed.", tableName, rowsUpdatedWithTechnicalName, rowsHashed);

                totalRowsUpdatedWithTechnicalName += rowsUpdatedWithTechnicalName;
                totalRowsHashed += rowsHashed;
            }

            log.info("Total rows updated with technical_name: {}", totalRowsUpdatedWithTechnicalName);
            log.info("Total rows hashed: {}", totalRowsHashed);

        } catch (Exception e) {
            throw new CustomChangeException("Error applying technical name change", e);
        }
        skipExecution = true;
    }


    private Set<String> collectAllFileIds(Connection connection, String[] tables) throws SQLException {

        Set<String> fileIds = new HashSet<>();

        for (String tableName : tables) {
            String selectSql = "SELECT DISTINCT file_id FROM " + tableName + " WHERE file_id IS NOT NULL";
            try (PreparedStatement select = connection.prepareStatement(selectSql); ResultSet resultSet = select.executeQuery()) {
                while (resultSet.next()) {
                    String fileId = resultSet.getString("file_id");
                    fileIds.add(fileId);
                }
            }
        }

        return fileIds;
    }


    private Map<String, String> buildFileIdToDossierTemplateIdMap(Connection connection, Set<String> fileIds) throws SQLException {

        Map<String, String> map = new HashMap<>();

        if (!fileIds.isEmpty()) {

            StringBuilder sqlBuilder = new StringBuilder();
            sqlBuilder.append("SELECT f.id AS file_id, d.dossier_template_id ");
            sqlBuilder.append("FROM file f ");
            sqlBuilder.append("JOIN dossier d ON f.dossier_id = d.id ");
            sqlBuilder.append("WHERE f.id IN (");

            String placeholders = String.join(",", Collections.nCopies(fileIds.size(), "?"));
            sqlBuilder.append(placeholders);
            sqlBuilder.append(")");

            String selectSql = sqlBuilder.toString();
            try (PreparedStatement select = connection.prepareStatement(selectSql)) {
                int index = 1;
                for (String fileId : fileIds) {
                    select.setString(index++, fileId);
                }
                try (ResultSet rs = select.executeQuery()) {
                    while (rs.next()) {
                        String fileId = rs.getString("file_id");
                        String dossierTemplateId = rs.getString("dossier_template_id");
                        map.put(fileId, dossierTemplateId);
                    }
                }
            }
        }

        return map;
    }


    private Map<String, String> buildReasonDossierTemplateIdToTechnicalNameMap(Connection connection) throws SQLException {

        Map<String, String> map = new HashMap<>();

        String selectSql = "SELECT reason, legal_basis_mapping_entity_dossier_template_id, technical_name FROM legal_basis_mapping_entity_legal_basis";
        try (PreparedStatement select = connection.prepareStatement(selectSql); ResultSet resultSet = select.executeQuery()) {
            while (resultSet.next()) {
                String reason = resultSet.getString("reason");
                String dossierTemplateId = resultSet.getString("legal_basis_mapping_entity_dossier_template_id");
                String technicalName = resultSet.getString("technical_name");

                if (reason != null && dossierTemplateId != null && technicalName != null) {
                    String key = getKey(reason, dossierTemplateId);
                    map.put(key, technicalName);
                }
            }
        }

        return map;
    }


    private int[] updateLegalBasisForTable(Connection connection,
                                           String tableName,
                                           HashFunction hashFunction,
                                           Map<String, String> fileIdToDossierTemplateIdMap,
                                           Map<String, String> reasonDossierTemplateIdToTechnicalNameMap) throws SQLException {

        PreparedStatement select = null;
        PreparedStatement update = null;
        ResultSet resultSet = null;

        int rowsUpdatedWithTechnicalName = 0;
        int rowsHashed = 0;

        try {
            String selectSql = "SELECT annotation_id, file_id, legal_basis FROM " + tableName + " WHERE legal_basis IS NOT NULL AND TRIM(legal_basis) <> '';";
            select = connection.prepareStatement(selectSql);
            resultSet = select.executeQuery();

            String updateSql = "UPDATE " + tableName + " SET legal_basis = ? WHERE annotation_id = ? AND file_id = ?";
            update = connection.prepareStatement(updateSql);

            while (resultSet.next()) {
                String annotationId = resultSet.getString("annotation_id");
                String fileId = resultSet.getString("file_id");
                String originalValue = resultSet.getString("legal_basis");

                if (originalValue == null) {
                    continue;
                }

                String dossierTemplateId = fileIdToDossierTemplateIdMap.get(fileId);

                String newValue = null;

                if (dossierTemplateId != null) {
                    String key = getKey(originalValue, dossierTemplateId);
                    newValue = reasonDossierTemplateIdToTechnicalNameMap.get(key);
                }

                if (newValue == null || newValue.trim().isEmpty()) {
                    newValue = hashFunction.hashString(originalValue, StandardCharsets.UTF_8).toString();
                    rowsHashed++;
                } else {
                    rowsUpdatedWithTechnicalName++;
                }

                update.setString(1, newValue);
                update.setString(2, annotationId);
                update.setString(3, fileId);
                update.executeUpdate();
            }

        } finally {
            if (resultSet != null) {
                resultSet.close();
            }
            if (select != null) {
                select.close();
            }
            if (update != null) {
                update.close();
            }
        }

        return new int[]{rowsUpdatedWithTechnicalName, rowsHashed};
    }


    private static String getKey(String originalValue, String dossierTemplateId) {

        return originalValue + "|" + dossierTemplateId;
    }


    @Override
    public String getConfirmationMessage() {

        return "Technical name change applied to legalbasis fields in specified tables.";
    }


    @Override
    public void setUp() {}


    @Override
    public void setFileOpener(ResourceAccessor resourceAccessor) {}


    @Override
    public ValidationErrors validate(Database database) {

        return new ValidationErrors();
    }

}
