package de.avatic.lcc.service.calculation;

import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
import de.avatic.lcc.model.db.packaging.PackagingDimension;
import de.avatic.lcc.model.db.packaging.PackagingType;
import de.avatic.lcc.model.db.premises.Premise;
import de.avatic.lcc.model.db.premises.PremiseState;
import de.avatic.lcc.model.db.properties.PackagingProperty;
import de.avatic.lcc.model.db.properties.PackagingPropertyMappingId;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.repositories.MaterialRepository;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.packaging.PackagingDimensionRepository;
import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository;
import de.avatic.lcc.repositories.packaging.PackagingRepository;
import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.access.DestinationService;
import de.avatic.lcc.service.access.PropertyService;
import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.api.TaxationResolverService;
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.badrequest.InvalidArgumentException;
import de.avatic.lcc.util.exception.badrequest.NotFoundException;
import de.avatic.lcc.util.exception.base.ForbiddenException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

@Service
public class PremiseCreationService {
    private final PremiseRepository premiseRepository;
    private final PremiseTransformer premiseTransformer;
    private final DestinationService destinationService;
    private final UserNodeRepository userNodeRepository;
    private final NodeRepository nodeRepository;
    private final MaterialRepository materialRepository;
    private final DimensionTransformer dimensionTransformer;
    private final PackagingRepository packagingRepository;
    private final PackagingDimensionRepository packagingDimensionRepository;
    private final PackagingPropertiesRepository packagingPropertiesRepository;
    private final AuthorizationService authorizationService;
    private final TaxationResolverService taxationResolverService;
    private final PropertyService propertyService;

    public PremiseCreationService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DestinationService destinationService, UserNodeRepository userNodeRepository, NodeRepository nodeRepository, MaterialRepository materialRepository, DimensionTransformer dimensionTransformer, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, CustomApiService customApiService, AuthorizationService authorizationService, TaxationResolverService taxationResolverService, PropertyService propertyService) {
        this.premiseRepository = premiseRepository;
        this.premiseTransformer = premiseTransformer;
        this.destinationService = destinationService;
        this.userNodeRepository = userNodeRepository;
        this.nodeRepository = nodeRepository;
        this.materialRepository = materialRepository;
        this.dimensionTransformer = dimensionTransformer;
        this.packagingRepository = packagingRepository;
        this.packagingDimensionRepository = packagingDimensionRepository;
        this.packagingPropertiesRepository = packagingPropertiesRepository;

        this.authorizationService = authorizationService;
        this.taxationResolverService = taxationResolverService;
        this.propertyService = propertyService;
    }

    @Transactional
    public List<PremiseDetailDTO> createPremises(List<Integer> materialIds, List<Integer> supplierIds, List<Integer> userSupplierIds, boolean createEmpty) {
        var userId = authorizationService.getUserId();
        userNodeRepository.checkOwner(userSupplierIds, userId);

        Optional<Double> defaultTariff = propertyService.getProperty(SystemPropertyMappingId.TARIFF_RATE);

        /* Build all resulting premises */
        List<TemporaryPremise> premises = Stream.concat(
                supplierIds.stream().flatMap(id -> materialIds.stream().map(materialId -> new TemporaryPremise(materialId, id, null, false))),
                userSupplierIds.stream().flatMap(id -> materialIds.stream().map(materialId -> new TemporaryPremise(materialId, null, id, true)))).toList();

        premises.forEach(p -> findExistingPremise(p, createEmpty, userId));

        premises.forEach(p -> verifyNode(p, userId));
        verifyMaterial(materialIds);

        List<TaxationResolverService.TaxationResolverResponse> tariffs = taxationResolverService.getTariffRates(premises.stream().map(p -> new TaxationResolverService.TaxationResolverRequest(materialRepository.getById(p.getMaterialId()).orElseThrow(), p.getCountryId())).distinct().toList());

        premises.forEach(p -> {
            if (p.getPremise() == null) { // create new

                p.setId(premiseRepository.insert(p.getMaterialId(), p.getSupplierId(), p.getUserSupplierId(), p.getGeoLat(), p.getGeoLng(), p.getCountryId(), userId));
                fillPremise(p, tariffs, defaultTariff, userId);

            } else if (p.getPremise().getState().equals(PremiseState.DRAFT)) { // recycle
                p.setId(p.getPremise().getId());
                if (createEmpty) {
                    // reset to defaults.
                    fillPremise(p, tariffs, defaultTariff, userId);
                    // remove destinations
                    destinationService.deleteAllDestinationsByPremiseId(Collections.singletonList(p.getId()), false);
                }

            } else if (p.getPremise().getState().equals(PremiseState.COMPLETED)) {
                // create a copy
                p.setId(premiseRepository.insert(p.getMaterialId(), p.getSupplierId(), p.getUserSupplierId(), p.getGeoLat(), p.getGeoLng(), p.getCountryId(), userId));
                copyPremise(p, userId);
                destinationService.duplicate(p.getPremise().getId(), p.getId());
            }
        });

        return premiseRepository.getPremisesById(premises.stream().map(TemporaryPremise::getId).toList()).stream().map(premiseTransformer::toPremiseDetailDTO).toList();
    }


    private void copyPremise(TemporaryPremise p, Integer userId) {
        var old = p.getPremise();

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

    private void fillPremise(TemporaryPremise p, List<TaxationResolverService.TaxationResolverResponse> tariffs, Optional<Double> optTariff, Integer userId) {

        if (!p.isUserSupplier()) {
            var packaging = packagingRepository.getByMaterialIdAndSupplierId(p.getMaterialId(), p.getSupplierId());
            Optional<PackagingDimension> hu = packaging.flatMap(o -> packagingDimensionRepository.getById(o.getHuId()));
            Optional<PackagingDimension> shu = packaging.flatMap(o -> packagingDimensionRepository.getById(o.getShuId()));

            if (hu.isPresent() && shu.isPresent()) {
                boolean stackable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.get().getId(), PackagingPropertyMappingId.STACKABLE.name()).map(PackagingProperty::getValue).map(Boolean::valueOf).orElse(false);
                boolean mixable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.get().getId(), PackagingPropertyMappingId.MIXABLE.name()).map(PackagingProperty::getValue).map(Boolean::valueOf).orElse(false);
                premiseRepository.resetPackaging(Collections.singletonList(p.getId()), hu.get(), shu.get(), stackable, mixable); //TODO clarify if the hu unit count in packaging data is total unit count or shu count (shu*hu or hu)
                premiseRepository.setPackagingId(p.getId(), packaging.get().getId());
            } else {
                premiseRepository.resetPackaging(Collections.singletonList(p.getId()), PackagingDimension.getEmpty(PackagingType.HU), PackagingDimension.getEmpty(PackagingType.SHU), true, true);
                premiseRepository.setPackagingId(p.getId(), null);
            }

            premiseRepository.resetPrice(Collections.singletonList(p.getId()));
        }

        var defaultTariff = optTariff.orElse(null);

        tariffs.stream()
                .filter(r -> r.material().getId().equals(p.getMaterialId()) && r.countryId().equals(p.getCountryId()))
                .findFirst()
                .ifPresent(value -> premiseRepository.updateMaterial(Collections.singletonList(
                                p.getId()),
                        value.actualHsCode(),
                        value.tariffRate() == null ? (defaultTariff == null ? null : BigDecimal.valueOf(defaultTariff)) : BigDecimal.valueOf(value.tariffRate()),
                        value.tariffRate() == null));
    }

    private void findExistingPremise(TemporaryPremise premise, boolean createEmpty, Integer userId) {
        var existingPremises = premiseRepository.findByMaterialIdAndSupplierId(premise.getMaterialId(), premise.getSupplierId(), premise.getUserSupplierId(), userId);
        Premise existingDraft = existingPremises.stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).findFirst().orElse(null);

        if (existingDraft != null) {
            premise.setPremise(existingDraft);
        } else if (!createEmpty) {
            Premise youngestCompleted = existingPremises.stream().filter(p -> p.getState().equals(PremiseState.COMPLETED) && p.getUserId().equals(userId)).findFirst().orElse(null);

            // no completed from current user, check if completed from other user.
            if (youngestCompleted == null) {
                youngestCompleted = existingPremises.stream().filter(p -> p.getState().equals(PremiseState.COMPLETED)).findFirst().orElse(null);
            }

            premise.setPremise(youngestCompleted);
        }
    }

    /**
     * Checks if the node of the given temporary premiss exists, and updates data in
     * temporary premiss.
     *
     * @param temporaryPremise
     */
    private void verifyNode(TemporaryPremise temporaryPremise, Integer userId) {

        var node = temporaryPremise.isUserSupplier() ?
                userNodeRepository.getById(temporaryPremise.getUserSupplierId()).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.USER_NODE, "id", String.valueOf(temporaryPremise.getUserSupplierId()))) :
                nodeRepository.getById(temporaryPremise.getSupplierId()).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.NODE, "id", String.valueOf(temporaryPremise.getSupplierId())));

        if (temporaryPremise.isUserSupplier()) {
            var id = userNodeRepository.getOwnerById(temporaryPremise.getUserSupplierId());

            if (id.isPresent() && !id.get().equals(userId)) {
                throw new ForbiddenException("Unable to access this node id " + temporaryPremise.getUserSupplierId());
            }
        }

        if (!node.getSource())
            throw new InvalidArgumentException((temporaryPremise.isUserSupplier() ? "User node" : "Node") + " with id " + node.getId() + " is not a source node.");

        temporaryPremise.setGeoLat(node.getGeoLat());
        temporaryPremise.setGeoLng(node.getGeoLng());
        temporaryPremise.setCountryId(node.getCountryId());

    }

    private void verifyMaterial(List<Integer> materialIds) {
        var missingMaterials = materialRepository.findMissingIds(materialIds);

        if (!missingMaterials.isEmpty())
            throw new InvalidArgumentException("Material with ids " + missingMaterials + " does not exist.");
    }

    private static class TemporaryPremise {


        private Integer materialId;
        private Integer supplierId;
        private Integer userSupplierId;
        private boolean isUserSupplier;

        /* found premise */
        private Premise premise;

        private BigDecimal geoLat;
        private BigDecimal geoLng;

        private Integer countryId;

        /* id of the premise used (can be newly created or recycled one)*/
        private Integer id;


        public TemporaryPremise(Integer materialId, Integer supplierId, Integer userSupplierId, boolean isUserSupplier) {
            this.materialId = materialId;
            this.supplierId = supplierId;
            this.userSupplierId = userSupplierId;
            this.isUserSupplier = isUserSupplier;
            this.premise = null;

        }

        public Integer getMaterialId() {
            return materialId;
        }

        public void setMaterialId(Integer materialId) {
            this.materialId = materialId;
        }

        public Integer getSupplierId() {
            return supplierId;
        }

        public void setSupplierId(Integer supplierId) {
            this.supplierId = supplierId;
        }

        public Integer getUserSupplierId() {
            return userSupplierId;
        }

        public void setUserSupplierId(Integer userSupplierId) {
            this.userSupplierId = userSupplierId;
        }

        public boolean isUserSupplier() {
            return isUserSupplier;
        }

        public void setUserSupplier(boolean userSupplier) {
            isUserSupplier = userSupplier;
        }

        public Premise getPremise() {
            return premise;
        }

        public void setPremise(Premise premise) {
            this.premise = premise;
        }

        public BigDecimal getGeoLat() {
            return this.geoLat;
        }

        public void setGeoLat(BigDecimal geoLat) {
            this.geoLat = geoLat;
        }

        public BigDecimal getGeoLng() {
            return this.geoLng;
        }

        public void setGeoLng(BigDecimal geoLng) {
            this.geoLng = geoLng;
        }

        public Integer getCountryId() {
            return this.countryId;
        }

        public void setCountryId(Integer countryId) {
            this.countryId = countryId;
        }

        public void addFoundPremises(List<Premise> foundPremises) {

        }

        public Integer getId() {
            return this.id;
        }

        public void setId(Integer id) {
            this.id = id;
        }
    }
}
