package de.avatic.lcc.service.access;

import de.avatic.lcc.dto.calculation.CalculationStatus;
import de.avatic.lcc.dto.calculation.ResolvePremiseDTO;
import de.avatic.lcc.dto.calculation.PremiseDTO;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.PackagingUpdateDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO;
import de.avatic.lcc.model.db.calculations.CalculationJob;
import de.avatic.lcc.model.db.calculations.CalculationJobState;
import de.avatic.lcc.model.db.premises.Premise;
import de.avatic.lcc.model.db.premises.PremiseState;
import de.avatic.lcc.repositories.calculation.CalculationJobDestinationRepository;
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
import de.avatic.lcc.repositories.calculation.CalculationJobRouteSectionRepository;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.properties.PropertySetRepository;
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
import de.avatic.lcc.service.calculation.execution.CalculationExecutionService;
import de.avatic.lcc.service.calculation.execution.CalculationStatusService;
import de.avatic.lcc.service.precalculation.PostCalculationCheckService;
import de.avatic.lcc.service.precalculation.PreCalculationCheckService;
import de.avatic.lcc.service.transformer.generic.DimensionTransformer;
import de.avatic.lcc.service.transformer.premise.PremiseTransformer;
import de.avatic.lcc.service.users.AuthorizationService;
import de.avatic.lcc.util.exception.base.InternalErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;

@Service
public class PremisesService {


    private static final Logger log = LoggerFactory.getLogger(PremisesService.class);

    private final PremiseRepository premiseRepository;
    private final PremiseTransformer premiseTransformer;
    private final DimensionTransformer dimensionTransformer;
    private final DestinationService destinationService;
    private final CalculationJobRepository calculationJobRepository;

    private final PropertySetRepository propertySetRepository;
    private final ValidityPeriodRepository validityPeriodRepository;
    private final CalculationStatusService calculationStatusService;
    private final CalculationExecutionService calculationExecutionService;
    private final PreCalculationCheckService preCalculationCheckService;
    private final CalculationJobDestinationRepository calculationJobDestinationRepository;
    private final CalculationJobRouteSectionRepository calculationJobRouteSectionRepository;
    private final PostCalculationCheckService postCalculationCheckService;
    private final AuthorizationService authorizationService;

    public PremisesService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DimensionTransformer dimensionTransformer, DestinationService destinationService, CalculationJobRepository calculationJobRepository, PropertySetRepository propertySetRepository, ValidityPeriodRepository validityPeriodRepository, CalculationStatusService calculationStatusService, CalculationExecutionService calculationExecutionService, PreCalculationCheckService preCalculationCheckService, CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository, PostCalculationCheckService postCalculationCheckService, AuthorizationService authorizationService) {
        this.premiseRepository = premiseRepository;
        this.premiseTransformer = premiseTransformer;
        this.dimensionTransformer = dimensionTransformer;
        this.destinationService = destinationService;
        this.calculationJobRepository = calculationJobRepository;

        this.propertySetRepository = propertySetRepository;
        this.validityPeriodRepository = validityPeriodRepository;
        this.calculationStatusService = calculationStatusService;
        this.calculationExecutionService = calculationExecutionService;
        this.preCalculationCheckService = preCalculationCheckService;
        this.calculationJobDestinationRepository = calculationJobDestinationRepository;
        this.calculationJobRouteSectionRepository = calculationJobRouteSectionRepository;
        this.postCalculationCheckService = postCalculationCheckService;
        this.authorizationService = authorizationService;
    }

    @Transactional(readOnly = true)
    public SearchQueryResult<PremiseDTO> listPremises(String filter, Integer page, Integer limit, Integer userId, Boolean draft, Boolean archived, Boolean done) {
        var admin = authorizationService.isSuper();
        userId = authorizationService.getUserId();
        return SearchQueryResult.map(premiseRepository.listPremises(filter, new SearchQueryPagination(page, limit), userId, draft, archived, done), admin ? premiseTransformer::toPremiseDTOWithUserInfo : premiseTransformer::toPremiseDTO);
    }

    @Transactional(readOnly = true)
    public List<PremiseDetailDTO> getPremises(List<Integer> premiseIds) {
        var admin = authorizationService.isSuper();
        var userId = authorizationService.getUserId();

        if (!admin)
            premiseRepository.checkOwner(premiseIds, userId);

        var premisses = premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).toList();

        return premisses.stream().map(premiseTransformer::toPremiseDetailDTO).toList();
    }


    public void startCalculation(List<Integer> premises) {
        var admin = authorizationService.isSuper();
        var userId = authorizationService.getUserId();

        if (!admin)
            premiseRepository.checkOwner(premises, userId);

        var validSetId = propertySetRepository.getValidSetId();
        var validPeriodId = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new InternalErrorException("no set of transport rates found that is VALID"));

        var checkResult = premises.stream().map(premiseId -> preCalculationCheckService.doPrecheck(premiseId, validSetId, validPeriodId)).toList();
        CompletableFuture.allOf(checkResult.toArray(new CompletableFuture[0])).join();

        var calculationIds = new ArrayList<Integer>();

        premises.forEach(p -> {

            var existingJob = calculationJobRepository.findJob(p, validSetId, validPeriodId);

            if (existingJob.isPresent()) {
                if (CalculationJobState.EXCEPTION == existingJob.get().getJobState()) {
                    calculationJobRepository.setStateTo(existingJob.get().getId(), CalculationJobState.CREATED);
                    calculationIds.add(existingJob.get().getId());
                } else if (CalculationJobState.SCHEDULED == existingJob.get().getJobState() && existingJob.get().getCalculationDate().plusMinutes(15).isBefore(LocalDateTime.now())) {
                    calculationJobRepository.reschedule(existingJob.get().getId());
                    calculationIds.add(existingJob.get().getId());
                }
            } else {
                CalculationJob job = new CalculationJob();

                job.setPremiseId(p);
                job.setJobState(CalculationJobState.CREATED);
                job.setCalculationDate(java.time.LocalDateTime.now());
                job.setUserId(userId);
                job.setPropertySetId(validSetId);
                job.setValidityPeriodId(validPeriodId);

                calculationIds.add(calculationJobRepository.insert(job));
            }

        });

        premiseRepository.setStatus(premises, PremiseState.COMPLETED);

        calculationIds.forEach(this::scheduleCalculation);

        try {
            var futures = calculationIds.stream().map(calculationExecutionService::launchJobCalculation).toList();
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

            for (var future : futures) {
                var jobResult = future.get();
                if (jobResult.getState().equals(CalculationJobState.EXCEPTION)) {
                    calculationJobRepository.setStateTo(jobResult.getJobId(), CalculationJobState.EXCEPTION);
                    throw new InternalErrorException("Calculation failed", "Calculation was not successful. Please contact Administrator.", new Exception(jobResult.getException()));
                } else {

                    postCalculationCheckService.doPostcheck(jobResult);

                    for (var destinationInfo : jobResult.getDestinationInfos()) {
                        destinationInfo.destinationCalculationJob().setCalculationJobId(jobResult.getJobId());
                        var destinationId = calculationJobDestinationRepository.insert(destinationInfo.destinationCalculationJob());

                        for (var sectionInfo : destinationInfo.sectionInfo()) {
                            var section = sectionInfo.result();
                            section.setCalculationJobDestinationId(destinationId);
                            calculationJobRouteSectionRepository.insert(section);
                        }
                    }

                    calculationJobRepository.setStateTo(jobResult.getJobId(), CalculationJobState.VALID);
                    log.info("Calculation job {} finished", jobResult.getJobId());
                }
            }

        } catch (CompletionException | InterruptedException | ExecutionException e) {
            throw new InternalErrorException("Task execution of calculation failed", "Task execution of calculation was not successful. Please contact Administrator.", e);
        }
    }

    public void scheduleCalculation(Integer id) {
        calculationJobRepository.setStateTo(id, CalculationJobState.SCHEDULED);
    }

    public CalculationStatus getCalculationStatus(Integer processId) {
        return calculationStatusService.getCalculationStatus(processId);
    }


    public void updatePackaging(PackagingUpdateDTO packagingDTO) {
        var admin = authorizationService.isSuper();
        var userId = authorizationService.getUserId();

        if (!admin)
            premiseRepository.checkOwner(packagingDTO.getPremiseIds(), userId);

        var dimensions = packagingDTO.getDimensions() == null ? null : dimensionTransformer.toDimensionEntity(packagingDTO.getDimensions());

        premiseRepository.resetPackaging(packagingDTO.getPremiseIds(), dimensions, packagingDTO.getStackable(), packagingDTO.getMixable());

    }

    public void updateMaterial(MaterialUpdateDTO materialUpdateDTO) {
        var admin = authorizationService.isSuper();
        var userId = authorizationService.getUserId();

        if (!admin)
            premiseRepository.checkOwner(materialUpdateDTO.getPremiseIds(), userId);



        var unlockedIds = premiseRepository.getIdsWithUnlockedTariffs(materialUpdateDTO.getPremiseIds());

        if (unlockedIds.isEmpty()) return;

        var tariffRate = materialUpdateDTO.getTariffRate() == null ? null : BigDecimal.valueOf(materialUpdateDTO.getTariffRate().doubleValue());
        premiseRepository.updateMaterial(unlockedIds, materialUpdateDTO.getHsCode(), tariffRate);

    }

    public void updatePrice(PriceUpdateDTO priceUpdateDTO) {
        var admin = authorizationService.isSuper();
        var userId = authorizationService.getUserId();

        if (!admin)
            premiseRepository.checkOwner(priceUpdateDTO.getPremiseIds(), userId);

        var price = priceUpdateDTO.getPrice() == null ? null : BigDecimal.valueOf(priceUpdateDTO.getPrice().doubleValue());
        var overseaShare = priceUpdateDTO.getOverseaShare() == null ? null : BigDecimal.valueOf(priceUpdateDTO.getOverseaShare().doubleValue());

        premiseRepository.updatePrice(priceUpdateDTO.getPremiseIds(), price, priceUpdateDTO.getIncludeFcaFee(), overseaShare);
    }

    @Transactional
    public void delete(List<Integer> premiseIds) {
        var admin = authorizationService.isSuper();
        var userId = authorizationService.getUserId();

        if (!admin)
            premiseRepository.checkOwner(premiseIds, userId);


        // only delete drafts.
        var toBeDeleted = premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).map(Premise::getId).toList();

        if (!toBeDeleted.isEmpty()) {
            destinationService.deleteAllDestinationsByPremiseId(toBeDeleted, false);
            premiseRepository.deletePremisesById(toBeDeleted);
        }

    }

    @Transactional
    public void archive(List<Integer> premiseIds) {
        var admin = authorizationService.isSuper();
        var userId = authorizationService.getUserId();

        if (!admin)
            premiseRepository.checkOwner(premiseIds, userId);


        // only archive completed.
        var premisses = premiseRepository.getPremisesById(premiseIds);
        premiseRepository.setStatus(premisses.stream().filter(p -> p.getState().equals(PremiseState.COMPLETED)).map(Premise::getId).toList(), PremiseState.ARCHIVED);
    }

    public ResolvePremiseDTO resolveStatus(List<Integer> premissIds) {
        ResolvePremiseDTO resolveDTO = new ResolvePremiseDTO();
        var drafts = new ArrayList<Integer>();
        var completed = new ArrayList<Integer>();
        var archived = new ArrayList<Integer>();

        var premisses = premiseRepository.getPremisesById(premissIds);

        premisses.forEach(p -> {
            if(PremiseState.DRAFT.equals(p.getState())) drafts.add(p.getId());
            else if(PremiseState.COMPLETED.equals(p.getState())) completed.add(p.getId());
            else if(PremiseState.ARCHIVED.equals(p.getState())) archived.add(p.getId());
        });

        resolveDTO.setCompleted(completed);
        resolveDTO.setDraft(drafts);
        resolveDTO.setArchived(archived);

        return resolveDTO;
    }

    @Transactional
    public List<Integer> duplicate(List<Integer> premissIds) {
        var admin = authorizationService.isSuper();
        var userId = authorizationService.getUserId();

        if (!admin)
            premiseRepository.checkOwner(premissIds, userId);

        var newIds = new ArrayList<Integer>();

        premissIds.forEach(id -> {

            var old = premiseRepository.getPremiseById(id).orElseThrow();
            var newId = premiseRepository.insert(old.getMaterialId(), old.getSupplierNodeId(), old.getUserSupplierNodeId(), BigDecimal.valueOf(old.getLocation().getLatitude()), BigDecimal.valueOf(old.getLocation().getLongitude()), old.getCountryId(), userId);

            premiseRepository.updateMaterial(Collections.singletonList(newId), old.getHsCode(), old.getTariffRate(), old.getTariffUnlocked());
            premiseRepository.updatePrice(Collections.singletonList(newId), old.getMaterialCost(), old.getFcaEnabled(), old.getOverseaShare());
            premiseRepository.resetPackaging(Collections.singletonList(newId), dimensionTransformer.toDimensionEntity(old), old.getHuStackable(), old.getHuMixable());
            premiseRepository.setPackagingId(newId, old.getPackagingId());

            destinationService.duplicate(old.getId(), newId);

            newIds.add(newId);

        });

        return newIds;
    }
}
