package ru.nbch.credit_tracker.service;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;
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_response.online_download.Inquiry;
import ru.nbch.credit_tracker.entities.external_response.online_download.Person;
import ru.nbch.credit_tracker.entities.external_response.online_download.Persons;
import ru.nbch.credit_tracker.entities.external_response.online_report.Report;
import ru.nbch.credit_tracker.entities.external_response.online_report.SgnlOnlineReport;
import ru.nbch.credit_tracker.entities.user_response.monitoring_signal_response.MonitoringSignalResponse;
import ru.nbch.credit_tracker.entities.user_response.monitoring_signal_response.Signal;
import ru.nbch.credit_tracker.entities.user_response.monitoring_signal_response.Signals;
import ru.nbch.credit_tracker.entities.user_response.monitoring_signal_response.Subject;
import ru.nbch.credit_tracker.service.signal.SignalRestService;
import ru.nbch.credit_tracker.service.xml.stax.MonitoringSignalResponseStaxXmlWriter;
import ru.nbch.credit_tracker.service.xml.stax.StaxXmlProcessor;
import ru.nbch.credit_tracker.utils.CTUtil;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Сервис периодического опроса официального сервиса сигналов на наличие новых отчетов по пакетам находящимся в статусе ON_MONITORING
 */
@Slf4j
@Service
public class MonitoringService {

    private final SignalRestService signalRestService;
    private final SignalService signalService;
    private final StaxXmlProcessor staxXmlProcessor;
    private final CreditTrackerSettings config;

    public MonitoringService(SignalRestService signalRestService,
                             SignalService signalService,
                             StaxXmlProcessor staxXmlProcessor,
                             CreditTrackerSettings config) {
        this.signalRestService = signalRestService;
        this.signalService = signalService;
        this.staxXmlProcessor = staxXmlProcessor;
        this.config = config;
    }

    public void run() {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new CustomizableThreadFactory("monitoring-"));
        long period = config.getMonitoringInterval() != null ? config.getMonitoringInterval() : 15;
        scheduler.scheduleAtFixedRate(this::monitor, 0, period, TimeUnit.MINUTES);
    }

    private void monitor() {
        log.info("Start monitoring");
        List<UserPackage> packages = signalService.findPackagesOnMonitoring();
        for (UserPackage userPackage : packages) {
            log.info("Processing package {}", userPackage.getPackageName());
            if (!signalService.hasNonNullfids(userPackage.getId())) {
                log.info("Package {} has no fids. Package will be skipped.", userPackage.getPackageName());
                continue;
            }
            SgnlOnlineReport onlineReports = signalRestService.getReports(userPackage.getUserId().substring(0, 6) + "_" + userPackage.getPackageName());

            if (onlineReports == null) {
                log.info("No reports found for package {}", userPackage.getPackageName());
                continue;
            }

            // Формируем список имен файлов, по которым сработали сигналы. Сюда входят только те имена, у которых парсится дата формирования отчета
            List<String> fileNames = onlineReports.getOperationReport().getReports().getReports().stream().map(Report::getFilename)
                    .filter(fileName -> CTUtil.parseDateFromReportFileName(fileName) != null).toList();

            if (fileNames.isEmpty()) {
                log.info("No new report found for package {}", userPackage.getPackageName());
                continue;
            }

            LocalDateTime lastDownloadedReportDate = CTUtil.parseDateFromReportFileName(userPackage.getLastDownloadedReport());

            if (lastDownloadedReportDate != null) {
                // Отбираем только те отчеты, дата которых позже даты последнего загруженного отчета
                fileNames = fileNames.stream()
                        .filter(fileName -> CTUtil.parseDateFromReportFileName(fileName).isAfter(lastDownloadedReportDate)) // отфильтровали только те имена, которые парсятся в дату, так что null тут не придет
                        .sorted(Comparator.comparing(CTUtil::parseDateFromReportFileName)) // Сортируем по дате
                        .toList();
            } else {
                fileNames = fileNames.stream()
                        .sorted(Comparator.comparing(CTUtil::parseDateFromReportFileName)) // Сортируем по дате
                        .toList();
            }

            if (StringUtils.isNotBlank(userPackage.getLastDownloadedReport())) {
                log.info("Last downloaded report found for package {}. Last Downloaded Report Name: {}", userPackage.getPackageName(), userPackage.getLastDownloadedReport());
            } else {
                log.info("Last Downloaded Report Name is empty");
            }

            for (String newReport : fileNames) {
                try {
                    String loadedReportPath = null;
                    if (StringUtils.isBlank(userPackage.getLastDownloadedReport())) {
                        loadedReportPath = loadReport(userPackage.getId(), newReport); // по этому пакету еще не было загруженных отчетов, продолжаем работу с полученным
                    } else if (lastDownloadedReportDate != null) {
                        LocalDateTime newReportDate = CTUtil.parseDateFromReportFileName(newReport);
                        if (newReportDate.isAfter(lastDownloadedReportDate)) {
                            loadedReportPath = loadReport(userPackage.getId(), newReport); // полученный отчет новее записанного в базу
                        }
                    }
                    if (loadedReportPath == null) {
                        log.info("Report was not loaded for package {}", userPackage.getPackageName());
                        continue;
                    }

                    String responsePath = generateResponse(userPackage, loadedReportPath);
                    String fileName = null;
                    if (responsePath != null) {
                        fileName = createResponseFile(userPackage, responsePath);
                    }
                    if (fileName != null) {
                        log.info("New report fileName: {}", fileName);
                        updateSignalHits(userPackage, responsePath, fileName);
                        signalService.updateLastDownloadedReport(userPackage.getId(), newReport);
                    } else {
                        log.info("Report was not created for package {}", userPackage.getPackageName());
                    }
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
        }
        log.info("Finish monitoring");
    }

    /**
     * Формирует результирующий xml файл для конечного пользователя на основе файла из официального сервиса сигналов
     * @return Путь к временному рещультирующему файлу
     */
    private String generateResponse(UserPackage userPackage, String loadedReportPath) {
        AtomicBoolean generateResponse = new AtomicBoolean(false); // Выставляется в true, если хотя бы один субъект с мониторинга подошел под условие срабатывания сигнала. В этом случае отчет сохраняется

        String folderPath = config.getLoadDir() + File.separator + "tmp_folder" + File.separator;
        String responsePath = folderPath + UUID.randomUUID() + "_response_output.xml";
        CTUtil.createFolder(folderPath);

        MonitoringSignalResponse response = new MonitoringSignalResponse();
        response.setPackageName(userPackage.getPackageName()); // Имя пакета, переданного при установке

        // Запись в результрующий файл
        MonitoringSignalResponseStaxXmlWriter staxXmlWriter = new MonitoringSignalResponseStaxXmlWriter(config.getEncoding());
        staxXmlWriter.writeWithStax(responsePath, response,
                writer -> {
                    try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(loadedReportPath)), 8 * 1024 * 1024)) {
                        staxXmlProcessor.read(in, Persons.class,
                                personsData -> { // как только считали пачку Persons сразу записали ее в результирующий файл
                                    List<Subject> subjectsBatch = processPersons(personsData.getPersons());
                                    if (!subjectsBatch.isEmpty()) {
                                        generateResponse.set(true);
                                    }
                                    writer.accept(subjectsBatch);
                                    personsData.getPersons().clear(); // чистим список для экономии памяти
                                },
                                pesons -> pesons.getPersons().isEmpty()
                                ,
                                10000,
                                "/Report/Persons/Person");
                    } catch (IOException e) {
                        throw new RuntimeException("Error while parsing nbki downloaded report", e);
                    }
                });


        CTUtil.removeFile(loadedReportPath);

        if (!generateResponse.get()) { // ни один субъект не подошел под условие, удаляем временный отчет
            CTUtil.removeFile(responsePath);
            return null;
        }

        return responsePath;
    }

    private List<Subject> processPersons(List<Person> persons) {
        List<Subject> subjects = new ArrayList<>();
        for (Person person : persons) {
            Signals signals = new Signals();
            for (Inquiry inquiry : person.getInquiries()) {
                // Отбираем только тех фл, у которых bus_category in (MFO, MKK, MFK) и есть сигналы с кодом 16
                if (!CTUtil.checkBusCategory(inquiry.getBusCategory()) || inquiry.getSignals().stream().noneMatch(signal -> signal.getN().equals(16))) {
                    continue;
                }
                Signal signal = new Signal();
                signal.setSignalType(1); // Код типа сигнала по внутреннему справочнику. В текущей версии: 1 = сигнал 16 от MFO/MKK/MFK
                signal.setInquiryDate(inquiry.getDate()); // Дата и время запроса КИ
                signal.setInquiryPurpose(inquiry.getInqP()); // Цель запроса КИ
                signals.getSignals().add(signal);
            }

            // Если есть хотя бы один заполненный сигнал, значит заполняем остальные данные
            if (!signals.getSignals().isEmpty()) {
                PhoneFidMap subjectData = signalService.findPhoneFidMapById(person.getUid());
                if (subjectData != null) {
                    Subject subject = new Subject();
                    subject.setId(subjectData.getSubjectId()); // Уникальный идентификатор субъекта (равен Id из XML-пакета при установке мониторинга)
                    subject.setClientId(subjectData.getPhone());
                    subject.setFlagDebt5000(subjectData.getFlagDebt5000());
                    subject.setFlag9012(subjectData.getFlag90days());
                    subject.setSignals(signals);
                    subjects.add(subject);
                }
            }
        }
        return subjects;
    }

    /**
     * Архивирует результирующий xml файл для пользователя
     * @return Путь к результрующему файлу. Пример <loadDir>/<member_code>/0001ZZ_MY_BATCH_TEST_0717_04_20250723_101929.zip
     */
    private String createResponseFile(UserPackage userPackage, String responsePath) {
        try {
            String fileName = CTUtil.generateResponseReportFileName(userPackage.getUserId(), userPackage.getPackageName());
            Thread.sleep(1000); // Если отчеты формируются слишком быстро, нужно подождать секунду, тк в имени отчета LocalDateTime до секунд
            String folderPath = config.getLoadDir() + File.separator + userPackage.getUserId().substring(0, 6) + File.separator;

            File folder = new File(folderPath);
            if (!folder.exists()) {
                folder.mkdir();
            }

            try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(responsePath)), 8 * 1024 * 1024);
                 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(folderPath + fileName + ".zip"));
                 ZipOutputStream zos = new ZipOutputStream(bos)) {

                ZipEntry zipEntry = new ZipEntry(fileName + ".xml");
                zos.putNextEntry(zipEntry);
                byte[] buffer = new byte[8 * 1024 * 1024];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) > 0) {
                    zos.write(buffer, 0, bytesRead);
                }
                zos.finish();
                zos.closeEntry();
            }
            return fileName;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (Exception e) {
            log.error("Ошибка при записи отчета пользователя в файл: ", e);
        }

        CTUtil.removeFile(responsePath);

        return null;
    }

    private void updateSignalHits(UserPackage userPackage, String responsePath, String reportFileName) {
        log.info("Updating signal hits for {}", userPackage.getPackageName());
        LocalDateTime responseSentAt = LocalDateTime.now();
        // Считываем результирующий файл пачками и обновляем таблицу hits
        try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(responsePath)), 8 * 1024 * 1024)) {
            staxXmlProcessor.read(in, MonitoringSignalResponse.class,
                    response -> { // как только считали пачку Subjects сразу обновлям таблицу hits
                        List<Hit> hits = new ArrayList<>();
                        response.getSubjects().getSubjects().forEach(subject -> {
                            subject.getSignals().getSignals().forEach(signal -> {
                                Hit hit = new Hit();
                                hit.setPackageId(userPackage.getId());
                                hit.setSubjectId(subject.getId());
                                hit.setSignalType(1);
                                hit.setResponseSentAt(responseSentAt);
                                hit.setReportFileName(reportFileName);
                                hits.add(hit);
                            });
                            if (!hits.isEmpty()) {
                                signalService.insertHits(hits);
                            }
                        });
                        response.getSubjects().getSubjects().clear(); // чистим список для экономии памяти
                    },
                    response -> response.getSubjects().getSubjects().isEmpty()
                    ,
                    10000,
                    "/MonitoringSignalResponse/Subjects/Subject");
        } catch (IOException e) {
            throw new RuntimeException("Error while and updating hits", e);
        }

        CTUtil.removeFile(responsePath); // удалям результирующий xml файл с диска (Он уже заархивирован к этому моменту)

        log.info("Signal hits updated for package {}", userPackage.getPackageName());
    }

    public String loadReport(Integer packageId, String reportName) {
        return signalRestService.downloadReport(packageId, reportName);
    }

}
