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

import static com.iqser.red.service.persistence.management.v1.processor.exception.DossierNotFoundException.DOSSIER_NOT_FOUND_MESSAGE;

import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierEntity;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.DossierNotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.WatermarkService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.DossierRepository;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.DossierTemplateRepository;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.FileRepository;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.ReportTemplateRepository;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.CreateOrUpdateDossierRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.DossierChange;

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

@Service
@RequiredArgsConstructor
@Slf4j
public class DossierPersistenceService {

    private final DossierRepository dossierRepository;
    private final FileRepository fileRepository;
    private final DossierTemplateRepository dossierTemplateRepository;
    private final ReportTemplateRepository reportTemplateRepository;
    private final DossierStatusPersistenceService dossierStatusPersistenceService;
    private final WatermarkService watermarkService;


    @Transactional
    public DossierEntity insert(CreateOrUpdateDossierRequest createOrUpdateDossierRequest) {

        DossierEntity dossier = new DossierEntity();
        BeanUtils.copyProperties(createOrUpdateDossierRequest, dossier, "watermarkId", "previewWatermarkId");
        dossier.setId(UUID.randomUUID().toString());
        dossier.setDate(OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
        dossier.setLastUpdated(OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
        dossier.setDossierTemplate(dossierTemplateRepository.getOne(createOrUpdateDossierRequest.getDossierTemplateId()));
        var reportTemplates = reportTemplateRepository.findAllById(createOrUpdateDossierRequest.getReportTemplateIds());
        reportTemplates.forEach(r -> r.getDossiers().add(dossier));
        dossier.setReportTemplates(reportTemplates);
        this.handleDossierStatus(createOrUpdateDossierRequest, dossier);
        this.handleWatermark(createOrUpdateDossierRequest, dossier);

        return dossierRepository.saveAndFlush(dossier);
    }


    private void handleDossierStatus(CreateOrUpdateDossierRequest createOrUpdateDossierRequest, DossierEntity dossier) {

        if (createOrUpdateDossierRequest.getDossierStatusId() != null) {
            var dossierStatus = dossierStatusPersistenceService.getDossierStatus(createOrUpdateDossierRequest.getDossierStatusId());
            if (dossierStatus.getDossierTemplateId().equals(createOrUpdateDossierRequest.getDossierTemplateId())) {
                dossier.setDossierStatus(dossierStatus);
            } else {
                throw new BadRequestException("Invalid Dossier Status");
            }
        } else {
            dossier.setDossierStatus(null);
        }
    }


    private void handleWatermark(CreateOrUpdateDossierRequest createOrUpdateDossierRequest, DossierEntity dossier) {

        if (createOrUpdateDossierRequest.getWatermarkId() != null) {
            if (!createOrUpdateDossierRequest.getWatermarkId().equals(dossier.getWatermarkId())) {
                var watermarkEntity = watermarkService.getWatermark(createOrUpdateDossierRequest.getWatermarkId());
                if (watermarkEntity.getDossierTemplateId().equals(createOrUpdateDossierRequest.getDossierTemplateId())) {
                    if (!watermarkEntity.isEnabled()) {
                        throw new BadRequestException("Watermark configuration is disabled");
                    }
                    dossier.setWatermarkId(createOrUpdateDossierRequest.getWatermarkId());
                    dossier.setWatermark(watermarkEntity);
                } else {
                    throw new BadRequestException("Invalid watermark id - dossierTemplate id does not match");
                }
            }
        } else {
            dossier.setWatermark(null);
        }
        if (createOrUpdateDossierRequest.getPreviewWatermarkId() != null) {
            if (!createOrUpdateDossierRequest.getPreviewWatermarkId().equals(dossier.getPreviewWatermarkId())) {
                var previewWatermarkEntity = watermarkService.getWatermark(createOrUpdateDossierRequest.getPreviewWatermarkId());
                if (previewWatermarkEntity.getDossierTemplateId().equals(createOrUpdateDossierRequest.getDossierTemplateId())) {
                    if (!previewWatermarkEntity.isEnabled()) {
                        throw new BadRequestException("Preview watermark configuration is disabled");
                    }
                    dossier.setPreviewWatermarkId(createOrUpdateDossierRequest.getPreviewWatermarkId());
                    dossier.setPreviewWatermark(previewWatermarkEntity);
                } else {
                    throw new BadRequestException("Invalid preview watermark id - dossierTemplate id does not match");
                }
            }
        } else {
            dossier.setPreviewWatermark(null);
        }
    }


    @Transactional
    public void update(String dossierId, CreateOrUpdateDossierRequest createOrUpdateDossierRequest) {

        dossierRepository.findById(dossierId).ifPresent(dossier -> {
            BeanUtils.copyProperties(createOrUpdateDossierRequest, dossier, "watermarkId", "previewWatermarkId");
            dossier.setDossierTemplate(dossierTemplateRepository.getOne(createOrUpdateDossierRequest.getDossierTemplateId()));
            dossier.setLastUpdated(OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));

            dossier.getReportTemplates()
                    .forEach(t -> t.getDossiers().removeIf(d -> d.getId().equals(dossierId) && !createOrUpdateDossierRequest.getReportTemplateIds().contains(t.getTemplateId())));
            var reportTemplates = reportTemplateRepository.findAllById(createOrUpdateDossierRequest.getReportTemplateIds());
            reportTemplates.forEach(r -> {
                if (!r.getDossiers().stream().map(DossierEntity::getId).collect(Collectors.toSet()).contains(dossierId)) {
                    r.getDossiers().add(dossier);
                }
            });
            dossier.setReportTemplates(reportTemplates);
            this.handleDossierStatus(createOrUpdateDossierRequest, dossier);
            this.handleWatermark(createOrUpdateDossierRequest, dossier);
        });

    }


    public DossierEntity getActiveOrArchivedDossier(String dossierId) {

        var dossier = findByDossierId(dossierId);

        if (dossier == null || dossier.getHardDeletedTime() != null || dossier.getSoftDeletedTime() != null) {
            throw new DossierNotFoundException(String.format(DOSSIER_NOT_FOUND_MESSAGE, dossierId));
        }

        return dossier;
    }


    public DossierEntity findByDossierId(String dossierId) {

        return dossierRepository.findById(dossierId).orElseThrow(() -> new DossierNotFoundException(String.format(DOSSIER_NOT_FOUND_MESSAGE, dossierId)));
    }


    public DossierEntity getAndValidateDossier(String dossierId) {
        // check whether the dossierId exists and is not deleted
        var dossier = findByDossierId(dossierId);
        if (dossier == null || dossier.getSoftDeletedTime() != null || dossier.getHardDeletedTime() != null || dossier.getArchivedTime() != null) {
            throw new DossierNotFoundException(String.format(DOSSIER_NOT_FOUND_MESSAGE, dossierId));
        }
        return dossier;

    }


    public List<DossierEntity> findAllDossiers() {

        return dossierRepository.findAll();

    }


    public List<DossierEntity> findAllDossiers(List<String> dossierIds) {

        if (!dossierIds.isEmpty()) {
            return dossierRepository.findAllById(dossierIds);
        } else {
            return Collections.emptyList();
        }
    }


    public List<DossierEntity> findAllDossiersForDossierTemplateId(String dossierTemplateId) {

        return dossierRepository.findByDossierTemplateId(dossierTemplateId);
    }


    @Transactional
    public void hardDelete(String dossierId) {

        dossierRepository.hardDelete(dossierId, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS), OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
    }


    @Transactional
    public void undelete(String dossierId) {

        int updateCount = dossierRepository.undelete(dossierId, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
        if (updateCount == 0) {
            throw new BadRequestException("Cannot undelete a hard-deleted dossier!");
        }
    }


    @Transactional
    public void markDossierAsDeleted(String dossierId, OffsetDateTime softDeletedTime) {

        dossierRepository.markDossierAsDeleted(dossierId, softDeletedTime, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
    }


    public Set<DossierChange> hasChangesSince(OffsetDateTime since) {

        var dossiersWithChanges = dossierRepository.findDossierChangeByLastUpdatedIsAfter(since.truncatedTo(ChronoUnit.MILLIS));
        var dossiersWithChangedFiles = fileRepository.findDossierChangeByLastUpdatedIsAfter(since.truncatedTo(ChronoUnit.MILLIS));

        Set<DossierChange> dossierChanges = new HashSet<>();

        for (var dossierId : dossiersWithChanges) {
            var dossierChange = new DossierChange(dossierId, true, dossiersWithChangedFiles.contains(dossierId));
            dossierChanges.add(dossierChange);
        }

        for (var dossierId : dossiersWithChangedFiles) {
            var dossierChange = new DossierChange(dossierId, dossiersWithChanges.contains(dossierId), true);
            dossierChanges.add(dossierChange);
        }

        return dossierChanges;
    }


    @Transactional
    public void archiveDossier(String dossierId) {

        dossierRepository.archiveDossier(dossierId, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
    }


    @Transactional
    public void unarchiveDossier(String dossierId) {

        int updateCount = dossierRepository.unarchiveDossier(dossierId, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
        if (updateCount == 0) {
            throw new BadRequestException("Cannot unarchive dossier!");
        }
    }

}
