package de.avatic.lcc.service.access;

import de.avatic.lcc.dto.calculation.CalculationStatus;
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.calculations.CalculationJob;
import de.avatic.lcc.model.calculations.CalculationJobState;
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
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.PropertyRepository;
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.transformer.generic.DimensionTransformer;
import de.avatic.lcc.service.transformer.premise.PremiseTransformer;
import de.avatic.lcc.util.exception.base.InternalErrorException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

@Service
public class PremisesService {

    private final PremiseRepository premiseRepository;
    private final PremiseTransformer premiseTransformer;
    private final DimensionTransformer dimensionTransformer;
    private final DestinationService destinationService;
    private final CalculationJobRepository calculationJobRepository;
    private final PropertyRepository propertyRepository;
    private final PropertySetRepository propertySetRepository;
    private final ValidityPeriodRepository validityPeriodRepository;
    private final CalculationStatusService calculationStatusService;
    private final CalculationExecutionService calculationExecutionService;

    public PremisesService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DimensionTransformer dimensionTransformer, DestinationService destinationService, CalculationJobRepository calculationJobRepository, PropertyRepository propertyRepository, PropertySetRepository propertySetRepository, ValidityPeriodRepository validityPeriodRepository, CalculationStatusService calculationStatusService, CalculationExecutionService calculationExecutionService) {
        this.premiseRepository = premiseRepository;
        this.premiseTransformer = premiseTransformer;
        this.dimensionTransformer = dimensionTransformer;
        this.destinationService = destinationService;
        this.calculationJobRepository = calculationJobRepository;
        this.propertyRepository = propertyRepository;
        this.propertySetRepository = propertySetRepository;
        this.validityPeriodRepository = validityPeriodRepository;
        this.calculationStatusService = calculationStatusService;
        this.calculationExecutionService = calculationExecutionService;
    }

    @Transactional(readOnly = true)
    public SearchQueryResult<PremiseDTO> listPremises(String filter, Integer page, Integer limit, Integer userId, Boolean deleted, Boolean archived, Boolean done) {

        //TODO check if user is admin. if not: use the user_id of currently authorized user.
        var admin = false;

        //TODO use actual user.
        userId = 1;

        return SearchQueryResult.map(premiseRepository.listPremises(filter, new SearchQueryPagination(page, limit), userId, deleted, archived, done), admin ? premiseTransformer::toPremiseDTOWithUserInfo : premiseTransformer::toPremiseDTO);

    }

    @Transactional(readOnly = true)
    public List<PremiseDetailDTO> getPremises(List<Integer> premiseIds) {
        //TODO use real user
        var userId = 1;
        premiseRepository.checkOwner(premiseIds, userId);

        return premiseRepository.getPremisesById(premiseIds).stream().map(premiseTransformer::toPremiseDetailDTO).toList();
    }



    @Transactional
    public Integer startCalculation(List<Integer> premises) {

        var userId = 1; // TODO get current user id

        var validSetId = propertySetRepository.getValidSetId();
        var validPeriodId = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new InternalErrorException("no valid period found that is VALID"));

        var calculationIds = new ArrayList<Integer>();

        premises.forEach(p -> {
            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));
        });

        //TODO set premise to completed.


        calculationIds.forEach(this::scheduleCalculation);
        return calculationStatusService.schedule(calculationIds);
    }

    //TODO: scheduled should be set by worker thread that processes the job.
    public void scheduleCalculation(Integer id) {
        calculationJobRepository.setStateTo(id, CalculationJobState.SCHEDULED);
        calculationExecutionService.calculateJob(id);
    }

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

    @Transactional
    public void updatePackaging(PackagingUpdateDTO packagingDTO) {

        //TODO check values. and return errors if needed
        var userId = 1; // todo get id from current user.
        premiseRepository.checkOwner(packagingDTO.getPremiseIds(), userId);

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

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

    }

    public void updateMaterial(MaterialUpdateDTO materialUpdateDTO) {
        //TODO check values. and return errors if needed
        var userId = 1; // todo get id from current user.
        premiseRepository.checkOwner(materialUpdateDTO.getPremiseIds(), userId);

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

    }

    public void updatePrice(PriceUpdateDTO priceUpdateDTO) {
        //TODO check values. and return errors if needed
        var userId = 1; // todo get id from current user.
        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) {
        //TODO check authorization
        var userId = 1;
        premiseRepository.checkOwner(premiseIds, userId);

        destinationService.deleteAllDestinationsByPremiseId(premiseIds, false);
        premiseRepository.deletePremisesById(premiseIds);
    }

}
