package de.votian.services.service;

import de.votian.services.dto.JobPricingResultDto;
import de.votian.services.dto.JobTourServiceDraftRowDto;
import de.votian.services.entity.Job;
import de.votian.services.entity.ServiceEntity;
import de.votian.services.entity.ServiceType;
import de.votian.services.repository.ServiceEntityRepository;
import de.votian.services.repository.ServiceTypeRepository;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@Transactional
public class JobTourServiceWriteService {

    private static final String COUNT_SQL =
            "SELECT COUNT(*) FROM tourservice WHERE jb_id = :jobId";
    private static final String SELECT_ROWS_SQL =
            "SELECT tr_sort, trs_srv_name FROM tourservice WHERE jb_id = :jobId";
    private static final String INSERT_SQL =
            "INSERT INTO tourservice (jb_id, csc_id, tr_sort, srv_id, trs_srv_name, srvt_id, trs_srvt_name, trs_price, trs_discount) " +
                    "VALUES (:jobId, :costCenterId, :tourSort, :serviceId, :serviceName, :serviceTypeId, :serviceTypeName, :price, :discount)";
    private static final String UPDATE_SQL =
            "UPDATE tourservice SET csc_id = :costCenterId, srv_id = :serviceId, srvt_id = :serviceTypeId, trs_srvt_name = :serviceTypeName, trs_price = :price, trs_discount = :discount " +
                    "WHERE jb_id = :jobId AND tr_sort = :tourSort AND trs_srv_name = :serviceName";
    private static final String DELETE_SQL =
            "DELETE FROM tourservice WHERE jb_id = :jobId AND tr_sort = :tourSort AND trs_srv_name = :serviceName";
    private static final String FIXPRICE_SERVICE_NAME = "Fixpreis";
    private static final String SERVICEPRICE_SERVICE_NAME = "Servicepreis";
    private static final int LEGACY_ACTIVE_MODE = 0;

    private final EntityManager entityManager;
    private final ServiceEntityRepository serviceEntityRepository;
    private final ServiceTypeRepository serviceTypeRepository;

    public JobTourServiceWriteService(EntityManager entityManager) {
        this(entityManager, null, null);
    }

    @Autowired
    public JobTourServiceWriteService(EntityManager entityManager,
                                      ServiceEntityRepository serviceEntityRepository,
                                      ServiceTypeRepository serviceTypeRepository) {
        this.entityManager = entityManager;
        this.serviceEntityRepository = serviceEntityRepository;
        this.serviceTypeRepository = serviceTypeRepository;
    }

    public void createBasePriceRows(Job job) {
        if (job == null || job.getId() == null || hasPersistedRows(job.getId())) {
            return;
        }

        Long costCenterId = resolveCostCenterId(job);
        BigDecimal servicePrice = amount(job.getServicePrice());
        BigDecimal basePrice = deriveBasePrice(job, servicePrice);

        if (basePrice.compareTo(BigDecimal.ZERO) != 0) {
            insertRow(job.getId(), job.getHeadquartersId(), costCenterId, 0, FIXPRICE_SERVICE_NAME, "", basePrice, BigDecimal.ZERO);
        }
        if (servicePrice.compareTo(BigDecimal.ZERO) != 0) {
            insertRow(job.getId(), job.getHeadquartersId(), costCenterId, 0, SERVICEPRICE_SERVICE_NAME, "", servicePrice, BigDecimal.ZERO);
        }
    }

    public void createBasePriceRows(Job job, JobPricingResultDto pricingResult) {
        if (job == null || job.getId() == null || hasPersistedRows(job.getId())) {
            return;
        }
        if (pricingResult == null || pricingResult.getJob() == null) {
            createBasePriceRows(job);
            return;
        }
        if (!pricingResult.getTourServices().isEmpty()) {
            insertManagedRows(job.getId(), job.getHeadquartersId(), resolveCostCenterId(job), pricingResult.getTourServices());
            return;
        }

        Job recalculated = pricingResult.getJob();
        Long costCenterId = resolveCostCenterId(job);
        BigDecimal discountRate = amount(pricingResult.getDiscountRate());
        ServiceRowAmounts serviceRowAmounts = resolveServiceRowAmounts(job, pricingResult, recalculated, discountRate);
        BigDecimal servicePrice = serviceRowAmounts.price();
        BigDecimal serviceDiscount = serviceRowAmounts.discount();

        if (hasAmount(job.getFixPrice())) {
            BigDecimal totalPrice = amount(recalculated.getTotalPrice() != null ? recalculated.getTotalPrice() : job.getTotalPrice());
            BigDecimal serviceBusinessVolume = effectiveBusinessVolume(serviceRowAmounts);
            BigDecimal baseBusinessVolume = totalPrice.subtract(serviceBusinessVolume).setScale(2, RoundingMode.HALF_UP);
            BigDecimal basePrice = reverseDiscount(baseBusinessVolume, discountRate);
            if (basePrice.compareTo(BigDecimal.ZERO) > 0) {
                insertRow(job.getId(), job.getHeadquartersId(), costCenterId, 0, FIXPRICE_SERVICE_NAME, "", basePrice, discountRate);
            }
        }

        if (servicePrice.compareTo(BigDecimal.ZERO) != 0) {
            insertRow(job.getId(), job.getHeadquartersId(), costCenterId, 0, SERVICEPRICE_SERVICE_NAME, "", servicePrice, serviceDiscount);
        }
    }

    public void syncBasePriceRows(Job job, JobPricingResultDto pricingResult) {
        if (job == null || job.getId() == null || pricingResult == null || pricingResult.getJob() == null) {
            return;
        }

        List<PersistedRow> persistedRows = loadPersistedRows(job.getId());
        if (!pricingResult.getTourServices().isEmpty()) {
            syncManagedRows(job.getId(), job.getHeadquartersId(), resolveCostCenterId(job), persistedRows, pricingResult.getTourServices());
            return;
        }
        if (persistedRows.stream().anyMatch(row -> !isManagedBaseRow(row))) {
            return;
        }

        Job recalculated = pricingResult.getJob();
        BigDecimal discountRate = amount(pricingResult.getDiscountRate());
        BigDecimal totalPrice = amount(recalculated.getTotalPrice() != null ? recalculated.getTotalPrice() : job.getTotalPrice());
        ServiceRowAmounts serviceRowAmounts = resolveServiceRowAmounts(job, pricingResult, recalculated, discountRate);
        BigDecimal servicePrice = serviceRowAmounts.price();
        BigDecimal serviceDiscount = serviceRowAmounts.discount();
        BigDecimal serviceBusinessVolume = effectiveBusinessVolume(serviceRowAmounts);
        BigDecimal baseBusinessVolume = totalPrice.subtract(serviceBusinessVolume).setScale(2, RoundingMode.HALF_UP);
        BigDecimal basePrice = reverseDiscount(baseBusinessVolume, discountRate);
        Long costCenterId = resolveCostCenterId(job);

        syncManagedRow(job.getId(), job.getHeadquartersId(), costCenterId, FIXPRICE_SERVICE_NAME, basePrice, discountRate);
        syncManagedRow(job.getId(), job.getHeadquartersId(), costCenterId, SERVICEPRICE_SERVICE_NAME, servicePrice, serviceDiscount);
    }

    private boolean hasPersistedRows(Long jobId) {
        Object rawCount = entityManager.createNativeQuery(COUNT_SQL)
                .setParameter("jobId", jobId)
                .getSingleResult();
        if (rawCount instanceof Number number) {
            return number.longValue() > 0;
        }
        if (rawCount instanceof String string && !string.isBlank()) {
            try {
                return Long.parseLong(string.trim()) > 0;
            } catch (NumberFormatException ignored) {
                return false;
            }
        }
        return false;
    }

    private List<PersistedRow> loadPersistedRows(Long jobId) {
        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(SELECT_ROWS_SQL)
                .setParameter("jobId", jobId)
                .getResultList();
        List<PersistedRow> result = new ArrayList<>();
        for (Object[] row : rows) {
            result.add(new PersistedRow(toInteger(value(row, 0)), asString(value(row, 1))));
        }
        return result;
    }

    private BigDecimal deriveBasePrice(Job job, BigDecimal servicePrice) {
        BigDecimal totalPrice = job.getTotalPrice();
        if (totalPrice != null) {
            return amount(totalPrice).subtract(servicePrice).setScale(2, RoundingMode.HALF_UP);
        }

        if (job.getSubtotalPrice() != null) {
            BigDecimal markupFactor = BigDecimal.ONE.add(amount(job.getMarkup())
                    .divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP));
            return amount(job.getSubtotalPrice()).multiply(markupFactor).setScale(2, RoundingMode.HALF_UP);
        }

        if (job.getFixPrice() != null) {
            return amount(job.getFixPrice());
        }
        return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    }

    private Long resolveCostCenterId(Job job) {
        if (job.getCostCenterPayerCashId() != null && job.getCostCenterPayerCashId() > 0) {
            return job.getCostCenterPayerCashId();
        }
        if (job.getCostCenterPayerId() != null && job.getCostCenterPayerId() > 0) {
            return job.getCostCenterPayerId();
        }
        return 0L;
    }

    private void syncManagedRow(Long jobId,
                                Long hqId,
                                Long costCenterId,
                                String serviceName,
                                BigDecimal price,
                                BigDecimal discount) {
        if (!hasAmount(price)) {
            deleteRow(jobId, 0, serviceName);
            return;
        }
        CatalogReference catalogReference = resolveCatalogReference(serviceName, "", hqId);

        int updated = entityManager.createNativeQuery(UPDATE_SQL)
                .setParameter("jobId", jobId)
                .setParameter("costCenterId", costCenterId != null ? costCenterId : 0L)
                .setParameter("tourSort", 0)
                .setParameter("serviceId", catalogReference.serviceId)
                .setParameter("serviceName", serviceName)
                .setParameter("serviceTypeId", catalogReference.serviceTypeId)
                .setParameter("serviceTypeName", "")
                .setParameter("price", amount(price))
                .setParameter("discount", amount(discount))
                .executeUpdate();
        if (updated == 0) {
            insertRow(jobId, hqId, costCenterId, 0, serviceName, "", amount(price), amount(discount));
        }
    }

    private void syncManagedRows(Long jobId,
                                 Long hqId,
                                 Long costCenterId,
                                 List<PersistedRow> persistedRows,
                                 List<JobTourServiceDraftRowDto> expectedRows) {
        List<JobTourServiceDraftRowDto> managedRows = expectedRows != null ? expectedRows : List.of();
        List<RowKey> expectedKeys = managedRows.stream()
                .filter(this::hasAmount)
                .map(row -> new RowKey(normalizeTourSort(row.getTourSort()), normalizeServiceName(row.getServiceName())))
                .toList();
        if (persistedRows.stream().anyMatch(row -> !expectedKeys.contains(row.key()) && !isManagedRouteRow(row))) {
            return;
        }

        for (JobTourServiceDraftRowDto row : managedRows) {
            if (!hasAmount(row)) {
                continue;
            }
            syncManagedRow(
                    jobId,
                    hqId,
                    costCenterId,
                    normalizeTourSort(row.getTourSort()),
                    normalizeServiceName(row.getServiceName()),
                    row.getServiceTypeName(),
                    row.getPrice(),
                    row.getDiscount()
            );
        }

        for (PersistedRow row : persistedRows) {
            if (!expectedKeys.contains(row.key())) {
                deleteRow(jobId, row.tourSort != null ? row.tourSort : 0, row.serviceName);
            }
        }
    }

    private void insertManagedRows(Long jobId,
                                   Long hqId,
                                   Long costCenterId,
                                   List<JobTourServiceDraftRowDto> rows) {
        if (rows == null) {
            return;
        }
        for (JobTourServiceDraftRowDto row : rows) {
            if (!hasAmount(row)) {
                continue;
            }
            insertRow(
                    jobId,
                    hqId,
                    costCenterId,
                    normalizeTourSort(row.getTourSort()),
                    normalizeServiceName(row.getServiceName()),
                    row.getServiceTypeName(),
                    row.getPrice(),
                    row.getDiscount()
            );
        }
    }

    private void deleteRow(Long jobId, int tourSort, String serviceName) {
        entityManager.createNativeQuery(DELETE_SQL)
                .setParameter("jobId", jobId)
                .setParameter("tourSort", tourSort)
                .setParameter("serviceName", serviceName)
                .executeUpdate();
    }

    private void insertRow(Long jobId,
                           Long hqId,
                           Long costCenterId,
                           int tourSort,
                           String serviceName,
                           String serviceTypeName,
                           BigDecimal price,
                           BigDecimal discount) {
        CatalogReference catalogReference = resolveCatalogReference(serviceName, serviceTypeName, hqId);
        entityManager.createNativeQuery(INSERT_SQL)
                .setParameter("jobId", jobId)
                .setParameter("costCenterId", costCenterId != null ? costCenterId : 0L)
                .setParameter("tourSort", tourSort)
                .setParameter("serviceId", catalogReference.serviceId)
                .setParameter("serviceName", serviceName)
                .setParameter("serviceTypeId", catalogReference.serviceTypeId)
                .setParameter("serviceTypeName", serviceTypeName != null ? serviceTypeName : "")
                .setParameter("price", amount(price))
                .setParameter("discount", amount(discount))
                .executeUpdate();
    }

    private void syncManagedRow(Long jobId,
                                Long hqId,
                                Long costCenterId,
                                int tourSort,
                                String serviceName,
                                String serviceTypeName,
                                BigDecimal price,
                                BigDecimal discount) {
        if (!hasAmount(price)) {
            deleteRow(jobId, tourSort, serviceName);
            return;
        }
        CatalogReference catalogReference = resolveCatalogReference(serviceName, serviceTypeName, hqId);

        int updated = entityManager.createNativeQuery(UPDATE_SQL)
                .setParameter("jobId", jobId)
                .setParameter("costCenterId", costCenterId != null ? costCenterId : 0L)
                .setParameter("tourSort", tourSort)
                .setParameter("serviceId", catalogReference.serviceId)
                .setParameter("serviceName", serviceName)
                .setParameter("serviceTypeId", catalogReference.serviceTypeId)
                .setParameter("serviceTypeName", serviceTypeName != null ? serviceTypeName : "")
                .setParameter("price", amount(price))
                .setParameter("discount", amount(discount))
                .executeUpdate();
        if (updated == 0) {
            insertRow(jobId, hqId, costCenterId, tourSort, serviceName, serviceTypeName, amount(price), amount(discount));
        }
    }

    private CatalogReference resolveCatalogReference(String serviceName, String serviceTypeName, Long hqId) {
        if (serviceEntityRepository == null || serviceTypeRepository == null) {
            return CatalogReference.empty();
        }
        String normalizedServiceName = normalizeServiceName(serviceName).trim();
        String normalizedServiceTypeName = serviceTypeName != null ? serviceTypeName.trim() : "";
        if (normalizedServiceName.isBlank() || normalizedServiceTypeName.isBlank()) {
            return CatalogReference.empty();
        }

        ResolvedServiceType resolvedServiceType = resolveServiceType(normalizedServiceTypeName, hqId);
        if (resolvedServiceType == null) {
            return CatalogReference.empty();
        }
        ServiceEntity service = findServiceByName(normalizedServiceName, resolvedServiceType.headquartersId);
        return new CatalogReference(
                service != null && service.getId() != null ? service.getId() : 0L,
                resolvedServiceType.id
        );
    }

    private ResolvedServiceType resolveServiceType(String name, Long hqId) {
        ServiceType localType = findServiceTypeByName(name, hqId);
        if (localType != null) {
            return new ResolvedServiceType(localType.getId() != null ? localType.getId() : 0L, normalizeHeadquartersId(hqId));
        }
        ServiceType globalType = findServiceTypeByName(name, 0L);
        if (globalType != null) {
            return new ResolvedServiceType(globalType.getId() != null ? globalType.getId() : 0L, 0L);
        }
        return null;
    }

    private ServiceType findServiceTypeByName(String name, Long hqId) {
        return serviceTypeRepository.findByHeadquartersId(normalizeHeadquartersId(hqId)).stream()
                .filter(type -> type.getMode() != null && type.getMode() == LEGACY_ACTIVE_MODE)
                .filter(type -> name.equals(type.getName()))
                .max(java.util.Comparator.comparing((ServiceType type) -> isCommonCatalogEntry(type.getCustomerId()) ? 1 : 0))
                .orElse(null);
    }

    private ServiceEntity findServiceByName(String name, Long hqId) {
        return serviceEntityRepository.findByHeadquartersId(normalizeHeadquartersId(hqId)).stream()
                .filter(service -> service.getMode() != null && service.getMode() == LEGACY_ACTIVE_MODE)
                .filter(found -> name.equals(found.getName()))
                .max(java.util.Comparator.comparing((ServiceEntity service) -> isCommonCatalogEntry(service.getCustomerId()) ? 1 : 0))
                .orElse(null);
    }

    private Long normalizeHeadquartersId(Long hqId) {
        return hqId != null ? hqId : 0L;
    }

    private boolean isCommonCatalogEntry(Long customerId) {
        return customerId == null || customerId == 0;
    }

    private boolean isManagedBaseRow(PersistedRow row) {
        if (row == null || row.tourSort == null) {
            return false;
        }
        return row.tourSort == 0
                && (FIXPRICE_SERVICE_NAME.equals(row.serviceName) || SERVICEPRICE_SERVICE_NAME.equals(row.serviceName));
    }

    private boolean isManagedRouteRow(PersistedRow row) {
        if (row == null || row.serviceName == null || row.serviceName.isBlank()) {
            return false;
        }
        if (row.tourSort != null && row.tourSort == 0) {
            return "Grundpreis".equals(row.serviceName) || SERVICEPRICE_SERVICE_NAME.equals(row.serviceName);
        }
        return row.serviceName.startsWith("PLZ ") || row.serviceName.contains(" nach ");
    }

    private ServiceRowAmounts resolveServiceRowAmounts(Job job,
                                                       JobPricingResultDto pricingResult,
                                                       Job recalculated,
                                                       BigDecimal discountRate) {
        BigDecimal totalServicePrice = amount(recalculated != null && recalculated.getServicePrice() != null
                ? recalculated.getServicePrice()
                : job != null ? job.getServicePrice() : null);
        BigDecimal automaticStreetServicePrice = amount(pricingResult != null ? pricingResult.getAutomaticStreetServicePrice() : null);
        BigDecimal baseServicePrice = pricingResult != null && pricingResult.getBaseServicePrice() != null
                ? amount(pricingResult.getBaseServicePrice())
                : totalServicePrice.subtract(automaticStreetServicePrice).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
        if (hasAmount(automaticStreetServicePrice)) {
            BigDecimal billedServicePrice = effectiveBusinessVolume(
                    new ServiceRowAmounts(
                            baseServicePrice,
                            pricingResult != null && pricingResult.isServicePriceDiscountApplied()
                                    ? discountRate
                                    : BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
                    )
            ).add(automaticStreetServicePrice).setScale(2, RoundingMode.HALF_UP);
            return new ServiceRowAmounts(billedServicePrice, BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
        }
        return new ServiceRowAmounts(
                totalServicePrice,
                pricingResult != null && pricingResult.isServicePriceDiscountApplied()
                        ? discountRate
                        : BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
        );
    }

    private BigDecimal effectiveBusinessVolume(ServiceRowAmounts amounts) {
        return applyDiscount(amounts.price(), amounts.discount());
    }

    private BigDecimal applyDiscount(BigDecimal amount, BigDecimal discountRate) {
        BigDecimal normalizedAmount = amount(amount);
        BigDecimal normalizedDiscount = amount(discountRate);
        if (normalizedDiscount.compareTo(BigDecimal.ZERO) == 0) {
            return normalizedAmount;
        }
        BigDecimal factor = BigDecimal.ONE.subtract(
                normalizedDiscount.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP)
        );
        return normalizedAmount.multiply(factor).setScale(2, RoundingMode.HALF_UP);
    }

    private BigDecimal reverseDiscount(BigDecimal amount, BigDecimal discountRate) {
        BigDecimal normalizedAmount = amount(amount);
        BigDecimal normalizedDiscount = amount(discountRate);
        if (normalizedDiscount.compareTo(BigDecimal.ZERO) == 0) {
            return normalizedAmount;
        }
        BigDecimal divisor = BigDecimal.ONE.subtract(
                normalizedDiscount.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP)
        );
        if (divisor.compareTo(BigDecimal.ZERO) == 0) {
            return normalizedAmount;
        }
        return normalizedAmount.divide(divisor, 2, RoundingMode.HALF_UP);
    }

    private BigDecimal amount(BigDecimal value) {
        return (value != null ? value : BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
    }

    private boolean hasAmount(BigDecimal value) {
        return value != null && value.setScale(2, RoundingMode.HALF_UP).compareTo(BigDecimal.ZERO) != 0;
    }

    private boolean hasAmount(JobTourServiceDraftRowDto row) {
        return row != null && hasAmount(row.getPrice());
    }

    private int normalizeTourSort(Integer value) {
        return value != null ? value : 0;
    }

    private String normalizeServiceName(String value) {
        return value != null ? value : "";
    }

    private Object value(Object[] row, int index) {
        return row != null && index >= 0 && index < row.length ? row[index] : null;
    }

    private Integer toInteger(Object value) {
        if (value instanceof Number number) {
            return number.intValue();
        }
        if (value instanceof String string && !string.isBlank()) {
            try {
                return Integer.parseInt(string.trim());
            } catch (NumberFormatException ignored) {
                return null;
            }
        }
        return null;
    }

    private String asString(Object value) {
        if (value == null) {
            return "";
        }
        String text = value.toString();
        return text.isBlank() ? "" : text;
    }

    private record PersistedRow(Integer tourSort, String serviceName) {
        private RowKey key() {
            return new RowKey(tourSort != null ? tourSort : 0, serviceName != null ? serviceName : "");
        }
    }

    private record RowKey(int tourSort, String serviceName) {
    }

    private record CatalogReference(Long serviceId, Long serviceTypeId) {
        private static CatalogReference empty() {
            return new CatalogReference(0L, 0L);
        }
    }

    private record ResolvedServiceType(Long id, Long headquartersId) {
    }

    private record ServiceRowAmounts(BigDecimal price, BigDecimal discount) {
    }
}
