package ru.nbch.credit_tracker.facade;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.user_request.active_packages.MonitoringActivePackagesRequest;
import ru.nbch.credit_tracker.entities.user_request.all_packages.MonitoringAllPackagesRequest;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.entities.user_response.active_packages.MonitoringActivePackagesResponse;
import ru.nbch.credit_tracker.entities.user_response.active_packages.Package;
import ru.nbch.credit_tracker.entities.user_response.active_packages.Packages;
import ru.nbch.credit_tracker.entities.user_response.all_packages.MonitoringAllPackagesResponse;
import ru.nbch.credit_tracker.entities.user_response.all_packages.PackageAll;
import ru.nbch.credit_tracker.entities.user_response.all_packages.PackagesAll;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringPackageDeleteResponse;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringReportListResponse;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringSetupResponse;
import ru.nbch.credit_tracker.enums.SignalError;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.enums.XmlErrorFormat;
import ru.nbch.credit_tracker.exceptions.SignalClientException;
import ru.nbch.credit_tracker.service.MonitoringErrorProcesingService;
import ru.nbch.credit_tracker.service.SignalService;
import ru.nbch.credit_tracker.service.scoring.AuthorizationService;
import ru.nbch.credit_tracker.service.signal.SignalRestService;
import ru.nbch.credit_tracker.service.xml.stax.StaxXmlProcessor;
import ru.nbch.credit_tracker.utils.CTUtil;
import ru.nbch.credit_tracker.utils.ValidationUtil;
import ru.nbch.credit_tracker.utils.ZipUtils;

import javax.xml.stream.XMLStreamException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static ru.nbch.credit_tracker.utils.error.ExceptionUtils.clientExcp;
import static ru.nbch.credit_tracker.utils.error.ExceptionUtils.serverExcp;

@Slf4j
@Service
public class SignalFacade {
    private final CreditTrackerSettings config;
    private final AuthorizationService authorizationService;
    private final SignalService signalService;
    private final SignalRestService signalRestService;
    private final MonitoringErrorProcesingService monitoringErrorProcesingService;
    private final StaxXmlProcessor staxXmlProcessor;

    private final ExecutorService executor;
    private final long maxXmlFileSize;


    public SignalFacade(CreditTrackerSettings config,
                        AuthorizationService authorizationService,
                        SignalService signalService,
                        SignalRestService signalRestService,
                        MonitoringErrorProcesingService monitoringErrorProcesingService,
                        StaxXmlProcessor staxXmlProcessor,
                        @Qualifier("executorService") ExecutorService executor) {
        this.config = config;
        this.authorizationService = authorizationService;
        this.signalService = signalService;
        this.signalRestService = signalRestService;
        this.monitoringErrorProcesingService = monitoringErrorProcesingService;
        this.staxXmlProcessor = staxXmlProcessor;
        this.executor = executor;
        this.maxXmlFileSize = config.getMaxXmlFileSize() * 1024 * 1024;
    }

    public ResponseEntity<MonitoringSetupResponse> setupMonitoring(MultipartFile file) {
        // первичные валидации
        if (file.isEmpty() || !isZipFile(file)) {
            throw clientExcp(SignalError.ERROR_001, XmlErrorFormat.MonitoringError);
        }

        // Валидация Архива на количетсво файлов, размер и расширение
        MonitoringRequest monitoringRequest;
        Integer packageId;
        ZipInputStream zipInputStream = null;
        String filePath;
        try {
            zipInputStream = new ZipInputStream(file.getInputStream());
            Optional<ZipEntry> xmlFileEntry = ZipUtils.extractValidEntry(zipInputStream);
            if (xmlFileEntry.isEmpty()) {
                throw clientExcp(SignalError.ERROR_001, XmlErrorFormat.MonitoringError);
            }
            if (xmlFileEntry.get().getSize() > maxXmlFileSize) {
                throw clientExcp(SignalError.ERROR_009, XmlErrorFormat.MonitoringError);
            }

            zipInputStream.close();
            zipInputStream = new ZipInputStream(file.getInputStream());
            zipInputStream.getNextEntry();

            filePath = writeTmpFile(zipInputStream);
        } catch (Exception e) {
            if (e instanceof SignalClientException) {
                throw (SignalClientException) e;
            }
            throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
        } finally {
            if (zipInputStream != null) {
                try {
                    zipInputStream.close();
                } catch (IOException e) {
                    throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
                }
            }
        }


        // валидация xml
        try {
            monitoringRequest = readAndValidateMonitoringRequest(filePath);
        } catch (Exception e) {
            CTUtil.removeFile(filePath);
            if (e.getCause() instanceof XMLStreamException) {
                throw clientExcp(SignalError.ERROR_011, XmlErrorFormat.MonitoringError);
            }
            if (e instanceof SignalClientException) {
                throw (SignalClientException) e;
            }
            throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
        }

        try {
            // проверка авторизации
            AuthorizationService.AuthResult authResult = authorizationService.findAuthInfo(monitoringRequest.getUserId(), monitoringRequest.getPassword());
            if (!authResult.isSuccess()) {
                signalService.rejectPackage(monitoringRequest);
                throw clientExcp(authResult.getError(), XmlErrorFormat.MonitoringError);
            }

            // проверка на уникальность пакета
            if (signalService.isPackageNotUnique(monitoringRequest)) {
                signalService.rejectPackage(monitoringRequest);
                throw clientExcp(SignalError.ERROR_007, XmlErrorFormat.MonitoringError);
            }

            packageId = signalService.registerPackage(monitoringRequest);
            log.info("Package registered. PackageId {}. Status: {}", packageId, Status.PROCESSING.name());
        } catch (Exception e) {
            CTUtil.removeFile(filePath);
            if (e instanceof SignalClientException) {
                throw (SignalClientException) e;
            }
            throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
        }

        // в отдельном потоке запускаем регистрацию субъектов, поиск фидов и расчет флагов просрочек
        registerFidsAndDebts(packageId, monitoringRequest.getPackageName(), filePath);

        return ResponseEntity.ok().body(new MonitoringSetupResponse());
    }

    private MonitoringRequest readAndValidateMonitoringRequest(String filePath) {
        MonitoringRequest monitoringRequest;
        try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(filePath)), 8 * 1024 * 1024)) {
            monitoringRequest = staxXmlProcessor.read(in, MonitoringRequest.class,
                    request -> {
                        try {
                            ValidationUtil.validateMonitoringRequest(request);
                        } catch (SignalClientException e) {
                            if (request != null) {
                                signalService.rejectPackage(request);
                            }
                            request = null;
                            throw e;
                        }
                        request.getSubjects().getSubject().clear();
                    },
                    request -> request.getSubjects().getSubject().isEmpty()
                    ,
                    40000, "/MonitoringRequest/Subjects/Subject");
        } catch (IOException e) {
            throw new RuntimeException("Error while parsing and validating monitoring request", e);
        }
        return monitoringRequest;
    }

    private String writeTmpFile(ZipInputStream zipInputStream) throws IOException {
        String folderPath = config.getLoadDir() + File.separator + "tmp_folder" + File.separator;
        String filePath = folderPath + UUID.randomUUID() + "monitoring_request.xml";
        CTUtil.createFolder(folderPath);

        try (OutputStream out = Files.newOutputStream(Path.of(filePath))) {

            byte[] buffer = new byte[8 * 1024 * 1024];
            int bytesRead;
            while ((bytesRead = zipInputStream.read(buffer)) > 0) {
                out.write(buffer, 0, bytesRead);
            }

            return filePath;
        }
    }

    private void registerFidsAndDebts(Integer packageId, String packageName, String filePath) {
        executor.execute(() -> {
            try {
                signalService.registerSubjects(filePath, packageId);
                CTUtil.removeFile(filePath);
                SignalService.UpdateResult updateResult = signalService.updateSubjects(packageId, null);
                if (updateResult.isNoFidsFound()) {
                    log.info("No fids found for PackageId: {} PackageName: {}. Package status ON_MONITORING. No registration in signal service.", packageId, packageName);
                    signalService.updatePackageStatus(packageId, Status.ON_MONITORING);
                    return;
                }
                if (signalRestService.setupMonitoring(packageId)) {
                    log.info("Package was successfully registered in signal service. PackageId: {} PackageName: {}", packageId, packageName);
                    signalService.updatePackageStatus(packageId, Status.ON_MONITORING);
                    monitoringErrorProcesingService.findErrors(packageId);
                } else {
                    log.info("Package registration was rejected by signal service. PackageId: {} PackageName: {}", packageId, packageName);
                    signalService.updatePackageStatus(packageId, Status.ERROR);
                }
            } catch (Exception e) {
                CTUtil.removeFile(filePath);
                signalService.updatePackageStatus(packageId, Status.ERROR);
                log.error("Ошибка при поиске фидов или расчете флагов просрочек. Package_id {}", packageId, e);
            }
        });
    }

    private boolean isZipFile(MultipartFile file) {
        return file.getContentType() != null &&
                (file.getContentType().equals("application/zip") || file.getContentType().equals("application/x-zip-compressed")) ||
                file.getOriginalFilename() != null && file.getOriginalFilename().toLowerCase().endsWith(".zip");
    }

    public ResponseEntity<MonitoringActivePackagesResponse> findActivePackages(MonitoringActivePackagesRequest activePackagesRequest) {
        try {
            AuthorizationService.AuthResult authResult = authorizationService.findAuthInfo(activePackagesRequest.getUserId(), activePackagesRequest.getPassword());
            if (!authResult.isSuccess()) {
                throw clientExcp(authResult.getError(), XmlErrorFormat.MonitoringActivePackagesError);
            }
            List<UserPackage> activePackages = signalService.findActivePackages(activePackagesRequest.getUserId());

            if (!activePackages.isEmpty()) {
                MonitoringActivePackagesResponse response = new MonitoringActivePackagesResponse();
                response.setStatus("OK");
                response.setMembercode(activePackagesRequest.getUserId().substring(0, 6));
                Packages packages = new Packages();
                activePackages.forEach(data -> {
                    Package pack = new Package();
                    pack.setPackageName(data.getPackageName());
                    pack.setCreatedAt(data.getCreatedAt() != null ? data.getCreatedAt().toString() : null);
                    packages.getPackages().add(pack);
                });
                response.setPackages(packages);
                return ResponseEntity.ok(response);
            } else {
                throw clientExcp(SignalError.ERROR_015, XmlErrorFormat.MonitoringActivePackagesError);
            }
        } catch (Exception e) {
            if (e instanceof SignalClientException) {
                throw (SignalClientException) e;
            }
            throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringActivePackagesError, e);
        }
    }

    public ResponseEntity<MonitoringAllPackagesResponse> findAllPackages(MonitoringAllPackagesRequest allPackagesRequest) {
        try {
            AuthorizationService.AuthResult authResult = authorizationService.findAuthInfo(allPackagesRequest.getUserId(), allPackagesRequest.getPassword());
            if (!authResult.isSuccess()) {
                throw clientExcp(authResult.getError(), XmlErrorFormat.MonitoringActivePackagesError);
            }
            List<UserPackage> activePackages = signalService.findAllPackages(allPackagesRequest.getUserId());

            if (!activePackages.isEmpty()) {
                MonitoringAllPackagesResponse response = new MonitoringAllPackagesResponse();
                response.setStatus("OK");
                response.setMembercode(allPackagesRequest.getUserId().substring(0, 6));
                PackagesAll packages = new PackagesAll();
                activePackages.forEach(data -> {
                    PackageAll pack = new PackageAll();
                    pack.setPackageName(data.getPackageName());
                    pack.setCreatedAt(data.getCreatedAt() != null ? data.getCreatedAt().toString() : null);
                    pack.setStatus(data.getStatus());
                    if (data.getStatus().equals(Status.DELETED.name())) {
                        pack.setDeactivatedAt(data.getDeactivatedAt() != null ? data.getDeactivatedAt().toString() : null);
                    }
                    packages.getPackages().add(pack);
                });
                response.setPackages(packages);
                return ResponseEntity.ok(response);
            } else {
                throw clientExcp(SignalError.ERROR_019, XmlErrorFormat.MonitoringActivePackagesError);
            }
        } catch (Exception e) {
            if (e instanceof SignalClientException) {
                throw (SignalClientException) e;
            }
            throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringActivePackagesError, e);
        }
    }

    public ResponseEntity<MonitoringPackageDeleteResponse> deletePackage(String username, String packageName) {
        try {
            if (!signalService.isPackageExists(username, packageName)) {
                throw clientExcp(SignalError.ERROR_016, XmlErrorFormat.MonitoringPackageDeleteError, packageName);
            }

            LocalDateTime deactivatedAt = LocalDateTime.now();
            signalService.deletePackage(username, packageName, deactivatedAt);

            MonitoringPackageDeleteResponse response = new MonitoringPackageDeleteResponse();
            response.setStatus("Deleted");
            response.setPackageName(packageName);
            response.setDeactivatedAt(deactivatedAt.toString());

            signalRestService.deletePackage(username, packageName);
            log.info("Package {} deleted", packageName);
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            if (e instanceof SignalClientException) {
                throw (SignalClientException) e;
            }
            throw serverExcp(SignalError.ERROR_017, XmlErrorFormat.MonitoringPackageDeleteError, e);
        }
    }

    public ResponseEntity<MonitoringReportListResponse> getReportList(String username, String packageName) {
        try {
            if (signalService.isPackagesByStatusExist(username, packageName, Status.PROCESSING)) {
                throw clientExcp(SignalError.ERROR_018, XmlErrorFormat.MonitoringReportListError, packageName);
            }
            if (signalService.isPackagesByStatusExist(username, packageName, Status.REJECTED)) {
                throw clientExcp(SignalError.ERROR_020, XmlErrorFormat.MonitoringReportListError, packageName);
            }
            if (signalService.isPackagesByStatusExist(username, packageName, Status.ERROR)) {
                throw clientExcp(SignalError.ERROR_021, XmlErrorFormat.MonitoringReportListError, packageName);
            }
            if (signalService.isPackagesByStatusExist(username, packageName, Status.DELETED)) {
                throw clientExcp(SignalError.ERROR_022, XmlErrorFormat.MonitoringReportListError, packageName);
            }
            if (!signalService.isPackagesByStatusExist(username, packageName, Status.ON_MONITORING)) {
                throw clientExcp(SignalError.ERROR_012, XmlErrorFormat.MonitoringReportListError, packageName);
            }

            String memberCode = username.substring(0, 6);
            String filePrefix = (memberCode + "_" + packageName).toUpperCase();
            String dirPath = config.getLoadDir() + File.separator + memberCode;
            File folder = new File(dirPath);

            MonitoringReportListResponse.Reports reports = new MonitoringReportListResponse.Reports();
            if (folder.exists() && folder.isDirectory()) {
                File[] files = folder.listFiles();
                if (files != null) {
                    for (File file : files) {
                        if (file.isFile() && file.getName().startsWith(filePrefix)) {
                            reports.getReports().add(file.getName());
                        }
                    }
                }
            }
            MonitoringReportListResponse response = new MonitoringReportListResponse();
            response.setPackageName(packageName);
            response.setReports(reports);
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            if (e instanceof SignalClientException) {
                throw (SignalClientException) e;
            }
            throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringReportListError, e);
        }
    }

    public ResponseEntity<Resource> downloadFile(String fileName) {
        try {
            String memberCode = CTUtil.getMemberCodeFromFileName(fileName);
            if (memberCode == null) {
                throw clientExcp(SignalError.ERROR_014, XmlErrorFormat.MonitoringDownloadError, fileName);
            }
            String dirPath = config.getLoadDir() + File.separator + memberCode;
            File folder = new File(dirPath);

            if (folder.exists() && folder.isDirectory()) {
                File file = new File(dirPath + File.separator + fileName);
                if (file.exists() && file.isFile()) {
                    return ResponseEntity.ok().body(new FileUrlResource(file.getPath()));
                }
            } else {
                throw clientExcp(SignalError.ERROR_014, XmlErrorFormat.MonitoringDownloadError, fileName);
            }
            throw clientExcp(SignalError.ERROR_014, XmlErrorFormat.MonitoringDownloadError, fileName);
        } catch (Exception e) {
            if (e instanceof SignalClientException) {
                throw (SignalClientException) e;
            }
            throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringDownloadError, e);
        }
    }
}
