package io.github.js0ny.ilp_coursework.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.Region;
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);
    }
}
