package ru.nbch.indicsearchservice.service;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
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.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.mutable.MutableLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import ru.nbch.indicsearchservice.aggregator.FileAggregator;
import ru.nbch.indicsearchservice.dto.request.AddSearchJobRequestDto;
import ru.nbch.indicsearchservice.dto.response.AddSearchJobResponseDto;
import ru.nbch.indicsearchservice.dto.response.JobStatusResponseDto;
import ru.nbch.indicsearchservice.exporter.ExcelExporter;
import ru.nbch.indicsearchservice.exporter.JsonExporter;
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.util.IndicSearchUtils;
import ru.nbch.indicsearchservice.util.MapperHolder;

@Service
public class IndicSearchService {
    private static final Logger log = LoggerFactory.getLogger(IndicSearchService.class);

    private static final boolean IS_EXPORT_AS_EXCEL = true;

    private final List<FirstLevelTableLoader> tableLoaders;
    private final List<FileAggregator> fileAggregators;
    private final ExcelExporter excelExporter;
    private final JsonExporter jsonExporter;
    private final SettingsService settingsService;
    private final LocalizationObjectMapper localizationObjectMapper;
    private final SettingsMapper settingsMapper;
    private final MapperHolder mapperHolder;
    private final IndicSearchUtils indicSearchUtils;

    public IndicSearchService(List<FirstLevelTableLoader> tableLoaders,
                              List<FileAggregator> fileAggregators,
                              ExcelExporter excelExporter,
                              JsonExporter jsonExporter,
                              SettingsService settingsService,
                              LocalizationObjectMapper localizationObjectMapper,
                              SettingsMapper settingsMapper,
                              MapperHolder mapperHolder,
                              IndicSearchUtils indicSearchUtils) {
        this.tableLoaders = tableLoaders;
        this.fileAggregators = fileAggregators;
        this.excelExporter = excelExporter;
        this.jsonExporter = jsonExporter;
        this.settingsService = settingsService;
        this.localizationObjectMapper = localizationObjectMapper;
        this.settingsMapper = settingsMapper;
        this.mapperHolder = mapperHolder;
        this.indicSearchUtils = indicSearchUtils;
    }

    public AddSearchJobResponseDto addSearchJob(AddSearchJobRequestDto request) {
        UUID taskId = UUID.randomUUID();

        StopWatch sw = new StopWatch();

        // Подготовительный этап. Получение мета данных из БД
        log.info("Prep step started. Getting metadata from database");
        sw.start();

        Map<String, TableMetaData> tableMetaDataMap = prepareTableMetaData(request.getDataType(), request.getSearchParameter());
        long rowLimit = Long.parseLong(settingsService.getSettingsValueByName("LimitDB"));

        sw.stop();
        log.info(sw.prettyPrint());
        sw = new StopWatch();

        // Первый этап. Получение данных из БД и заполнение файлов
        log.info("First step started. Getting data from database and filling csv files");
        sw.start();

        Map<String, Path> tableFileMap = new ConcurrentHashMap<>();
        tableLoaders.parallelStream().forEach(tl -> tl.searchBy(
            taskId,
            request.getDataType(),
            request.getSearchParameter(),
            request.getSearchValue(),
            request.getMemberCode().substring(0, 6),
            tableFileMap,
            tableMetaDataMap
        ));

        sw.stop();
        log.info(sw.prettyPrint());
        sw = new StopWatch();

        // Второй этап. Объединение файлов в один
        log.info("Second step started. Aggregating table files into one.");
        sw.start();

        MutableLong rowCounter = new MutableLong();
        Path aggregatedFile = fileAggregators.stream().map(fa -> fa.aggregateFiles(
                taskId,
                request.getDataType(),
                request.getSearchParameter(),
                request.getSearchValue(),
                request.getMemberCode().substring(0, 6),
                tableFileMap,
                rowCounter))
            .findFirst()
            .orElse(null);

        sw.stop();
        log.info(sw.prettyPrint());
        sw = new StopWatch();

        for (Path tableFilePath : tableFileMap.values()) {
            try {
                Files.deleteIfExists(tableFilePath);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        // Третий этап. Экспорт агрегированного файла в JSON или XLSX
        log.info("Third step started. Exporting aggregated file into JSON/XLSX.");
        sw.start();

        if (rowCounter.longValue() < rowLimit && !IS_EXPORT_AS_EXCEL) {
            jsonExporter.writeJsonFile(aggregatedFile, taskId, tableMetaDataMap);
        } else {
            fillTableMetaDataWithLocalization(tableMetaDataMap);
            excelExporter.writeExcelFile(aggregatedFile, taskId, rowCounter.longValue(), tableMetaDataMap);
        }

        sw.stop();
        log.info(sw.prettyPrint());

//        try {
//            Files.deleteIfExists(aggregatedFile);
//        } catch (IOException e) {
//            throw new RuntimeException(e);
//        }

        return null;
    }

    public JobStatusResponseDto getJobStatus(Long id) {
        return null;
    }

    public ResponseEntity<?> export(Long id, String exportType) {
        return null;
    }

    private 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.getOrDefault(key, Optional.empty());

            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);
            }
        }

        return tableMetaDataMap;
    }

    private 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.getOrDefault(tableKey, Optional.empty());
            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.getOrDefault(primaryKey, Optional.empty());

                if (primaryLoc.isPresent() && StringUtils.isNotBlank(primaryLoc.get())) {
                    metaData.addLocalization(field, primaryLoc.get());
                } else {
                    Optional<String> fallbackLoc = localizationValues.getOrDefault(fallbackKey, Optional.empty());
                    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);
        }
    }
}
