# DroneInfoService.java

    package io.github.js0ny.ilp_coursework.service;

    import io.github.js0ny.ilp_coursework.data.common.LngLat;
    import io.github.js0ny.ilp_coursework.data.external.Drone;
    import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
    import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
    import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
    import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;

    import org.springframework.lang.Nullable;
    import org.springframework.stereotype.Service;
    import org.springframework.web.client.RestTemplate;

    import java.net.URI;
    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.time.LocalTime;
    import java.util.*;
    import java.util.stream.Collectors;

    @Service
    public class DroneInfoService {

        private final String baseUrl;
        private final String dronesForServicePointsEndpoint = "drones-for-service-points";
        public static final String servicePointsEndpoint = "service-points";
        public static final String restrictedAreasEndpoint = "restricted-areas";

        private final RestTemplate restTemplate;

        /** Constructor, handles the base url here. */
        public DroneInfoService() {
            this(new RestTemplate());
        }

        public DroneInfoService(RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
            String baseUrl = System.getenv("ILP_ENDPOINT");
            if (baseUrl == null || baseUrl.isBlank()) {
                this.baseUrl = "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
            } else {
                // Defensive: Add '/' to the end of the URL
                if (!baseUrl.endsWith("/")) {
                    baseUrl += "/";
                }
                this.baseUrl = baseUrl;
            }
        }

        /**
         * Return an array of ids of drones with/without cooling capability
         *
         * <p>Associated service method with {@code /dronesWithCooling/{state}}
         *
         * @param state determines the capability filtering
         * @return if {@code state} is true, return ids of drones with cooling capability, else without
         *     cooling
         * @see
         *     io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
         */
        public List<String> dronesWithCooling(boolean state) {
            // URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
            // Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
            List<Drone> drones = fetchAllDrones();

            if (drones == null) {
                return new ArrayList<>();
            }

            return drones.stream()
                    .filter(drone -> drone.capability().cooling() == state)
                    .map(Drone::id)
                    .collect(Collectors.toList());
        }

        /**
         * Return a {@link Drone}-style json data structure with the given {@code id}
         *
         * <p>Associated service method with {@code /droneDetails/{id}}
         *
         * @param id The id of the drone
         * @return drone json body of given id
         * @throws NullPointerException when cannot fetch available drones from remote
         * @throws IllegalArgumentException when drone with given {@code id} cannot be found this should
         *     lead to a 404
         * @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
         */
        public Drone droneDetail(String id) {
            List<Drone> drones = fetchAllDrones();

            for (var drone : drones) {
                if (drone.id().equals(id)) {
                    return drone;
                }
            }

            // This will result in 404
            throw new IllegalArgumentException("drone with that ID cannot be found");
        }

        /**
         * Return an array of ids of drones that match all the requirements in the medical dispatch
         * records
         *
         * <p>Associated service method with
         *
         * @param rec array of medical dispatch records
         * @return List of drone ids that match all the requirements
         * @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones
         */
        public List<String> dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
            List<Drone> drones = fetchAllDrones();

            if (rec == null || rec.length == 0) {
                return drones.stream()
                        .filter(Objects::nonNull)
                        .map(Drone::id)
                        .collect(Collectors.toList());
            }

            /*
             * Traverse and filter drones, pass every record's requirement to helper
             */
            return drones.stream()
                    .filter(d -> d != null && d.capability() != null)
                    .filter(
                            d ->
                                    Arrays.stream(rec)
                                            .filter(r -> r != null && r.requirements() != null)
                                            .allMatch(r -> droneMatchesRequirement(d, r)))
                    .map(Drone::id)
                    .collect(Collectors.toList());
        }

        /**
         * Helper to check if a drone meets the requirement of a medical dispatch.
         *
         * @param drone the drone to be checked
         * @param record the medical dispatch record containing the requirement
         * @return true if the drone meets the requirement, false otherwise
         * @throws IllegalArgumentException when record requirements or drone capability is invalid
         *     (capacity and id cannot be null in {@code MedDispathRecDto})
         */
        public boolean droneMatchesRequirement(Drone drone, MedDispatchRecRequest record) {
            var requirements = record.requirements();
            if (requirements == null) {
                throw new IllegalArgumentException("requirements cannot be null");
            }
            var capability = drone.capability();
            if (capability == null) {
                throw new IllegalArgumentException("drone capability cannot be null");
            }

            float requiredCapacity = requirements.capacity();
            if (requiredCapacity <= 0 || capability.capacity() < requiredCapacity) {
                return false;
            }

            // Use boolean wrapper to allow null (not specified) values
            boolean requiredCooling = requirements.cooling();
            boolean requiredHeating = requirements.heating();

            // Case 1: required is null: We don't care about it
            // Case 2: required is false: We don't care about it (high capability adapts to
            // low requirements)
            // Case 3: capability is true: Then always matches
            // See: https://piazza.com/class/me9vp64lfgf4sn/post/100
            boolean matchesCooling = !requiredCooling || capability.cooling();
            boolean matchesHeating = !requiredHeating || capability.heating();

            // Conditions: All requirements matched + availability matched, use helper
            // For minimal privilege, only pass drone id to check availability
            return (matchesCooling && matchesHeating && checkAvailability(drone.id(), record)); // &&
            // checkCost(drone, record) // checkCost is more expensive than
            // checkAvailability
        }

        /**
         * Helper to check if a drone is available at the required date and time
         *
         * @param droneId the id of the drone to be checked
         * @param record the medical dispatch record containing the required date and time
         * @return true if the drone is available, false otherwise
         */
        private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
            URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
            ServicePointDrones[] servicePoints =
                    restTemplate.getForObject(droneUrl, ServicePointDrones[].class);

            LocalDate requiredDate = record.date();
            DayOfWeek requiredDay = requiredDate.getDayOfWeek();
            LocalTime requiredTime = record.time();

            assert servicePoints != null;
            for (var servicePoint : servicePoints) {
                var drone = servicePoint.locateDroneById(droneId); // Nullable
                if (drone != null) {
                    return drone.checkAvailability(requiredDay, requiredTime);
                }
            }

            return false;
        }

        private LngLat queryServicePointLocationByDroneId(String droneId) {
            URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
            ServicePointDrones[] servicePoints =
                    restTemplate.getForObject(droneUrl, ServicePointDrones[].class);

            assert servicePoints != null;
            for (var sp : servicePoints) {
                var drone = sp.locateDroneById(droneId); // Nullable
                if (drone != null) {
                    return queryServicePointLocation(sp.servicePointId());
                }
            }

            return null;
        }

        @Nullable
        private LngLat queryServicePointLocation(int id) {
            URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);

            ServicePoint[] servicePoints =
                    restTemplate.getForObject(servicePointUrl, ServicePoint[].class);

            assert servicePoints != null;
            for (var sp : servicePoints) {
                if (sp.id() == id) {
                    // We dont consider altitude
                    return new LngLat(sp.location());
                }
            }
            return null;
        }

        public List<Drone> fetchAllDrones() {
            System.out.println("fetchAllDrones called");
            String dronesEndpoint = "drones";
            URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
            System.out.println("Fetching from URL: " + droneUrl);
            Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
            return drones == null ? new ArrayList<>() : Arrays.asList(drones);
        }

        public List<RestrictedArea> fetchRestrictedAreas() {
            URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint);
            RestrictedArea[] restrictedAreas =
                    restTemplate.getForObject(restrictedUrl, RestrictedArea[].class);
            assert restrictedAreas != null;
            return Arrays.asList(restrictedAreas);
        }

        public List<ServicePoint> fetchServicePoints() {
            URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
            ServicePoint[] servicePoints =
                    restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
            assert servicePoints != null;
            return Arrays.asList(servicePoints);
        }

        public List<ServicePointDrones> fetchDronesForServicePoints() {
            URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
            ServicePointDrones[] servicePointDrones =
                    restTemplate.getForObject(servicePointDronesUrl, ServicePointDrones[].class);
            assert servicePointDrones != null;
            return Arrays.asList(servicePointDrones);
        }
    }