package de.avatic.lcc.service.calculation.execution.steps;

import de.avatic.lcc.model.calculation.ContainerCalculationResult;
import de.avatic.lcc.dto.generic.ContainerType;
import de.avatic.lcc.dto.generic.PalletType;
import de.avatic.lcc.model.db.packaging.PackagingDimension;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.db.utils.WeightUnit;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import org.jetbrains.annotations.Debug.Renderer;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;

/**
 * Service class responsible for calculating container loading patterns and capacities.
 * Handles different container types and packaging dimensions to determine optimal loading solutions.
 */
@Service
public class ContainerCalculationService {

    /**
     * Tolerance factor applied to packaging dimensions to account for practical loading conditions.
     * Current value is 2% (0.02).
     */
    private static final double DIMENSION_TOLERANCE = 0.02;

    /**
     * Repository for accessing system properties related to container calculations.
     */
    private final PropertyRepository propertyRepository;

    /**
     * Constructs a new ContainerCalculationService with the specified property repository.
     *
     * @param propertyRepository Repository for accessing system properties
     */
    public ContainerCalculationService(PropertyRepository propertyRepository) {
        this.propertyRepository = propertyRepository;
    }

    /**
     * Calculates the optimal loading pattern for a given handling unit and container type.
     *
     * @param hu            The handling unit dimensions to be loaded
     * @param containerType The type of container to be loaded
     * @return ContainerCalculationResult containing loading pattern and capacity information
     */
    public ContainerCalculationResult doCalculation(Integer setId, PackagingDimension hu, ContainerType containerType) {

        var weightInKg = BigDecimal.valueOf(WeightUnit.KG.convertFromG(hu.getWeight()));
        var maxContainerLoad = BigDecimal.valueOf(getMaxContainerLoad(containerType, setId));
        var maxUnitByWeight =  maxContainerLoad.divide(weightInKg, 0, RoundingMode.FLOOR).intValueExact();

        var dimensions = hu.withTolerance(DIMENSION_TOLERANCE);

        var solutionHorizontal = solveLayer(SolutionType.HORIZONTAL, dimensions, containerType.getLength(), containerType.getWidth());
        var solutionVertical = solveLayer(SolutionType.VERTICAL, dimensions, containerType.getWidth(), containerType.getLength());
        var bestSolution =  solutionHorizontal.getTotal() < solutionVertical.getTotal() ? solutionVertical : solutionHorizontal;
        int layers = getLayerCount(dimensions, containerType);

        if(PalletType.EURO_PALLET.fitsOn(hu) && bestSolution.getTotal() < containerType.getPalletCount(PalletType.EURO_PALLET)) {
            return new ContainerCalculationResult(Math.min(containerType.getPalletCount(PalletType.EURO_PALLET)*layers,maxUnitByWeight), layers, null, (containerType.getPalletCount(PalletType.EURO_PALLET)*layers) > maxUnitByWeight, containerType, dimensions, maxContainerLoad.intValueExact());
        } else if(PalletType.INDUSTRIAL_PALLET.fitsOn(hu) && bestSolution.getTotal() < containerType.getPalletCount(PalletType.INDUSTRIAL_PALLET)) {
            return new ContainerCalculationResult(Math.min(containerType.getPalletCount(PalletType.INDUSTRIAL_PALLET)*layers,maxUnitByWeight), layers, null, (containerType.getPalletCount(PalletType.INDUSTRIAL_PALLET)*layers) > maxUnitByWeight, containerType, dimensions, maxContainerLoad.intValueExact());
        }

        return createContainerCalculationResult(bestSolution, layers, maxUnitByWeight, containerType, dimensions, maxContainerLoad.intValueExact());
    }


    /**
     * Calculates the optimal loading pattern for a single layer in the container.
     *
     * @param type            The orientation type of the solution (horizontal or vertical)
     * @param dimension       The dimensions of the handling unit
     * @param containerWidth  The width of the container
     * @param containerLength The length of the container
     * @return Solution containing the optimal loading pattern for the layer
     */
    private static Solution solveLayer(SolutionType type, PackagingDimension dimension,
                                       int containerWidth, int containerLength) {

        var horizontalColumn = new Column(new Block(dimension, BlockType.HORIZONTAL), containerWidth);
        var verticalColumn = new Column(new Block(dimension, BlockType.VERTICAL), containerWidth);
        var circularColumn = new Column(new Block(dimension, BlockType.CIRCULAR), containerWidth);

        Solution bestSolution = new Solution(type,0, 0, 0, horizontalColumn, verticalColumn, circularColumn);

        int maxHorizontalColumns = containerLength / horizontalColumn.getColumnWidth();
        int maxVerticalColumns = containerLength / verticalColumn.getColumnWidth();
        int maxCircularColumns = containerLength / circularColumn.getColumnWidth();

        for (int horizontalColumnCount = 0; horizontalColumnCount <= maxHorizontalColumns; horizontalColumnCount++) {
            for (int verticalColumnCount = 0; verticalColumnCount <= maxVerticalColumns; verticalColumnCount++) {
                for (int circularColumnCount = 0; circularColumnCount <= maxCircularColumns; circularColumnCount++) {
                    // Check constraints
                    if (fitsInContainer(horizontalColumn, verticalColumn, circularColumn,
                            horizontalColumnCount, verticalColumnCount, circularColumnCount, containerLength)) {

                        Solution current = new Solution(type,
                                horizontalColumnCount, verticalColumnCount, circularColumnCount,
                                horizontalColumn, verticalColumn, circularColumn);

                        if (current.getTotal() > bestSolution.getTotal()) {
                            bestSolution = current;
                        }
                    }
                }
            }
        }

        return bestSolution;
    }

    /**
     * Checks if the given combination of columns fits within the container length.
     *
     * @param horizontalColumn Column with horizontal orientation
     * @param verticalColumn   Column with vertical orientation
     * @param circularColumn   Column with circular orientation
     * @param horizontalCount  Number of horizontal columns
     * @param verticalCount    Number of vertical columns
     * @param circularCount    Number of circular columns
     * @param containerLength  Length of the container
     * @return true if the combination fits, false otherwise
     */
    private static boolean fitsInContainer(Column horizontalColumn, Column verticalColumn, Column circularColumn,
                                           int horizontalCount, int verticalCount, int circularCount,
                                           int containerLength) {
        return horizontalColumn.getColumnWidth() * horizontalCount
                + verticalColumn.getColumnWidth() * verticalCount
                + circularColumn.getColumnWidth() * circularCount <= containerLength;
    }

    /**
     * Retrieves the maximum load capacity for a given container type from system properties.
     *
     * @param containerType The type of container
     * @return Maximum load capacity in weight units
     * @throws IllegalArgumentException if container type is not supported
     * @throws IllegalStateException    if required property is missing
     */
    private int getMaxContainerLoad(ContainerType containerType, Integer setId) {
        Map<ContainerType, SystemPropertyMappingId> mappings = Map.of(
                ContainerType.FEU, SystemPropertyMappingId.FEU_LOAD,
                ContainerType.TRUCK, SystemPropertyMappingId.TRUCK_LOAD,
                ContainerType.TEU, SystemPropertyMappingId.TEU_LOAD,
                ContainerType.HC, SystemPropertyMappingId.FEU_LOAD
        );

        SystemPropertyMappingId mappingId = mappings.get(containerType);
        if (mappingId == null) {
            throw new IllegalArgumentException("Unsupported container type: " + containerType);
        }

        var value = propertyRepository.getPropertyByMappingId(mappingId, setId)
                .orElseThrow(() -> new IllegalStateException("Missing property: " + mappingId))
                .getCurrentValue();

        return Integer.parseInt(value);
    }

    /**
     * Calculates how many layers of handling units can be stacked in the container.
     *
     * @param dimensions    The dimensions of the handling unit
     * @param containerType The type of container
     * @return Number of possible layers
     */
    private int getLayerCount(PackagingDimension dimensions, ContainerType containerType) {
        return containerType.getHeight()/dimensions.getHeight();
    }

    /**
     * Creates a container calculation result from the given parameters.
     *
     * @param solution         The optimal loading solution
     * @param layers           Number of possible layers
     * @param maxUnitByWeight  Maximum units allowed by weight
     * @param containerType    The type of container
     * @param hu               The handling unit dimensions
     * @param maxContainerLoad Maximum container load capacity
     * @return ContainerCalculationResult with all calculation details
     */
    private ContainerCalculationResult createContainerCalculationResult(
            Solution solution, int layers, int maxUnitByWeight,
            ContainerType containerType, PackagingDimension hu, int maxContainerLoad) {

        int totalUnits = Math.min(solution.getTotal() * layers, maxUnitByWeight);
        boolean weightExceeded = (solution.getTotal()* layers) > maxUnitByWeight;

        return new ContainerCalculationResult(
                totalUnits, layers, null, weightExceeded,
                containerType, hu, maxContainerLoad);
    }

    private enum BlockType {
        HORIZONTAL(1), VERTICAL(1), CIRCULAR(4);

        private final int resultValue;

        BlockType(int resultValue) {
            this.resultValue = resultValue;
        }

        public int getBlockColumnLength(PackagingDimension dimension) {
            return switch (this) {
                case HORIZONTAL -> dimension.getLength();
                case VERTICAL -> dimension.getWidth();
                case CIRCULAR -> dimension.getWidth() + dimension.getLength();
            };
        }

        public int getColumnWidth(PackagingDimension dimension) {
            return switch (this) {
                case VERTICAL -> dimension.getLength();
                case HORIZONTAL -> dimension.getWidth();
                case CIRCULAR -> dimension.getWidth() + dimension.getLength();
            };
        }

        public int getResultValue() {
            return resultValue;
        }

    }

    private enum SolutionType {
        HORIZONTAL, VERTICAL
    }

    @Renderer(text = "getFullDebugInfo()")
    private static class Solution {

        SolutionType type;

        Map<BlockType, Integer> rowCount = new HashMap<>();
        Map<BlockType, Column> columns = new HashMap<>();


        public Solution(SolutionType type, int horizontalCount, int verticalCount, int circularCount, Column horizontalColumn, Column verticalColumn, Column circularColumn) {
            rowCount.put(BlockType.HORIZONTAL, horizontalCount);
            rowCount.put(BlockType.VERTICAL, verticalCount);
            rowCount.put(BlockType.CIRCULAR, circularCount);
            columns.put(BlockType.HORIZONTAL, horizontalColumn);
            columns.put(BlockType.VERTICAL, verticalColumn);
            columns.put(BlockType.CIRCULAR, circularColumn);
            this.type = type;
        }

        public int getTotal() {
            return rowCount.get(BlockType.HORIZONTAL) * columns.get(BlockType.HORIZONTAL).getResultValue()
                    + rowCount.get(BlockType.VERTICAL) * columns.get(BlockType.VERTICAL).getResultValue()
                    + rowCount.get(BlockType.CIRCULAR) * columns.get(BlockType.CIRCULAR).getResultValue();
        }

        public String getFullDebugInfo() {
            return String.format("Solution [%s HUs: %d]",
                    type, getTotal());
        }
    }

    @Renderer(text = "getFullDebugInfo()")
    private static class Column {

        private final int blocksPerColumn;
        private final Block block;

        public Column(Block block, int columnLength) {
            this.block = block;
            this.blocksPerColumn = block.calculateBlockCount(columnLength);
        }

        public int getResultValue() {
            return block.getResultValue() * blocksPerColumn;
        }

        public int getColumnWidth() {
            return block.getBlockWidth();
        }

        public String getFullDebugInfo() {
            return "Column " + blocksPerColumn + "x" + block.getFullDebugInfo();
        }
    }

    @Renderer(text = "getFullDebugInfo()")
        private record Block(PackagingDimension dimension, BlockType type) {

        public int calculateBlockCount(int containerColumnLength) {
                return containerColumnLength / type.getBlockColumnLength(dimension);
            }

            public int getResultValue() {
                return type.getResultValue();
            }

            public int getBlockWidth() {
                return type.getColumnWidth(dimension);
            }

            public String getFullDebugInfo() {
                return "BLOCK %s W%s L%s H%s".formatted(type.name(), type.getColumnWidth(dimension), type.getBlockColumnLength(dimension), dimension.getHeight());
            }
        }
}
