package ru.nbch.indicsearchservice.util;

import java.sql.Connection;
import java.sql.JDBCType;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.sql.DataSource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Component;
import ru.nbch.indicsearchservice.loader.FirstLevelTableLoader;
import ru.nbch.indicsearchservice.model.LocalizationObject;
import ru.nbch.indicsearchservice.model.Settings;
import ru.nbch.indicsearchservice.model.TableMetaData;
import ru.nbch.indicsearchservice.model.enums.DataType;
import ru.nbch.indicsearchservice.model.enums.SearchParameter;
import ru.nbch.indicsearchservice.repository.bank.LocalizationObjectMapper;
import ru.nbch.indicsearchservice.repository.bank.SettingsMapper;
import ru.nbch.indicsearchservice.service.SettingsService;

@Component
public class TableMetaDataUtil {
    private final List<FirstLevelTableLoader> tableLoaders;
    private final SettingsService settingsService;
    private final MapperHolder mapperHolder;
    private final SettingsMapper settingsMapper;
    private final LocalizationObjectMapper localizationObjectMapper;
    private final IndicSearchUtils indicSearchUtils;
    private final DataSource indicDataSource;

    public TableMetaDataUtil(List<FirstLevelTableLoader> tableLoaders,
                             SettingsService settingsService,
                             MapperHolder mapperHolder,
                             SettingsMapper settingsMapper,
                             LocalizationObjectMapper localizationObjectMapper,
                             IndicSearchUtils indicSearchUtils,
                             @Qualifier("indicDataSource") DataSource indicDataSource) {
        this.tableLoaders = tableLoaders;
        this.settingsService = settingsService;
        this.mapperHolder = mapperHolder;
        this.settingsMapper = settingsMapper;
        this.localizationObjectMapper = localizationObjectMapper;
        this.indicSearchUtils = indicSearchUtils;
        this.indicDataSource = indicDataSource;
    }

    public Map<String, TableMetaData> prepareTableMetaData(DataType dataType, SearchParameter searchParameter) {
        Map<String, TableMetaData> tableMetaDataMap = new ConcurrentHashMap<>();

        // Получаем список таблиц
        Set<String> tableNames = tableLoaders.parallelStream()
            .map(tl -> tl.getTableNamesIfSuitable(dataType, searchParameter))
            .flatMap(Set::stream)
            .collect(Collectors.toSet());

        if (tableNames.isEmpty()) {
            return tableMetaDataMap;
        }

        tableNames.forEach(name -> tableMetaDataMap.put(name, new TableMetaData(name)));

        // Составляем массив ключей из SETTINGS
        Map<String, String> keyToTable = new HashMap<>();
        List<String> columnKeys = new ArrayList<>();
        for (String table : tableNames) {
            String key = "db.table." + table.toUpperCase() + ".fields";
            columnKeys.add(key);
            keyToTable.put(key, table);
        }

        // Получаем данные из SETTINGS одним запросом
        Map<String, Optional<String>> settingsValues = settingsService.getSettingsValuesByNames(columnKeys);
        Set<String> missingTables = new HashSet<>();
        List<Settings> batchInsertSettings = new ArrayList<>();

        for (String key : columnKeys) {
            String table = keyToTable.get(key);
            Optional<String> valueOpt = settingsValues.get(key);

            if (valueOpt.isPresent() && StringUtils.isNotBlank(valueOpt.get())) {
                String[] fields = valueOpt.get().split(";");
                for (String field : fields) {
                    tableMetaDataMap.get(table).addField(field.trim());
                }
            } else {
                missingTables.add(table);
            }
        }

        // Если найдены пропущенные значения, то добавляем их в БД
        if (!missingTables.isEmpty()) {
            List<LinkedHashMap<String, String>> rows =
                mapperHolder.getIndicMapper().getTableColumnsByTableNames(new ArrayList<>(missingTables));

            Map<String, List<String>> tableToColumns = new HashMap<>();
            for (Map<String, String> row : rows) {
                tableToColumns.computeIfAbsent(row.get("TBNAME"), t -> new ArrayList<>()).add(row.get("NAME"));
            }

            for (String table : missingTables) {
                List<String> columns = tableToColumns.getOrDefault(table, List.of());
                String key = "db.table." + table.toUpperCase() + ".fields";
                String value = String.join(";", columns);

                Settings newSettings = new Settings();
                newSettings.setName(key);
                newSettings.setValue(value);
                batchInsertSettings.add(newSettings);

                for (String column : columns) {
                    tableMetaDataMap.get(table).addField(column.trim());
                }
            }

            if (!batchInsertSettings.isEmpty()) {
                settingsMapper.insertBatch(batchInsertSettings);
            }
        }

        // Получаем размеры некоторых полей
        for (Map.Entry<String, List<String>> entry : IndicSearchCommon.getCastableFields().entrySet()) {
            String tableName = entry.getKey();
            List<String> tableFields = entry.getValue();

            if (!tableMetaDataMap.containsKey(tableName)) {
                continue;
            }

            try (Connection connection = DataSourceUtils.getConnection(indicDataSource);
                 ResultSet resultSet = connection.getMetaData().getColumns(null, null, tableName, null)) {
                while (resultSet.next()) {
                    String columnName = resultSet.getString("COLUMN_NAME");
                    if (tableFields.contains(columnName) && tableMetaDataMap.get(tableName).getFieldMetaData().containsKey(columnName)) {
                        TableMetaData.FieldMetaData foundFieldMetaData = tableMetaDataMap.get(tableName).getFieldMetaData().get(columnName);
                        foundFieldMetaData.setJdbcType(JDBCType.valueOf(resultSet.getInt("DATA_TYPE")));
                        foundFieldMetaData.setColumnSize(resultSet.getInt("COLUMN_SIZE"));
                    }
                }

                DataSourceUtils.releaseConnection(connection, indicDataSource);
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }

        return tableMetaDataMap;
    }

    public void fillTableMetaDataWithLocalization(Map<String, TableMetaData> tableMetaDataMap) {
        if (tableMetaDataMap == null || tableMetaDataMap.isEmpty()) {
            return;
        }

        // Получаем все ключи для LOCALIZATION_OBJECT
        Set<String> localizationKeys = new HashSet<>();
        for (TableMetaData metaData : tableMetaDataMap.values()) {
            String tableUpper = metaData.getTableName().toUpperCase();
            localizationKeys.add("db.viewer.table." + tableUpper);

            for (String field : metaData.getFieldMetaData().keySet()) {
                String fieldUpper = field.toUpperCase();
                localizationKeys.add("db.viewer." + tableUpper + ".field." + fieldUpper);
                localizationKeys.add("db.viewer.field." + fieldUpper);
            }
        }

        // Получаем данные из LOCALIZATION_OBJECT одним запросом
        Map<String, Optional<String>> localizationValues = indicSearchUtils.getOptionalLocalizationStrings(new ArrayList<>(localizationKeys));
        List<LocalizationObject> toInsertLocalizations = new ArrayList<>();

        // Вставляем локализацию в tableMetaDataMap
        for (TableMetaData metaData : tableMetaDataMap.values()) {
            String tableUpper = metaData.getTableName().toUpperCase();
            String tableKey = "db.viewer.table." + tableUpper;

            // Локализация для таблиц
            Optional<String> tableLoc = localizationValues.get(tableKey);
            if (tableLoc.isPresent() && StringUtils.isNotBlank(tableLoc.get())) {
                metaData.setTableNameLocalization(tableLoc.get());
            } else {
                String fallbackText = "Таблица " + tableUpper;
                metaData.setTableNameLocalization(fallbackText);

                LocalizationObject newLocalizationObject = new LocalizationObject();
                newLocalizationObject.setCode(tableKey);
                newLocalizationObject.setText(fallbackText);
                toInsertLocalizations.add(newLocalizationObject);
            }

            // Локализация для полей
            for (String field : metaData.getFieldMetaData().keySet()) {
                String fieldUpper = field.toUpperCase();
                String primaryKey = "db.viewer." + tableUpper + ".field." + fieldUpper;
                String fallbackKey = "db.viewer.field." + fieldUpper;

                Optional<String> primaryLoc = localizationValues.get(primaryKey);

                if (primaryLoc.isPresent() && StringUtils.isNotBlank(primaryLoc.get())) {
                    metaData.addLocalization(field, primaryLoc.get());
                } else {
                    Optional<String> fallbackLoc = localizationValues.get(fallbackKey);
                    if (fallbackLoc.isPresent() && StringUtils.isNotBlank(fallbackLoc.get())) {
                        metaData.addLocalization(field, fallbackLoc.get());
                    } else {
                        String fallbackText = "Поле " + fieldUpper;
                        metaData.addLocalization(field, fallbackText);

                        LocalizationObject newLocalizationObject = new LocalizationObject();
                        newLocalizationObject.setCode(primaryKey);
                        newLocalizationObject.setText(fallbackText);
                        toInsertLocalizations.add(newLocalizationObject);
                    }
                }
            }
        }

        // Если найдены пропущенные значения, то добавляем их в БД
        if (!toInsertLocalizations.isEmpty()) {
            localizationObjectMapper.insertBatch(toInsertLocalizations);
        }
    }
}
