package ru.nbch.credit_tracker.service;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.builder.PersonFactoryBuilder;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.dto.Hit;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.external_request.signals.Auth;
import ru.nbch.credit_tracker.entities.external_request.signals.Person;
import ru.nbch.credit_tracker.entities.external_request.signals.Signals;
import ru.nbch.credit_tracker.entities.external_response.monitoring_report_error.MonitoringReportErrors;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.service.indic.FlagsService;
import ru.nbch.credit_tracker.service.indic.FlagsService2;
import ru.nbch.credit_tracker.service.indic.PersonService;
import ru.nbch.credit_tracker.service.indic.PhoneService2;
import ru.nbch.credit_tracker.service.signal.HitsService;
import ru.nbch.credit_tracker.service.signal.PhoneFidMapService;
import ru.nbch.credit_tracker.service.signal.UserPackageService;
import ru.nbch.credit_tracker.service.xml.stax.SignalsStaxXmlWriter;
import ru.nbch.credit_tracker.service.xml.stax.StaxXmlProcessor;
import ru.nbch.credit_tracker.utils.CTUtil;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;

@Slf4j
@Service
public class SignalService {

    private final CreditTrackerSettings config;
    private final HitsService hitsService;
    private final UserPackageService userPackageService;
    private final PhoneFidMapService phoneFidMapService;
    private final PhoneService2 phoneService;
    private final FlagsService2 flagsService;
    private final PersonService personService;
    private final StaxXmlProcessor staxXmlProcessor;

    private final int batchSize = 10000;

    public SignalService(CreditTrackerSettings config,
                         HitsService hitsService,
                         UserPackageService userPackageService,
                         PhoneFidMapService phoneFidMapService,
                         PhoneService2 phoneService,
                         FlagsService2 flagsService,
                         PersonService personService,
                         StaxXmlProcessor staxXmlProcessor) {
        this.config = config;
        this.hitsService = hitsService;
        this.userPackageService = userPackageService;
        this.phoneFidMapService = phoneFidMapService;
        this.phoneService = phoneService;
        this.flagsService = flagsService;
        this.personService = personService;
        this.staxXmlProcessor = staxXmlProcessor;
    }

    public boolean isPackageNotUnique(MonitoringRequest request) {
        return userPackageService.isPackageNotUnique(request.getUserId(), request.getPackageName());
    }

    public Integer registerPackage(MonitoringRequest request) {
        UserPackage userPackage = UserPackage.builder()
                .userId(request.getUserId())
                .packageName(request.getPackageName())
                .createdAt(LocalDateTime.now())
                .status(Status.PROCESSING.name())
                .build();
        return userPackageService.insertPackage(userPackage);
    }

    public void registerSubjects(String filePath, Integer packageId) {
        log.info("Start registering subjects. PackageId: {}", packageId);
        try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(filePath)), 8 * 1024 * 1024)) {
            staxXmlProcessor.read(in, MonitoringRequest.class,
                    request -> {
                        if (!request.getSubjects().getSubject().isEmpty()) {
                            phoneFidMapService.registerSubjects(request.getSubjects().getSubject(), packageId);
                            log.trace("Registered subject's batch. Total count: {}. PackageId: {}", request.getSubjects().getSubject().size(), packageId);
                            request.getSubjects().getSubject().clear();
                        }
                    },
                    request -> request.getSubjects().getSubject().isEmpty()
                    ,
                    batchSize, "/MonitoringRequest/Subjects/Subject");
        } catch (IOException e) {
            throw new RuntimeException("Error while parsing monitoring request", e);
        }
        log.info("Subjects were registered. PackageId: {}", packageId);
    }

    /**
     * Обрабатывает все PhoneFidMap по packageId, выгружая их пачками и фильтруя по переданному фильтру
     */
    public UpdateResult updateSubjects(Integer packageId, Predicate<PhoneFidMap> filter) {
        boolean updated = false;
        boolean noFidsFound = true;
        int offset = 0;
        List<PhoneFidMap> batch;
        Map<Integer, PhoneFidMap> phonesByFidToProcessing = new HashMap<>();
        do {
            batch = phoneFidMapService.findFullPhoneFidMap(packageId, offset, batchSize);

            List<PhoneFidMap> filteredItems;
            if (filter != null) {
                filteredItems = batch.stream().filter(filter).toList();
            } else {
                filteredItems = batch;
            }

            if (!updated && !filteredItems.isEmpty()) {
                updated = true;
            }

            if (!filteredItems.isEmpty()) {
                Map<Integer, PhoneFidMap> fidsMapByBatch = phoneService.defineFidByPhone(filteredItems);
                log.debug("Actual fids size by phone's batch: {}", fidsMapByBatch.size());

                if (fidsMapByBatch.size() + phonesByFidToProcessing.size() <= batchSize) {
                    phonesByFidToProcessing.putAll(fidsMapByBatch);
                } else {
                    boolean fidsFound = updateFlags(phonesByFidToProcessing, packageId);
                    if (noFidsFound) {
                        noFidsFound = !fidsFound;
                    }
                    phonesByFidToProcessing = new HashMap<>(fidsMapByBatch);
                }
            }

            offset += batchSize;
        } while (!batch.isEmpty());

        if (!phonesByFidToProcessing.isEmpty()) {
            boolean fidsFound = updateFlags(phonesByFidToProcessing, packageId);
            if (noFidsFound) {
                noFidsFound = !fidsFound;
            }
        }

        return new UpdateResult(updated, noFidsFound);
    }

    /**
     * Обрабатывает пачку PhoneFidMap: обновляет фиды и флаги просрочек
     * @return fidsFound true - если хотя бы по одному номеру был найден фид, false - в противном случае
     */
    // TODO Добавить отдельный метод. Если не нашелся фид, надо обновить в базе fid = null и флаги = null calculated_at=now()
    public boolean updateFlags(Map<Integer, PhoneFidMap> fidsMap, Integer packageId) {
        log.info("Start calculating flags for subjects batch. PackageId: {}", packageId);
        Map<Integer, FlagsService.FlagsData> flagsDataMap = flagsService.findFlags(fidsMap.keySet().stream().toList());
        List<PhoneFidMap> subjectToUpdate = new ArrayList<>();
        for (Map.Entry<Integer, FlagsService.FlagsData> entry : flagsDataMap.entrySet()) {
            Integer fid = entry.getKey();
            FlagsService.FlagsData flagsData = entry.getValue();
            PhoneFidMap phoneFidMap = fidsMap.get(fid);

            phoneFidMap.setFid(fid);
            phoneFidMap.setFlagDebt5000(flagsData.getFlagDebt5000());
            phoneFidMap.setFlag90days(flagsData.getFlag90days());
            phoneFidMap.setCalculatedAt(LocalDateTime.now());

            subjectToUpdate.add(phoneFidMap);
        }

        log.debug("Start updating subject's batch");
        if (!subjectToUpdate.isEmpty()) {
            int updateBatchSize = 7000; // 9 параметров на одну запись для обновления. Максимальное количество параметров ~65000
            for (int j = 0; j < subjectToUpdate.size(); j += updateBatchSize) {
                int endIndex = Math.min(j + updateBatchSize, subjectToUpdate.size());
                phoneFidMapService.updatePhoneFidMaps(subjectToUpdate.subList(j, endIndex));
                log.trace("Updated fids and flags for subject's batch from: {} to: {}. Total count: {}. PackageId: {}", j, endIndex, subjectToUpdate.size(), packageId);
            }
        }
        log.debug("Complete updating subject's batch");

        log.info("Finish calculating flags for subjects. PackageId: {}. Batch size: {}", packageId, fidsMap.size());
        return !fidsMap.isEmpty();
    }

    public void rejectPackage(MonitoringRequest request) {
        UserPackage userPackage = UserPackage.builder()
                .userId(request.getUserId())
                .packageName(request.getPackageName())
                .createdAt(LocalDateTime.now())
                .status(Status.REJECTED.name())
                .build();
        userPackageService.insertPackage(userPackage);
    }

    public void updatePackageStatus(Integer packageId, Status status) {
        userPackageService.updatePackageStatus(packageId, status);
    }

    /**
     * Формирует файл запроса в официальный сервис сигналов, выгружая паспортные данные пачками и сразу записывая их в файл
     *
     * @return Путь к файлу запроса к официальному сервису сигналов на постановку на мониторинг
     */
    public String writeSignals(Integer packageId) {
        String folderPath = config.getLoadDir() + File.separator + "tmp_folder" + File.separator;
        String filePath = folderPath + UUID.randomUUID() + "_output.xml";
        CTUtil.createFolder(folderPath);

        Signals signals = buildSignals(packageId);

        SignalsStaxXmlWriter signalsStaxXmlWriter = new SignalsStaxXmlWriter(config.getEncoding());

        signalsStaxXmlWriter.writeWithStax(filePath,
                signals,
                writer -> buildPersons(packageId, writer));

        return filePath;
    }

    private Signals buildSignals(Integer packageId) {
        UserPackage userPackage = userPackageService.findPackage(packageId);
        Signals signals = new Signals();
        signals.setOnline("2"); // Всегда=2 (только онлайн-сигналы)
        signals.setProcessWithErrors("1"); // Всегда=1 (обработать пакет полностью, по обнаруженным ошибкам (при наличии) сформировать файл отказа, записи без ошибок установить на мониторинг)
        signals.setId(userPackage.getUserId().substring(0, 6) + "_" + userPackage.getPackageName()); // <MEMBERCODE>_<PackageName>
        Auth auth = new Auth();
        auth.setUser(config.getSignalsuser());
        signals.setAuth(auth);
        return signals;
    }

    /**
     * Обрабатывает все PhoneFidMap по packageId, выгружая их пачками и фильтруя по переданному фильтру.
     * writer записывает пачку сразу в файл
     */
    private void buildPersons(Integer packageId, Consumer<List<Person>> writer) {
        int offset = 0;
        List<PhoneFidMap> batch;
        do {
            batch = phoneFidMapService.findFullPhoneFidMap(packageId, offset, batchSize);
            List<Person> persons = findPersons(batch);
            if (!persons.isEmpty()) {
                writer.accept(persons); // записывает пачку Persons в файл
            }
            offset += batchSize;
        } while (!batch.isEmpty());

    }

    /**
     * Формирует список Person для постановки на мониторинг в официальный сервис сигналов на основе списка PhoneFidMap
     */
    private List<Person> findPersons(List<PhoneFidMap> subjects) {
        if (subjects.isEmpty()) {
            return Collections.emptyList();
        }
        List<Person> persons = new ArrayList<>();
        Map<Integer, PersonService.PersonData> personFidMap = personService.findPersonData(subjects.stream()
                .map(PhoneFidMap::getFid)
                .filter(Objects::nonNull).toList());
        subjects.forEach(data -> {
            if (data.getFid() != null) {
                PersonService.PersonData personData = personFidMap.get(data.getFid());
                Person person = PersonFactoryBuilder.builder()
                        .personData(personData)
                        .uid(data.getId())
                        .build();
                if (person != null) {
                    persons.add(person);
                }
            }
        });
        return persons;
    }

    public void updateSubjectsErrors(String filePath, Integer packageId) {
        log.info("Start updating subjects errors. PackageId: {}", packageId);
        try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(filePath)), 8 * 1024 * 1024)) {
            staxXmlProcessor.read(in, MonitoringReportErrors.class,
                    request -> {
                        if (!request.getPersons().isEmpty()) {
                            List<PhoneFidMap> subjects = request.getPersons().stream().map(p -> {
                                log.error("Ошибка при формировании субъекта. package_id = {}, id = {}. Описание: {}", packageId, p.getUid(), p.getValue());
                                return PhoneFidMap.builder()
                                        .id(p.getUid())
                                        .errorText(p.getValue())
                                        .build();
                            }).toList();
                            phoneFidMapService.updatePhoneFidMapsErrors(subjects);
                            log.trace("Updated subject's batch. Total count: {}. PackageId: {}", request.getPersons().size(), packageId);
                            request.getPersons().clear();
                        }
                    },
                    request -> request.getPersons().isEmpty()
                    ,
                    batchSize, "/Errors/p");
        } catch (IOException e) {
            throw new RuntimeException("Error while parsing MonitoringReportErrors", e);
        }
        log.info("Subject's errors were updated. PackageId: {}", packageId);
    }

    public List<UserPackage> findActivePackages(String userId) {
        return userPackageService.findUserPackages(userId, Status.ON_MONITORING);
    }

    public List<UserPackage> findAllPackages(String userId) {
        return userPackageService.findAllPackagesByUserId(userId);
    }

    public boolean isPackageExists(String username, String packageName) {
        return userPackageService.isPackageExists(username, packageName, Status.ON_MONITORING);
    }

    public boolean isPackagesByStatusExist(String username, String packageName, Status status) {
        return userPackageService.hasActivePackages(username, packageName, status);
    }

    public void deletePackage(String username, String packageName, LocalDateTime deactivatedAt) {
        userPackageService.deletePackage(username, packageName, Status.DELETED, Status.ON_MONITORING, deactivatedAt);
    }

    public List<UserPackage> findPackagesOnMonitoring() {
        return userPackageService.findAllPackagesByStatus(Status.ON_MONITORING);
    }

    public List<UserPackage> findPackagesOnProcessing() {
        return userPackageService.findAllPackagesByStatus(Status.PROCESSING);
    }

    public void insertHits(List<Hit> hits) {
        hitsService.insertHits(hits);
    }

    public void updateLastDownloadedReport(Integer id, String fileName) {
        userPackageService.updatePackageLastDownloadedReport(id, fileName);
    }

    public PhoneFidMap findPhoneFidMapById(Integer id) {
        return phoneFidMapService.findPhoneFidMapById(id);
    }

    public boolean hasNonNullfids(Integer packageId) {
        return phoneFidMapService.hasNonNullfids(packageId);
    }

    public UserPackage findPackage(Integer packageId) {
        return userPackageService.findPackage(packageId);
    }

    @Getter
    @AllArgsConstructor
    public static class UpdateResult {
        private boolean updated;
        private boolean noFidsFound;
    }
}
