package com.iqser.red.service.persistence.management.v1.processor.service.job;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.primitives.Floats;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.FileEntity;
import com.iqser.red.service.persistence.management.v1.processor.service.FileManagementStorageService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.FileStatusPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.FileRepository;
import com.iqser.red.service.persistence.management.v1.processor.utils.StorageIdUtils;
import com.iqser.red.service.persistence.management.v1.processor.utils.TenantUtils;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
import com.iqser.red.service.redaction.v1.server.data.DocumentPageProto;
import com.iqser.red.service.redaction.v1.server.data.DocumentPositionDataProto;
import com.iqser.red.service.redaction.v1.server.data.DocumentStructureProto;
import com.iqser.red.service.redaction.v1.server.data.DocumentTextDataProto;
import com.iqser.red.service.redaction.v1.server.data.EntryDataProto;
import com.iqser.red.service.redaction.v1.server.data.LayoutEngineProto;
import com.iqser.red.service.redaction.v1.server.data.NodeTypeProto;
import com.iqser.red.service.redaction.v1.server.data.old.DocumentPage;
import com.iqser.red.service.redaction.v1.server.data.old.DocumentPositionData;
import com.iqser.red.service.redaction.v1.server.data.old.DocumentStructure;
import com.iqser.red.service.redaction.v1.server.data.old.DocumentTextData;
import com.iqser.red.service.redaction.v1.server.data.old.LayoutEngine;
import com.iqser.red.storage.commons.exception.StorageObjectDoesNotExist;
import com.iqser.red.storage.commons.service.StorageService;
import com.knecon.fforesight.tenantcommons.TenantContext;
import com.knecon.fforesight.tenantcommons.TenantProvider;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@Service
@DisallowConcurrentExecution
public class DocumentDataFilesConversionJob implements Job {

    private final FileRepository fileRepository;
    private final FileStatusPersistenceService fileStatusPersistenceService;
    private final TenantProvider tenantProvider;
    private final FileManagementStorageService fileManagementStorageService;
    private final StorageService storageService;
    private final ObjectMapper objectMapper;

    private static final long MAX_JOB_DURATION = 2 * 60 * 60 * 1000L;
    private static final int FILE_BATCH_SIZE = 32;


    @Override
    public void execute(JobExecutionContext jobExecutionContext) {

        long startTime = System.currentTimeMillis();

        tenantProvider.getTenants()
                .forEach(tenant -> {

                    if (!TenantUtils.isTenantReadyForPersistence(tenant)) {
                        return;
                    }

                    TenantContext.setTenantId(tenant.getTenantId());
                    convertAllDocumentDataFiles(startTime);

                });
    }


    @SneakyThrows
    private void convertAllDocumentDataFiles(long startTime) {

        while (System.currentTimeMillis() - startTime < MAX_JOB_DURATION) {

            List<FileEntity> files = fileRepository.findByProtobufMigrationDoneFalse(PageRequest.of(0, FILE_BATCH_SIZE));
            if (files.isEmpty()) {
                break;
            }

            String tenantId = TenantContext.getTenantId();
            files.parallelStream()
                    .forEach(file -> {
                        TenantContext.setTenantId(tenantId);
                        log.info("Reading dossier {} file {} from storage", file.getDossierId(), file.getId());

                        String oldDocumentStructureStorageId = StorageIdUtils.getStorageId(file.getDossierId(), file.getId(), FileType.DOCUMENT_STRUCTURE.name(), ".json");
                        String newDocumentStructureStorageId = StorageIdUtils.getStorageId(file.getDossierId(), file.getId(), FileType.DOCUMENT_STRUCTURE);

                        try (InputStream inputStream = fileManagementStorageService.getObject(TenantContext.getTenantId(), oldDocumentStructureStorageId)) {
                            DocumentStructure oldDocumentStructure = objectMapper.readValue(inputStream, DocumentStructure.class);
                            DocumentStructureProto.DocumentStructure newDocumentStructure = convertDocumentStructure(oldDocumentStructure);
                            storageService.storeProtoObject(TenantContext.getTenantId(), newDocumentStructureStorageId, newDocumentStructure);
                            //storageService.deleteObject(TenantContext.getTenantId(), oldDocumentStructureStorageId);
                        } catch (StorageObjectDoesNotExist e) {
                            log.info("No document structure found for {}, {}, ignoring....", file.getDossierId(), file.getId());
                        } catch (IOException e) {
                            log.error("Document structure could not be mapped for {}, {}, ignoring....", file.getDossierId(), file.getId());
                        }

                        String oldDocumentPagesStorageId = StorageIdUtils.getStorageId(file.getDossierId(), file.getId(), FileType.DOCUMENT_PAGES.name(), ".json");
                        String newDocumentPagesStorageId = StorageIdUtils.getStorageId(file.getDossierId(), file.getId(), FileType.DOCUMENT_PAGES);
                        try (InputStream inputStream = fileManagementStorageService.getObject(TenantContext.getTenantId(), oldDocumentPagesStorageId)) {
                            DocumentPage[] oldDocumentPages = objectMapper.readValue(inputStream, DocumentPage[].class);
                            DocumentPageProto.AllDocumentPages newDocumentPages = convertAllDocumentPages(oldDocumentPages);
                            storageService.storeProtoObject(TenantContext.getTenantId(), newDocumentPagesStorageId, newDocumentPages);
                            //storageService.deleteObject(TenantContext.getTenantId(), oldDocumentPagesStorageId);
                        } catch (StorageObjectDoesNotExist e) {
                            log.info("No document pages found for {}, {}, ignoring....", file.getDossierId(), file.getId());
                        } catch (IOException e) {
                            log.error("Document pages could not be mapped for {}, {}, ignoring....", file.getDossierId(), file.getId());
                        }

                        String oldDocumentPositionDataStorageId = StorageIdUtils.getStorageId(file.getDossierId(), file.getId(), FileType.DOCUMENT_POSITION.name(), ".json");
                        String newDocumentPositionDataStorageId = StorageIdUtils.getStorageId(file.getDossierId(), file.getId(), FileType.DOCUMENT_POSITION);
                        try (InputStream inputStream = fileManagementStorageService.getObject(TenantContext.getTenantId(), oldDocumentPositionDataStorageId)) {
                            DocumentPositionData[] oldDocumentPositionData = objectMapper.readValue(inputStream, DocumentPositionData[].class);
                            DocumentPositionDataProto.AllDocumentPositionData newDocumentPositionData = convertAllDocumentPositionData(oldDocumentPositionData);
                            storageService.storeProtoObject(TenantContext.getTenantId(), newDocumentPositionDataStorageId, newDocumentPositionData);
                            //storageService.deleteObject(TenantContext.getTenantId(), oldDocumentPositionDataStorageId);
                        } catch (StorageObjectDoesNotExist e) {
                            log.info("No document position data found for {}, {}, ignoring....", file.getDossierId(), file.getId());
                        } catch (IOException e) {
                            log.error("Document position data could not be mapped for {}, {}, ignoring....", file.getDossierId(), file.getId());
                        }

                        String oldDocumentTextDataStorageId = StorageIdUtils.getStorageId(file.getDossierId(), file.getId(), FileType.DOCUMENT_TEXT.name(), ".json");
                        String newDocumentTextDataStorageId = StorageIdUtils.getStorageId(file.getDossierId(), file.getId(), FileType.DOCUMENT_TEXT);
                        try (InputStream inputStream = fileManagementStorageService.getObject(TenantContext.getTenantId(), oldDocumentTextDataStorageId)) {
                            DocumentTextData[] oldDocumentTextData = objectMapper.readValue(inputStream, DocumentTextData[].class);
                            DocumentTextDataProto.AllDocumentTextData newDocumentTextData = convertAllDocumentTextData(oldDocumentTextData);
                            storageService.storeProtoObject(TenantContext.getTenantId(), newDocumentTextDataStorageId, newDocumentTextData);
                            //storageService.deleteObject(TenantContext.getTenantId(), oldDocumentTextDataStorageId);
                        } catch (StorageObjectDoesNotExist e) {
                            log.info("No document text data found for {}, {}, ignoring....", file.getDossierId(), file.getId());
                        } catch (IOException e) {
                            log.error("Document text data could not be mapped for {}, {}, ignoring....", file.getDossierId(), file.getId());
                        }
                    });
            fileStatusPersistenceService.setProtobufMigrationDoneForFiles(files.stream()
                                                                                  .map(FileEntity::getId)
                                                                                  .toList());
        }
    }


    private static EntryDataProto.EntryData convertEntryData(DocumentStructure.EntryData oldEntryData) {

        EntryDataProto.EntryData.Builder builder = EntryDataProto.EntryData.newBuilder();

        builder.setType(NodeTypeProto.NodeType.valueOf(oldEntryData.getType().name()));
        builder.addAllTreeId(Arrays.stream(oldEntryData.getTreeId()).boxed()
                                     .collect(Collectors.toList()));
        builder.addAllAtomicBlockIds(Arrays.asList(oldEntryData.getAtomicBlockIds()));
        builder.addAllPageNumbers(Arrays.asList(oldEntryData.getPageNumbers()));

        builder.putAllProperties(oldEntryData.getProperties());

        if (oldEntryData.getChildren() != null) {
            oldEntryData.getChildren()
                    .forEach(child -> builder.addChildren(convertEntryData(child)));
        }

        builder.addAllEngines(oldEntryData.getEngines()
                                      .stream()
                                      .map(DocumentDataFilesConversionJob::convertLayoutEngine)
                                      .toList());

        return builder.build();
    }


    private static LayoutEngineProto.LayoutEngine convertLayoutEngine(LayoutEngine oldLayoutEngine) {

        return LayoutEngineProto.LayoutEngine.valueOf(oldLayoutEngine.name());
    }


    private static DocumentStructureProto.DocumentStructure convertDocumentStructure(DocumentStructure oldStructure) {

        DocumentStructureProto.DocumentStructure.Builder newBuilder = DocumentStructureProto.DocumentStructure.newBuilder();

        if (oldStructure.getRoot() != null) {
            newBuilder.setRoot(convertEntryData(oldStructure.getRoot()));
        }

        return newBuilder.build();
    }


    private static DocumentPageProto.DocumentPage convertDocumentPage(DocumentPage oldPage) {

        return DocumentPageProto.DocumentPage.newBuilder()
                .setNumber(oldPage.getNumber())
                .setHeight(oldPage.getHeight())
                .setWidth(oldPage.getWidth())
                .setRotation(oldPage.getRotation())
                .build();
    }


    private static DocumentPageProto.AllDocumentPages convertAllDocumentPages(DocumentPage[] oldPages) {

        DocumentPageProto.AllDocumentPages.Builder allPagesBuilder = DocumentPageProto.AllDocumentPages.newBuilder();

        for (DocumentPage oldPage : oldPages) {
            DocumentPageProto.DocumentPage newPage = convertDocumentPage(oldPage);
            allPagesBuilder.addDocumentPages(newPage);
        }

        return allPagesBuilder.build();
    }


    private static DocumentPositionDataProto.DocumentPositionData convertDocumentPositionData(DocumentPositionData oldData) {

        DocumentPositionDataProto.DocumentPositionData.Builder builder = DocumentPositionDataProto.DocumentPositionData.newBuilder()
                .setId(oldData.getId())
                .addAllStringIdxToPositionIdx(Arrays.stream(oldData.getStringIdxToPositionIdx()).boxed()
                                                      .collect(Collectors.toList()));

        for (float[] pos : oldData.getPositions()) {
            DocumentPositionDataProto.DocumentPositionData.Position position = DocumentPositionDataProto.DocumentPositionData.Position.newBuilder()
                    .addAllValue(Floats.asList(pos))
                    .build();
            builder.addPositions(position);
        }

        return builder.build();
    }


    private static DocumentPositionDataProto.AllDocumentPositionData convertAllDocumentPositionData(DocumentPositionData[] oldDataList) {

        DocumentPositionDataProto.AllDocumentPositionData.Builder allDataBuilder = DocumentPositionDataProto.AllDocumentPositionData.newBuilder();

        for (DocumentPositionData oldData : oldDataList) {
            allDataBuilder.addDocumentPositionData(convertDocumentPositionData(oldData));
        }

        return allDataBuilder.build();
    }


    private static DocumentTextDataProto.DocumentTextData convertDocumentTextData(DocumentTextData oldData) {

        DocumentTextDataProto.DocumentTextData.Builder builder = DocumentTextDataProto.DocumentTextData.newBuilder()
                .setId(oldData.getId())
                .setPage(oldData.getPage())
                .setSearchText(oldData.getSearchText())
                .setNumberOnPage(oldData.getNumberOnPage())
                .setStart(oldData.getStart())
                .setEnd(oldData.getEnd())
                .addAllLineBreaks(Arrays.stream(oldData.getLineBreaks()).boxed()
                                          .collect(Collectors.toList()));

        return builder.build();
    }


    private static DocumentTextDataProto.AllDocumentTextData convertAllDocumentTextData(DocumentTextData[] oldDataList) {

        DocumentTextDataProto.AllDocumentTextData.Builder allDataBuilder = DocumentTextDataProto.AllDocumentTextData.newBuilder();

        for (DocumentTextData oldData : oldDataList) {
            allDataBuilder.addDocumentTextData(convertDocumentTextData(oldData));
        }

        return allDataBuilder.build();
    }

}
