package de.votian.services.service;

import de.votian.services.dto.DispositionJobRowDto;
import de.votian.services.dto.DispositionStationDto;
import de.votian.services.dto.DispositionVehicleRowDto;
import de.votian.services.entity.Address;
import de.votian.services.entity.Company;
import de.votian.services.entity.CostCenter;
import de.votian.services.entity.Courier;
import de.votian.services.entity.CourierVehicle;
import de.votian.services.entity.Customer;
import de.votian.services.entity.Job;
import de.votian.services.entity.Tour;
import de.votian.services.entity.VehicleDisposition;
import de.votian.services.repository.AddressRepository;
import de.votian.services.repository.CompanyRepository;
import de.votian.services.repository.CostCenterRepository;
import de.votian.services.repository.CourierRepository;
import de.votian.services.repository.CourierVehicleRepository;
import de.votian.services.repository.CustomerRepository;
import de.votian.services.repository.JobRepository;
import de.votian.services.repository.TourRepository;
import de.votian.services.repository.VehicleDispositionRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Service
@Transactional
public class DispositionService {

    private final JobRepository jobRepository;
    private final TourRepository tourRepository;
    private final CostCenterRepository costCenterRepository;
    private final CustomerRepository customerRepository;
    private final CompanyRepository companyRepository;
    private final CourierRepository courierRepository;
    private final CourierVehicleRepository courierVehicleRepository;
    private final VehicleDispositionRepository vehicleDispositionRepository;
    private final AddressRepository addressRepository;

    public DispositionService(JobRepository jobRepository,
                              TourRepository tourRepository,
                              CostCenterRepository costCenterRepository,
                              CustomerRepository customerRepository,
                              CompanyRepository companyRepository,
                              CourierRepository courierRepository,
                              CourierVehicleRepository courierVehicleRepository,
                              VehicleDispositionRepository vehicleDispositionRepository,
                              AddressRepository addressRepository) {
        this.jobRepository = jobRepository;
        this.tourRepository = tourRepository;
        this.costCenterRepository = costCenterRepository;
        this.customerRepository = customerRepository;
        this.companyRepository = companyRepository;
        this.courierRepository = courierRepository;
        this.courierVehicleRepository = courierVehicleRepository;
        this.vehicleDispositionRepository = vehicleDispositionRepository;
        this.addressRepository = addressRepository;
    }

    @Transactional(readOnly = true)
    public List<DispositionJobRowDto> findJobsForDay(Long hqId, LocalDate day) {
        List<Job> jobs = loadDispositionJobs(hqId, day);
        return buildJobRows(jobs);
    }

    @Transactional(readOnly = true)
    public List<DispositionVehicleRowDto> findVehiclesForDay(Long hqId, LocalDate day) {
        List<Courier> couriers = new ArrayList<>(courierRepository.findByHeadquartersId(hqId));
        couriers.sort(Comparator.comparing(Courier::getSid, Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));

        List<Job> jobs = loadDispositionJobs(hqId, day);
        Map<Long, Long> assignedJobsByCourier = new HashMap<>();
        for (Job job : jobs) {
            if (job.getCourierId() != null) {
                assignedJobsByCourier.merge(job.getCourierId(), 1L, (a, b) -> Objects.requireNonNull(a) + Objects.requireNonNull(b));
            }
        }

        List<CourierVehicle> allVehicles = courierVehicleRepository.findByHeadquartersId(hqId);
        Map<Long, List<CourierVehicle>> vehiclesByCourier = new HashMap<>();
        for (CourierVehicle vehicle : allVehicles) {
            vehiclesByCourier.computeIfAbsent(vehicle.getCourierId(), ignored -> new ArrayList<>()).add(vehicle);
        }

        List<Long> dayJobIds = jobs.stream().map(Job::getId).toList();
        Map<Long, Long> assignedJobsByVehicle = new HashMap<>();
        if (!dayJobIds.isEmpty()) {
            for (VehicleDisposition disposition : vehicleDispositionRepository.findByJobIdIn(dayJobIds)) {
                if (disposition.getCourierVehicleId() != null) {
                    assignedJobsByVehicle.merge(disposition.getCourierVehicleId(), 1L, (a, b) -> Objects.requireNonNull(a) + Objects.requireNonNull(b));
                }
            }
        }

        List<DispositionVehicleRowDto> rows = new ArrayList<>();
        for (Courier courier : couriers) {
            List<CourierVehicle> vehicles = vehiclesByCourier.getOrDefault(courier.getId(), List.of());
            if (vehicles.isEmpty()) {
                DispositionVehicleRowDto row = new DispositionVehicleRowDto();
                row.setCourierId(courier.getId());
                row.setCourierSid(courier.getSid());
                row.setCourierEid(courier.getEid());
                row.setAvailable(courier.getAvailable());
                row.setCourierVehicleSid("Hauptfahrzeug");
                row.setVehicleTypeId(courier.getVehicleTypeId());
                row.setAssignedJobs(assignedJobsByCourier.getOrDefault(courier.getId(), 0L));
                rows.add(row);
                continue;
            }

            vehicles.sort(Comparator.comparing(CourierVehicle::getSid, Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
            for (CourierVehicle vehicle : vehicles) {
                DispositionVehicleRowDto row = new DispositionVehicleRowDto();
                row.setCourierId(courier.getId());
                row.setCourierSid(courier.getSid());
                row.setCourierEid(courier.getEid());
                row.setAvailable(courier.getAvailable());
                row.setCourierVehicleId(vehicle.getId());
                row.setCourierVehicleSid(vehicle.getSid());
                row.setVehicleTypeId(vehicle.getVehicleTypeId());
                row.setAssignedJobs(assignedJobsByVehicle.getOrDefault(vehicle.getId(), 0L));
                rows.add(row);
            }
        }
        return rows;
    }

    @Transactional(readOnly = true)
    public List<DispositionStationDto> findStationsForDay(Long hqId, LocalDate day) {
        List<Job> jobs = loadDispositionJobs(hqId, day);
        if (jobs.isEmpty()) {
            return List.of();
        }

        Map<Long, Job> jobsById = new HashMap<>();
        for (Job job : jobs) {
            jobsById.put(job.getId(), job);
        }

        List<Tour> tours = tourRepository.findByJobIdInOrderByJobIdAscSortAsc(jobs.stream().map(Job::getId).toList());
        Map<Long, CostCenter> costCenterById = loadCostCenters(jobs, groupToursByJobId(tours));
        Map<Long, String> customerNames = loadCustomerNames(costCenterById);
        Map<Long, Address> addresses = loadAddresses(tours.stream().map(Tour::getAddressId).toList());

        List<DispositionStationDto> rows = new ArrayList<>();
        for (Tour tour : tours) {
            Job job = jobsById.get(tour.getJobId());
            if (job == null) {
                continue;
            }
            Address address = addresses.get(tour.getAddressId());
            Long costCenterId = resolveCostCenterId(job, List.of(tour));
            CostCenter costCenter = costCenterById.get(costCenterId);

            DispositionStationDto dto = new DispositionStationDto();
            dto.setJobId(job.getId());
            dto.setOrderTime(job.getOrderTime());
            dto.setStopSort(tour.getSort());
            dto.setStopStatus(tour.getStatus());
            dto.setCustomerName(costCenter != null ? customerNames.getOrDefault(costCenter.getCustomerId(), "") : "");
            dto.setCommissionNo(tour.getCommissionNo());
            dto.setStopName(firstNonBlank(tour.getComp(), tour.getPerson(), ""));
            dto.setStreet(address != null ? address.getStreet() : "");
            dto.setZipcode(address != null ? address.getZipcode() : "");
            dto.setCity(address != null ? address.getCity() : "");
            dto.setCountry(address != null ? address.getCountry() : "");
            dto.setRemark(tour.getRemark());
            dto.setPhone(tour.getPhone());
            dto.setCourierSid(job.getCourierSid());
            rows.add(dto);
        }
        return rows;
    }

    public DispositionJobRowDto assignVehicle(Long hqId, Long jobId, Long courierId, Long courierVehicleId) {
        Job job = jobRepository.findById(Objects.requireNonNull(jobId))
                .orElseThrow(() -> new IllegalArgumentException("Auftrag nicht gefunden: " + jobId));
        validateJobScope(job, hqId);

        Courier courier;
        CourierVehicle vehicle = null;
        if (courierVehicleId != null) {
            vehicle = courierVehicleRepository.findById(Objects.requireNonNull(courierVehicleId))
                    .orElseThrow(() -> new IllegalArgumentException("Fahrzeug nicht gefunden: " + courierVehicleId));
            courier = courierRepository.findById(Objects.requireNonNull(vehicle.getCourierId()))
                    .orElseThrow(() -> new IllegalArgumentException("Kurier zum Fahrzeug nicht gefunden."));
        } else if (courierId != null) {
            courier = courierRepository.findById(courierId)
                    .orElseThrow(() -> new IllegalArgumentException("Kurier nicht gefunden: " + courierId));
        } else {
            throw new IllegalArgumentException("Kurier oder Fahrzeug muss angegeben werden.");
        }

        if (!hqId.equals(courier.getHeadquartersId())) {
            throw new IllegalArgumentException("Kurier gehoert nicht zur aktiven Niederlassung.");
        }

        job.setCourierId(courier.getId());
        job.setCourierSid(courier.getSid());
        job.setVehicleTypeId(vehicle != null ? vehicle.getVehicleTypeId() : courier.getVehicleTypeId());
        if (job.getHeadquartersDispoId() == null || job.getHeadquartersDispoId() == 0) {
            job.setHeadquartersDispoId(hqId);
        }
        if (job.getStatus() == null || Integer.valueOf(0).equals(job.getStatus())) {
            job.setStatus(1);
        }
        jobRepository.save(job);

        if (vehicle != null) {
            VehicleDisposition disposition = vehicleDispositionRepository.findByJobId(jobId).orElseGet(VehicleDisposition::new);
            disposition.setJobId(jobId);
            disposition.setCourierVehicleId(vehicle.getId());
            vehicleDispositionRepository.save(disposition);
        } else {
            vehicleDispositionRepository.deleteByJobId(jobId);
        }

        return buildJobRows(List.of(job)).get(0);
    }

    public void clearVehicleAssignment(Long hqId, Long jobId) {
        Job job = jobRepository.findById(Objects.requireNonNull(jobId))
                .orElseThrow(() -> new IllegalArgumentException("Auftrag nicht gefunden: " + jobId));
        validateJobScope(job, hqId);

        job.setCourierId(null);
        job.setCourierSid(null);
        job.setVehicleTypeId(null);
        if (Integer.valueOf(1).equals(job.getStatus())) {
            job.setStatus(0);
        }
        jobRepository.save(job);
        vehicleDispositionRepository.deleteByJobId(jobId);
    }

    private List<Job> loadDispositionJobs(Long hqId, LocalDate day) {
        LocalDate effectiveDay = day != null ? day : LocalDate.now();
        return jobRepository.findForDispositionDay(
                hqId,
                effectiveDay.atStartOfDay(),
                effectiveDay.atTime(23, 59, 59)
        ).stream()
                .filter(job -> !Integer.valueOf(10).equals(job.getStatus()))
                .filter(job -> !"2".equals(job.getStorno()))
                .toList();
    }

    private List<DispositionJobRowDto> buildJobRows(List<Job> jobs) {
        if (jobs.isEmpty()) {
            return List.of();
        }
        return buildJobRow(jobs, loadJobContext(jobs));
    }

    private JobContext loadJobContext(List<Job> jobs) {
        List<Long> jobIds = jobs.stream().map(Job::getId).toList();
        Map<Long, List<Tour>> toursByJobId = groupToursByJobId(tourRepository.findByJobIdInOrderByJobIdAscSortAsc(jobIds));
        Map<Long, CostCenter> costCenters = loadCostCenters(jobs, toursByJobId);
        Map<Long, String> customerNames = loadCustomerNames(costCenters);

        Map<Long, VehicleDisposition> dispositionByJobId = new HashMap<>();
        for (VehicleDisposition disposition : vehicleDispositionRepository.findByJobIdIn(jobIds)) {
            dispositionByJobId.put(disposition.getJobId(), disposition);
        }

        List<Long> vehicleIds = dispositionByJobId.values().stream()
                .map(VehicleDisposition::getCourierVehicleId)
                .filter(id -> id != null)
                .toList();
        Map<Long, CourierVehicle> vehicles = new HashMap<>();
        if (!vehicleIds.isEmpty()) {
            for (CourierVehicle vehicle : courierVehicleRepository.findAllById(vehicleIds)) {
                vehicles.put(vehicle.getId(), vehicle);
            }
        }

        List<Long> addressIds = new ArrayList<>();
        for (List<Tour> tours : toursByJobId.values()) {
            for (Tour tour : tours) {
                if (tour.getAddressId() != null) {
                    addressIds.add(tour.getAddressId());
                }
            }
        }
        Map<Long, Address> addresses = loadAddresses(addressIds);

        return new JobContext(toursByJobId, costCenters, customerNames, dispositionByJobId, vehicles, addresses);
    }

    private List<DispositionJobRowDto> buildJobRow(List<Job> jobs, JobContext context) {
        List<DispositionJobRowDto> rows = new ArrayList<>();
        for (Job job : jobs) {
            DispositionJobRowDto dto = new DispositionJobRowDto();
            List<Tour> tours = context.toursByJobId.getOrDefault(job.getId(), List.of());
            Tour pickup = findTour(tours, 1);
            Tour delivery = findTour(tours, 2);
            Long costCenterId = resolveCostCenterId(job, tours);
            CostCenter costCenter = context.costCenters.get(costCenterId);
            Long customerId = costCenter != null ? costCenter.getCustomerId() : null;
            VehicleDisposition disposition = context.dispositionByJobId.get(job.getId());
            CourierVehicle vehicle = disposition != null ? context.vehicles.get(disposition.getCourierVehicleId()) : null;

            dto.setJobId(job.getId());
            dto.setStatus(job.getStatus());
            dto.setOrderTime(job.getOrderTime());
            dto.setFinishTime(job.getFinishTime());
            dto.setTourName(job.getTourName());
            dto.setCustomerId(customerId);
            dto.setCustomerName(customerId != null ? context.customerNames.getOrDefault(customerId, "") : "");
            dto.setCostCenterId(costCenterId);
            dto.setCostCenterName(costCenter != null ? costCenter.getName() : "");
            dto.setCommissionNo(firstNonBlank(
                    pickup != null ? pickup.getCommissionNo() : null,
                    delivery != null ? delivery.getCommissionNo() : null
            ));
            dto.setPickupName(formatTourStop(pickup));
            dto.setPickupStreet(addressField(context.addresses, pickup, Address::getStreet));
            dto.setPickupZipcode(addressField(context.addresses, pickup, Address::getZipcode));
            dto.setPickupCity(addressField(context.addresses, pickup, Address::getCity));
            dto.setDeliveryName(formatTourStop(delivery));
            dto.setDeliveryStreet(addressField(context.addresses, delivery, Address::getStreet));
            dto.setDeliveryZipcode(addressField(context.addresses, delivery, Address::getZipcode));
            dto.setDeliveryCity(addressField(context.addresses, delivery, Address::getCity));
            dto.setCourierId(job.getCourierId());
            dto.setCourierSid(job.getCourierSid());
            dto.setVehicleTypeId(job.getVehicleTypeId());
            dto.setCourierVehicleId(vehicle != null ? vehicle.getId() : null);
            dto.setCourierVehicleSid(vehicle != null ? vehicle.getSid() : "");
            dto.setHeadquartersDispoId(job.getHeadquartersDispoId());
            dto.setTotalPrice(job.getTotalPrice());
            dto.setCourierPrice(job.getCourierPrice());
            dto.setExported(job.getExportTime() != null);
            rows.add(dto);
        }
        return rows;
    }

    private Map<Long, List<Tour>> groupToursByJobId(List<Tour> tours) {
        Map<Long, List<Tour>> toursByJobId = new LinkedHashMap<>();
        for (Tour tour : tours) {
            toursByJobId.computeIfAbsent(tour.getJobId(), ignored -> new ArrayList<>()).add(tour);
        }
        return toursByJobId;
    }

    private Map<Long, CostCenter> loadCostCenters(List<Job> jobs, Map<Long, List<Tour>> toursByJobId) {
        List<Long> costCenterIds = new ArrayList<>();
        for (Job job : jobs) {
            Long costCenterId = resolveCostCenterId(job, toursByJobId.getOrDefault(job.getId(), List.of()));
            if (costCenterId != null && !costCenterIds.contains(costCenterId)) {
                costCenterIds.add(costCenterId);
            }
        }

        Map<Long, CostCenter> costCenters = new HashMap<>();
        if (!costCenterIds.isEmpty()) {
            for (CostCenter costCenter : costCenterRepository.findAllById(costCenterIds)) {
                costCenters.put(costCenter.getId(), costCenter);
            }
        }
        return costCenters;
    }

    private Map<Long, String> loadCustomerNames(Map<Long, CostCenter> costCenters) {
        List<Long> customerIds = new ArrayList<>();
        for (CostCenter costCenter : costCenters.values()) {
            if (costCenter.getCustomerId() != null && !customerIds.contains(costCenter.getCustomerId())) {
                customerIds.add(costCenter.getCustomerId());
            }
        }

        Map<Long, Customer> customers = new HashMap<>();
        if (!customerIds.isEmpty()) {
            for (Customer customer : customerRepository.findAllById(customerIds)) {
                customers.put(customer.getId(), customer);
            }
        }

        List<Long> companyIds = new ArrayList<>();
        for (Customer customer : customers.values()) {
            if (customer.getCompanyId() != null && !companyIds.contains(customer.getCompanyId())) {
                companyIds.add(customer.getCompanyId());
            }
        }

        Map<Long, Company> companies = new HashMap<>();
        if (!companyIds.isEmpty()) {
            for (Company company : companyRepository.findAllById(companyIds)) {
                companies.put(company.getId(), company);
            }
        }

        Map<Long, String> names = new HashMap<>();
        for (Customer customer : customers.values()) {
            Company company = companies.get(customer.getCompanyId());
            String name = company != null && company.getComp() != null && !company.getComp().isBlank()
                    ? company.getComp()
                    : customer.getEid();
            names.put(customer.getId(), name);
        }
        return names;
    }

    private Map<Long, Address> loadAddresses(List<Long> addressIds) {
        List<Long> uniqueAddressIds = new ArrayList<>();
        for (Long addressId : addressIds) {
            if (addressId != null && !uniqueAddressIds.contains(addressId)) {
                uniqueAddressIds.add(addressId);
            }
        }
        Map<Long, Address> addresses = new HashMap<>();
        if (!uniqueAddressIds.isEmpty()) {
            for (Address address : addressRepository.findAllById(uniqueAddressIds)) {
                addresses.put(address.getId(), address);
            }
        }
        return addresses;
    }

    private void validateJobScope(Job job, Long hqId) {
        if (!(hqId.equals(job.getHeadquartersId()) || hqId.equals(job.getHeadquartersDispoId()))) {
            throw new IllegalArgumentException("Auftrag gehoert nicht zur aktiven Niederlassung.");
        }
    }

    private Long resolveCostCenterId(Job job, List<Tour> tours) {
        if (job.getCostCenterPayerId() != null && job.getCostCenterPayerId() > 0) {
            return job.getCostCenterPayerId();
        }
        Tour pickup = findTour(tours, 1);
        if (pickup != null && pickup.getCostCenterId() != null && pickup.getCostCenterId() > 0) {
            return pickup.getCostCenterId();
        }
        if (job.getCostCenterRelatedId() != null && job.getCostCenterRelatedId() > 0) {
            return job.getCostCenterRelatedId();
        }
        return null;
    }

    private Tour findTour(List<Tour> tours, int sort) {
        return tours.stream()
                .filter(tour -> Integer.valueOf(sort).equals(tour.getSort()))
                .findFirst()
                .orElse(null);
    }

    private String formatTourStop(Tour tour) {
        return tour != null ? firstNonBlank(tour.getComp(), tour.getPerson(), "") : "";
    }

    private String firstNonBlank(String... values) {
        for (String value : values) {
            if (value != null && !value.isBlank()) {
                return value;
            }
        }
        return "";
    }

    private String addressField(Map<Long, Address> addresses, Tour tour, AddressExtractor extractor) {
        if (tour == null || tour.getAddressId() == null) {
            return "";
        }
        Address address = addresses.get(tour.getAddressId());
        return address != null ? extractor.extract(address) : "";
    }

    @FunctionalInterface
    private interface AddressExtractor {
        String extract(Address address);
    }

    private record JobContext(Map<Long, List<Tour>> toursByJobId,
                              Map<Long, CostCenter> costCenters,
                              Map<Long, String> customerNames,
                              Map<Long, VehicleDisposition> dispositionByJobId,
                              Map<Long, CourierVehicle> vehicles,
                              Map<Long, Address> addresses) {
    }
}
