package de.votian.services.service;

import de.votian.services.dto.JobPricingResultDto;
import de.votian.services.dto.JobTourServiceDraftRowDto;
import de.votian.services.dto.TourDetailDto;
import de.votian.services.entity.Address;
import de.votian.services.entity.CostCenter;
import de.votian.services.entity.CostCenterAddress;
import de.votian.services.entity.Job;
import de.votian.services.entity.ServiceZone;
import de.votian.services.entity.ServiceZoneMapping;
import de.votian.services.entity.ServiceZipCode;
import de.votian.services.repository.AddressRepository;
import de.votian.services.repository.CostCenterAddressRepository;
import de.votian.services.repository.CostCenterRepository;
import de.votian.services.repository.ServiceZoneMappingRepository;
import de.votian.services.repository.ServiceZoneRepository;
import de.votian.services.repository.ServiceZipCodeRepository;
import org.springframework.stereotype.Service;
import org.w3c.dom.Element;
import java.math.BigDecimal;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Service
public class LegacyXmlIntegrationService {

    private static final String BASIC_PRICE_SERVICE = "Grundpreis";
    private static final String SERVICE_PRICE_SERVICE = "Servicepreis";

    private final LegacyXmlCustomerAuthService authService;
    private final CostCenterRepository costCenterRepository;
    private final CostCenterAddressRepository costCenterAddressRepository;
    private final AddressRepository addressRepository;
    private final ServiceZoneRepository serviceZoneRepository;
    private final ServiceZoneMappingRepository serviceZoneMappingRepository;
    private final ServiceZipCodeRepository serviceZipCodeRepository;
    private final JobPricingService jobPricingService;

    public LegacyXmlIntegrationService(LegacyXmlCustomerAuthService authService,
                                       CostCenterRepository costCenterRepository,
                                       CostCenterAddressRepository costCenterAddressRepository,
                                       AddressRepository addressRepository,
                                       ServiceZoneRepository serviceZoneRepository,
                                       ServiceZoneMappingRepository serviceZoneMappingRepository,
                                       ServiceZipCodeRepository serviceZipCodeRepository,
                                       JobPricingService jobPricingService) {
        this.authService = authService;
        this.costCenterRepository = costCenterRepository;
        this.costCenterAddressRepository = costCenterAddressRepository;
        this.addressRepository = addressRepository;
        this.serviceZoneRepository = serviceZoneRepository;
        this.serviceZoneMappingRepository = serviceZoneMappingRepository;
        this.serviceZipCodeRepository = serviceZipCodeRepository;
        this.jobPricingService = jobPricingService;
    }

    public String handleCostcenterRequest(String xmlRequest) {
        try {
            Element request = parseRequest(xmlRequest, "costcenter");
            boolean debug = isDebugEnabled(request);
            LegacyXmlCustomerAuthService.AuthContext auth = authenticateCustomer(request);
            String operation = upper(text(request, "operation"), "GET");
            if ("SET".equals(operation)) {
                throw new LegacyXmlRequestException("104", "SET wird aktuell nicht unterstuetzt.");
            }
            Integer addressType = parseInteger(text(request, "address_type"), 4);
            List<CostCenter> costCenters = auth.requestedCostCenter() != null
                    ? List.of(auth.requestedCostCenter())
                    : costCenterRepository.findByCustomerId(auth.customer().getId()).stream()
                    .sorted(Comparator.comparing(CostCenter::getName, Comparator.nullsLast(String::compareToIgnoreCase)))
                    .toList();

            StringBuilder payload = new StringBuilder("<costcenters>");
            for (CostCenter costCenter : costCenters) {
                CostCenterAddress costCenterAddress = costCenterAddressRepository
                        .findByCostCenterIdAndAddressTypeId(costCenter.getId(), addressType)
                        .orElse(null);
                if (costCenterAddress == null) {
                    continue;
                }
                Address address = costCenterAddress.getAddressId() != null
                        ? addressRepository.findById(Objects.requireNonNull(costCenterAddress.getAddressId())).orElse(null)
                        : null;
                payload.append("<costcenter>")
                        .append(tagCData("costcenter_name", costCenter.getName()))
                        .append(tagCData("costcenter_pre_no", stringValue(costCenter.getParentId())))
                        .append(tagCData("street", address != null ? address.getStreet() : ""))
                        .append(tagCData("zipcode", address != null ? address.getZipcode() : ""))
                        .append(tagCData("city", address != null ? address.getCity() : ""))
                        .append(tagCData("country", address != null ? address.getCountry() : ""))
                        .append(tagCData("hsno", costCenterAddress.getHsno()))
                        .append(tagCData("floor", costCenterAddress.getFloor()))
                        .append(tagCData("person", costCenterAddress.getPerson()))
                        .append(tagCData("phone", costCenterAddress.getPhone()))
                        .append(tagCData("email", costCenterAddress.getEmail()))
                        .append(tagCData("remark", costCenterAddress.getRemark()))
                        .append(tagCData("comp", costCenterAddress.getComp()))
                        .append(tagCData("comp2", costCenterAddress.getComp2()))
                        .append(tagCData("comp3", costCenterAddress.getComp3()))
                        .append(tagCData("comp4", costCenterAddress.getComp4()))
                        .append("</costcenter>");
            }
            payload.append("</costcenters>");
            return ok(payload.toString(), debugMessage(debug, "costcenter", auth.customer().getEid()));
        } catch (LegacyXmlRequestException ex) {
            return error(ex, null);
        } catch (Exception ex) {
            return error(new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen."), null);
        }
    }

    public String handleZoneRequest(String xmlRequest) {
        try {
            Element request = parseRequest(xmlRequest, "zone");
            boolean debug = isDebugEnabled(request);
            LegacyXmlCustomerAuthService.AuthContext auth = authenticateCustomer(request);
            String operation = upper(text(request, "operation"), "GET");
            if ("SET".equals(operation)) {
                throw new LegacyXmlRequestException("104", "SET wird aktuell nicht unterstuetzt.");
            }
            String zipcode = normalizeZipcode(text(request, "eco_zipcode"));
            if (zipcode.isBlank()) {
                throw new LegacyXmlRequestException("102", "Die PLZ ist keiner Zone zugeordnet!");
            }
            ServiceZipCode serviceZipCode = serviceZipCodeRepository.findByZipcode(zipcode).orElse(null);
            if (serviceZipCode == null) {
                throw new LegacyXmlRequestException("102", "Die PLZ ist keiner Zone zugeordnet! [" + zipcode + "]");
            }
            List<ServiceZone> zones = serviceZoneRepository.findByHeadquartersIdAndCustomerIdOrderByZoneNo(
                    auth.customer().getHeadquartersId(),
                    auth.customer().getId()
            );
            Map<Long, ServiceZone> zonesById = new LinkedHashMap<>();
            for (ServiceZone zone : zones) {
                zonesById.put(zone.getId(), zone);
            }
            ServiceZone resolvedZone = null;
            if (!zonesById.isEmpty()) {
                for (ServiceZoneMapping mapping : serviceZoneMappingRepository.findByZoneIdIn(zonesById.keySet())) {
                    if (Objects.equals(mapping.getZipCodeId(), serviceZipCode.getId())) {
                        resolvedZone = zonesById.get(mapping.getZoneId());
                        if (resolvedZone != null) {
                            break;
                        }
                    }
                }
            }
            if (resolvedZone == null) {
                throw new LegacyXmlRequestException("102", "Die PLZ ist keiner Zone zugeordnet! [" + zipcode + "]");
            }
            String payload = "<zone>"
                    + tag("zone_no", stringValue(resolvedZone.getZoneNo()))
                    + tagCData("zone_name", resolvedZone.getName())
                    + "</zone>";
            return ok(payload, debugMessage(debug, "zone", zipcode));
        } catch (LegacyXmlRequestException ex) {
            return error(ex, null);
        } catch (Exception ex) {
            return error(new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen."), null);
        }
    }

    public String handlePriceRequest(String xmlRequest) {
        try {
            Element request = parseRequest(xmlRequest, "costs");
            boolean debug = isDebugEnabled(request);
            LegacyXmlCustomerAuthService.AuthContext auth = authenticateCustomer(request);
            String operation = upper(text(request, "operation"), "GET");
            if ("SET".equals(operation)) {
                throw new LegacyXmlRequestException("104", "SET wird aktuell nicht unterstuetzt.");
            }

            String serviceFunction = text(request, "service_function");
            if (serviceFunction.isBlank()) {
                serviceFunction = "default";
            }
            if (!"default".equalsIgnoreCase(serviceFunction) && !"zipcode".equalsIgnoreCase(serviceFunction)) {
                throw new LegacyXmlRequestException("104", "Die Funktion wird aktuell nicht unterstuetzt.");
            }

            String zipcodeFrom = normalizeZipcode(text(request, "zipcode_from"));
            String zipcodeTo = normalizeZipcode(text(request, "zipcode_to"));
            Integer vehicleTypeId = parseInteger(text(request, "vehicle_type_id"), null);
            String priceMode = text(request, "price_mode");
            if (priceMode.isBlank()) {
                priceMode = "1";
            }

            Job draft = new Job();
            draft.setHeadquartersId(auth.customer().getHeadquartersId());
            if (auth.effectiveCostCenter() != null) {
                draft.setCostCenterPayerId(auth.effectiveCostCenter().getId());
            }
            draft.setVehicleTypeId(vehicleTypeId);
            draft.setOrderTime(resolvePricingDateTime(request));

            List<TourDetailDto> tours = new ArrayList<>();
            tours.add(createTour(0, zipcodeFrom));
            tours.add(createTour(1, zipcodeTo));

            JobPricingResultDto result = jobPricingService.calculateDraft(draft, List.of(), tours);
            BigDecimal priceValue1 = BigDecimal.ZERO;
            BigDecimal priceValue2 = BigDecimal.ZERO;
            for (JobTourServiceDraftRowDto row : result.getTourServices()) {
                if (row.getServiceName() == null || row.getPrice() == null) {
                    continue;
                }
                if (BASIC_PRICE_SERVICE.equalsIgnoreCase(row.getServiceName())) {
                    priceValue1 = priceValue1.add(row.getPrice());
                } else if (!SERVICE_PRICE_SERVICE.equalsIgnoreCase(row.getServiceName())) {
                    priceValue2 = priceValue2.add(row.getPrice());
                }
            }
            BigDecimal priceSum = priceValue1.add(priceValue2);
            String payload = "<costs>"
                    + tag("date", draft.getOrderTime().toLocalDate().toString())
                    + tag("vehicle_type_id", stringValue(vehicleTypeId))
                    + tag("zipcode_from", zipcodeFrom)
                    + tag("zipcode_to", zipcodeTo)
                    + tag("price_mode", priceMode)
                    + tag("price_value_1", decimalValue(priceValue1))
                    + tag("price_value_2", decimalValue(priceValue2))
                    + tag("price_value_sum", decimalValue(priceSum))
                    + "</costs>";
            return ok(payload, debugMessage(debug, "price", zipcodeFrom + "->" + zipcodeTo));
        } catch (LegacyXmlRequestException ex) {
            return error(ex, null);
        } catch (Exception ex) {
            return error(new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen."), null);
        }
    }

    private LegacyXmlCustomerAuthService.AuthContext authenticateCustomer(Element request) {
        Element auth = child(request, "auth");
        if (auth == null) {
            throw new LegacyXmlRequestException("101", "Die Authentifizierungsdaten sind nicht in Ordnung.");
        }
        return authService.authenticate(new LegacyXmlCustomerAuthService.AuthRequest(
                text(auth, "auth_type"),
                text(auth, "customer"),
                text(auth, "account"),
                text(auth, "password"),
                text(auth, "session_id"),
                text(auth, "costcenter_name")
        ));
    }

    private LocalDateTime resolvePricingDateTime(Element request) {
        Integer day = parseInteger(text(request, "day"), null);
        Integer month = parseInteger(text(request, "month"), null);
        Integer year = parseInteger(text(request, "year"), null);
        if (day == null || month == null || year == null) {
            return LocalDateTime.now();
        }
        try {
            return LocalDate.of(year, month, day).atStartOfDay();
        } catch (DateTimeException ex) {
            return LocalDateTime.now();
        }
    }

    private TourDetailDto createTour(int sort, String zipcode) {
        TourDetailDto tour = new TourDetailDto();
        tour.setSort(sort);
        tour.setZipcode(zipcode);
        return tour;
    }

    private Element parseRequest(String xmlRequest, String requestTag) throws Exception {
        return LegacyXmlXmlParsing.parseRequest(xmlRequest, requestTag);
    }

    private Element child(Element parent, String tagName) {
        return LegacyXmlXmlParsing.child(parent, tagName);
    }

    private String text(Element parent, String tagName) {
        return LegacyXmlXmlParsing.text(parent, tagName);
    }

    private boolean isDebugEnabled(Element request) {
        String value = upper(text(request, "debug_on"), "");
        return "ON".equals(value) || "YES".equals(value);
    }

    private String upper(String value, String fallback) {
        return value == null || value.isBlank() ? fallback : value.trim().toUpperCase();
    }

    private Integer parseInteger(String value, Integer fallback) {
        if (value == null || value.isBlank()) {
            return fallback;
        }
        try {
            return Integer.valueOf(value.trim());
        } catch (NumberFormatException ex) {
            return fallback;
        }
    }

    private String normalizeZipcode(String value) {
        if (value == null) {
            return "";
        }
        String digits = value.trim();
        if (digits.length() >= 5) {
            return digits.substring(0, 5);
        }
        return "0".repeat(Math.max(0, 5 - digits.length())) + digits;
    }

    private String ok(String payload, String debug) {
        return "<xml><state>OK</state>" + payload + debugTag(debug) + "</xml>";
    }

    private String error(LegacyXmlRequestException ex, String debug) {
        return "<xml><state>NOT OK</state><errors><error>"
                + tag("err_no", ex.getErrorCode())
                + tagCData("err_desc", ex.getDescription())
                + "</error></errors>"
                + debugTag(debug)
                + "</xml>";
    }

    private String debugMessage(boolean enabled, String type, String detail) {
        return enabled ? type + ": " + detail : "";
    }

    private String debugTag(String debug) {
        return debug == null || debug.isBlank() ? "" : tagCData("debug", debug);
    }

    private String tag(String name, String value) {
        return "<" + name + ">" + escapeXml(value) + "</" + name + ">";
    }

    private String tagCData(String name, String value) {
        return "<" + name + "><![CDATA[" + cdata(value) + "]]></" + name + ">";
    }

    private String cdata(String value) {
        return value == null ? "" : value.replace("]]>", "]]]]><![CDATA[>");
    }

    private String escapeXml(String value) {
        if (value == null) {
            return "";
        }
        return value
                .replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&apos;");
    }

    private String stringValue(Object value) {
        return value == null ? "" : value.toString();
    }

    private String decimalValue(BigDecimal value) {
        return value == null ? "0.00" : value.toPlainString();
    }
}
