package de.avatic.lcc.service.excelMapper;

import de.avatic.lcc.model.excel.ExcelNode;
import de.avatic.lcc.model.bulk.BulkInstruction;
import de.avatic.lcc.model.bulk.BulkInstructionType;
import de.avatic.lcc.model.bulk.HiddenTableType;
import de.avatic.lcc.model.bulk.header.HiddenCountryHeader;
import de.avatic.lcc.model.bulk.header.NodeHeader;
import de.avatic.lcc.model.db.country.Country;
import de.avatic.lcc.model.db.country.IsoCode;
import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.service.bulk.helper.ConstraintGenerator;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import de.avatic.lcc.util.exception.internalerror.ExcelValidationError;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellUtil;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class NodeExcelMapper {


    private final HeaderGenerator headerGenerator;
    private final NodeRepository nodeRepository;
    private final CountryRepository countryRepository;
    private final ConstraintGenerator constraintGenerator;

    public NodeExcelMapper(HeaderGenerator headerGenerator, NodeRepository nodeRepository, CountryRepository countryRepository, ConstraintGenerator constraintGenerator) {
        this.headerGenerator = headerGenerator;
        this.nodeRepository = nodeRepository;
        this.countryRepository = countryRepository;
        this.constraintGenerator = constraintGenerator;
    }

    public void fillSheet(Sheet sheet, CellStyle headerStyle) {
        headerGenerator.generateHeader(sheet, NodeHeader.class, headerStyle);
        nodeRepository.listAllNodes(false).forEach(node -> mapToRow(node, sheet.createRow(sheet.getLastRowNum() + 1)));
        headerGenerator.fixWidth(sheet, NodeHeader.class);
    }

    private void mapToRow(Node node, Row row) {
        row.createCell(NodeHeader.OPERATION.ordinal()).setCellValue(BulkInstructionType.UPDATE.name());
        row.createCell(NodeHeader.MAPPING_ID.ordinal()).setCellValue(node.getExternalMappingId());
        row.createCell(NodeHeader.NAME.ordinal()).setCellValue(node.getName());
        row.createCell(NodeHeader.ADDRESS.ordinal()).setCellValue(node.getAddress());
        row.createCell(NodeHeader.COUNTRY.ordinal()).setCellValue(countryRepository.getById(node.getCountryId()).orElseThrow().getIsoCode().getCode());
        row.createCell(NodeHeader.GEO_LATITUDE.ordinal()).setCellValue(node.getGeoLat().doubleValue());
        row.createCell(NodeHeader.GEO_LONGITUDE.ordinal()).setCellValue(node.getGeoLng().doubleValue());
        row.createCell(NodeHeader.IS_SOURCE.ordinal()).setCellValue(node.getSource() ? "true" : "false");
        row.createCell(NodeHeader.IS_INTERMEDIATE.ordinal()).setCellValue(node.getIntermediate() ? "true" : "false");
        row.createCell(NodeHeader.IS_DESTINATION.ordinal()).setCellValue(node.getDestination() ? "true" : "false");
        row.createCell(NodeHeader.OUTBOUND_COUNTRIES.ordinal()).setCellValue(mapOutboundCountriesToCell(node.getOutboundCountries()));
        row.createCell(NodeHeader.PREDECESSOR_NODES.ordinal()).setCellValue(mapChains(node.getNodePredecessors()));
        row.createCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).setCellValue(node.getPredecessorRequired() ? "true" : "false");
    }

    private String mapChains(List<Map<Integer, Integer>> chains) {
        return chains.stream().map(this::mapChain).collect(Collectors.joining("; "));
    }

    private String mapChain(Map<Integer, Integer> chain) {
        var seq = new ArrayList<>(chain.keySet());
        Collections.sort(seq);
        return seq.stream().map(s -> nodeRepository.getById(chain.get(s)).orElseThrow().getExternalMappingId()).collect(Collectors.joining(", "));
    }

    private String mapOutboundCountriesToCell(Collection<Integer> outboundCountryIds) {
        return outboundCountryIds.stream().map(countryRepository::getById).filter(Optional::isPresent).map(Optional::get).map(Country::getIsoCode).map(IsoCode::getCode).collect(Collectors.joining(", "));
    }

    public void createConstraints(Workbook workbook, Sheet sheet) {
        var namedRange = constraintGenerator.createReference(workbook, HiddenCountryHeader.ISO_CODE.ordinal(), HiddenTableType.COUNTRY_HIDDEN_TABLE);

        constraintGenerator.createFormulaListConstraint(sheet, NodeHeader.COUNTRY.ordinal(), namedRange.getNameName());
        constraintGenerator.createDecimalConstraint(sheet, NodeHeader.GEO_LATITUDE.ordinal(), -90.0, 90.0);
        constraintGenerator.createDecimalConstraint(sheet, NodeHeader.GEO_LONGITUDE.ordinal(), -180.0, 180.0);
        constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_SOURCE.ordinal());
        constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_INTERMEDIATE.ordinal());
        constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_DESTINATION.ordinal());
        constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal());
        constraintGenerator.createLengthConstraint(sheet, NodeHeader.ADDRESS.ordinal(), 1, 500);
        constraintGenerator.createLengthConstraint(sheet, NodeHeader.NAME.ordinal(), 1, 255);

        constraintGenerator.createEnumConstraint(sheet, NodeHeader.OPERATION.ordinal(), BulkInstructionType.class);

        constraintGenerator.createUniqueConstraint(sheet, NodeHeader.MAPPING_ID.ordinal());

    }

    public boolean isSheet(Sheet sheet) {
        try {
            headerGenerator.validateHeader(sheet, NodeHeader.class);
        } catch (ExcelValidationError e) {
            return false;
        }
        return true;
    }

    public List<BulkInstruction<ExcelNode>> extractSheet(Sheet sheet) {
        headerGenerator.validateHeader(sheet, NodeHeader.class);

        var nodes = new ArrayList<BulkInstruction<ExcelNode>>();

        sheet.forEach(row -> {
            if (row.getRowNum() == 0) return;

            if (!isEmpty(row))
                nodes.add(mapToEntity(row));
        });

        if(nodes.stream().map(BulkInstruction::getEntity).map(ExcelNode::getExternalMappingId).distinct().count() != nodes.size()) {
            throw new ExcelValidationError("Duplicate external mapping ids found in sheet");
        }

        return nodes;
    }

    private boolean isEmpty(Row row) {

        for(Cell cell : row) {
            if(cell != null) {
                if (cell.getCellType() != CellType.BLANK) {
                    return false;
                }
            }
        }

        return true;
    }

    private BulkInstruction<ExcelNode> mapToEntity(Row row) {
        ExcelNode entity = new ExcelNode();

        validateConstraints(row);

        entity.setExternalMappingId(row.getCell(NodeHeader.MAPPING_ID.ordinal()).getStringCellValue());
        entity.setName(row.getCell(NodeHeader.NAME.ordinal()).getStringCellValue());
        entity.setAddress(row.getCell(NodeHeader.ADDRESS.ordinal()).getStringCellValue());
        entity.setCountryId(IsoCode.valueOf(row.getCell(NodeHeader.COUNTRY.ordinal()).getStringCellValue()));

        entity.setGeoLat(mapGeoCoordinate(CellUtil.getCell(row, NodeHeader.GEO_LATITUDE.ordinal())));
        entity.setGeoLng(mapGeoCoordinate(CellUtil.getCell(row, NodeHeader.GEO_LONGITUDE.ordinal())));

        entity.setSource(Boolean.valueOf(row.getCell(NodeHeader.IS_SOURCE.ordinal()).getStringCellValue()));
        entity.setIntermediate(Boolean.valueOf(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()).getStringCellValue()));
        entity.setDestination(Boolean.valueOf(row.getCell(NodeHeader.IS_DESTINATION.ordinal()).getStringCellValue()));
        entity.setPredecessorRequired(Boolean.valueOf(row.getCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).getStringCellValue()));
        entity.setNodePredecessors(mapChainsFromCell(CellUtil.getCell(row, NodeHeader.PREDECESSOR_NODES.ordinal()).getStringCellValue()));
        entity.setOutboundCountries(mapOutboundCountriesFromCell(CellUtil.getCell(row, NodeHeader.OUTBOUND_COUNTRIES.ordinal()).getStringCellValue()));
        return new BulkInstruction<>(entity, BulkInstructionType.valueOf(row.getCell(NodeHeader.OPERATION.ordinal()).getStringCellValue()));
    }


    private void validateConstraints(Row row) {

        constraintGenerator.validateStringCell(row, NodeHeader.MAPPING_ID.ordinal());
        constraintGenerator.validateStringCell(row, NodeHeader.COUNTRY.ordinal());
        constraintGenerator.validateStringCell(row, NodeHeader.OUTBOUND_COUNTRIES.ordinal(), true);
        constraintGenerator.validateStringCell(row, NodeHeader.PREDECESSOR_NODES.ordinal(), true);
        constraintGenerator.validateStringCell(row, NodeHeader.NAME.ordinal());
        constraintGenerator.validateStringCell(row, NodeHeader.ADDRESS.ordinal());

        constraintGenerator.validateDecimalConstraint(row, NodeHeader.GEO_LATITUDE.ordinal(), -90.0, 90.0, true);
        constraintGenerator.validateDecimalConstraint(row, NodeHeader.GEO_LONGITUDE.ordinal(), -180.0, 180.0, true);

        constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_SOURCE.ordinal());
        constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_INTERMEDIATE.ordinal());
        constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_DESTINATION.ordinal());
        constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal());
        constraintGenerator.validateLengthConstraint(row, NodeHeader.ADDRESS.ordinal(), 1, 500);
        constraintGenerator.validateLengthConstraint(row, NodeHeader.NAME.ordinal(), 1, 255);
        constraintGenerator.validateEnumConstraint(row, NodeHeader.OPERATION.ordinal(), BulkInstructionType.class);

    }

    private BigDecimal mapGeoCoordinate(Cell geoCoordinate) {
        if (geoCoordinate == null) return null;
        if(geoCoordinate.getCellType() == CellType.BLANK) return null;
        return BigDecimal.valueOf(geoCoordinate.getNumericCellValue());
    }

    private List<String> mapOutboundCountriesFromCell(String outboundCountryIds) {
        if (outboundCountryIds == null || outboundCountryIds.isBlank()) return Collections.emptyList();
        return Arrays.stream(outboundCountryIds.split(",")).map(String::trim).toList();
    }

    private List<List<String>> mapChainsFromCell(String cell) {
        if (cell == null || cell.isBlank()) return Collections.emptyList();
        return Arrays.stream(cell.split(";")).map(String::trim).map(this::mapChainFromCell).toList();
    }

    private List<String> mapChainFromCell(String chain) {
        return Arrays.stream(chain.split(",")).map(String::trim).toList();
    }

}
