content: package io.vividcode.happyride.dispatchservice;
  
  import io.eventuate.tram.events.aggregates.ResultWithDomainEvents;
  import io.vividcode.happyride.common.PositionVO;
  import io.vividcode.happyride.dispatchservice.api.events.DispatchDomainEvent;
  import io.vividcode.happyride.dispatchservice.api.events.TripDispatchFailedReason;
  import io.vividcode.happyride.dispatchservice.dataaccess.DispatchRepository;
  import io.vividcode.happyride.dispatchservice.domain.Dispatch;
  import io.vividcode.happyride.dispatchservice.domain.DispatchDomainEventPublisher;
  import io.vividcode.happyride.tripservice.api.events.DriverAcceptTripDetails;
  import io.vividcode.happyride.tripservice.api.events.TripDetails;
  import java.time.Duration;
  import java.util.Optional;
  import java.util.Set;
  import java.util.function.Consumer;
  import jakarta.transaction.Transactional;
  import lombok.extern.slf4j.Slf4j;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.stereotype.Service;
  
  @Service
  @Slf4j
  public class DispatchService {
  
    @Autowired
    DriverLocationService driverLocationService;
  
    @Autowired
    TripAcceptanceService tripAcceptanceService;
  
    @Autowired
    DispatchRepository dispatchRepository;
  
    @Autowired
    DispatchDomainEventPublisher eventPublisher;
  
    private final Duration acceptanceCheckInterval = Duration.ofSeconds(10);
  
    public void verifyDispatch(final TripDetails tripDetails) {
      final Set<AvailableDriver> availableDrivers = this
          .findAvailableDrivers(tripDetails);
      if (availableDrivers.isEmpty()) {
        throw new DispatchVerificationException("No available drivers");
      }
    }
  
    @Transactional
    public void dispatchTrip(final String tripId, final TripDetails tripDetails) {
      final Set<AvailableDriver> availableDrivers = this
          .findAvailableDrivers(tripDetails);
      this.saveAndPublishEvents(
          Dispatch.createDispatch(tripId, tripDetails, availableDrivers));
      if (!availableDrivers.isEmpty()) {
        this.tripAcceptanceService.startTripAcceptanceCheck(tripId, tripDetails,
            this.acceptanceCheckInterval,
            this::selectTripAcceptance, this::notifyTripDispatchFailed);
        log.info("Dispatch trip {} to drivers {}", tripId, availableDrivers);
      }
    }
  
    private Set<AvailableDriver> findAvailableDrivers(
        final TripDetails tripDetails) {
      final PositionVO startPos = tripDetails.getStartPos();
      return this.driverLocationService
          .findAvailableDrivers(startPos.getLng(), startPos.getLat());
    }
  
    @Transactional
    public void submitTripAcceptance(final String tripId,
        final DriverAcceptTripDetails details) {
      log.info("Driver {} wants to accept trip {}", details.getDriverId(),
          tripId);
      this.withCurrentDispatch(tripId, dispatch -> {
        this.dispatchRepository.save(dispatch.submitTripAcceptance(details));
        this.tripAcceptanceService.addDriverToAcceptTrip(tripId, details);
      });
    }
  
    @Transactional
    public void selectTripAcceptance(final String tripId, final String driverId) {
      log.info("Select driver {} to accept trip {}", driverId, tripId);
      this.withCurrentDispatch(tripId, dispatch ->
          this.saveAndPublishEvents(dispatch.selectTripAcceptance(driverId)));
    }
  
    public Optional<Dispatch> findCurrentDispatchByTrip(final String tripId) {
      return this.dispatchRepository.findCurrentDispatch(tripId);
    }
  
    private void withCurrentDispatch(final String tripId,
        final Consumer<Dispatch> dispatchConsumer,
        final Runnable noDispatchAction) {
      final Optional<Dispatch> optionalDispatch = this
          .findCurrentDispatchByTrip(tripId);
      if (optionalDispatch.isPresent()) {
        dispatchConsumer.accept(optionalDispatch.get());
      } else {
        log.warn("No active dispatch found for trip {}", tripId);
        noDispatchAction.run();
      }
    }
  
    private void withCurrentDispatch(final String tripId,
        final Consumer<Dispatch> dispatchConsumer) {
      this.withCurrentDispatch(tripId, dispatchConsumer, () -> {
      });
    }
  
    @Transactional
    public void notifyTripDispatchFailed(final String tripId,
        final TripDispatchFailedReason reason) {
      log.info("Failed to dispatch trip {} with reason {}", tripId, reason);
      this.withCurrentDispatch(tripId,
          dispatch -> this.saveAndPublishEvents(
              dispatch.markAsFailed(tripId, reason)));
    }
  
    @Transactional
    public void cancelDispatch(final String tripId) {
      this.withCurrentDispatch(tripId,
          dispatch -> this.saveAndPublishEvents(dispatch.markAsCancelled()));
      this.tripAcceptanceService.cancelTripAcceptanceCheck(tripId);
    }
  
    private void saveAndPublishEvents(
        final ResultWithDomainEvents<Dispatch, DispatchDomainEvent> result) {
      final Dispatch dispatch = result.result;
      this.dispatchRepository.save(dispatch);
      this.eventPublisher.publish(dispatch, result.events);
    }
  }
  