IndoorDirectionsController.java

package com.soen390.backend.controller;

import com.soen390.backend.exception.IndoorResourceNotFoundException;
import com.soen390.backend.exception.InvalidIndoorRequestException;
import com.soen390.backend.object.IndoorDirectionResponse;
import com.soen390.backend.service.IndoorDirectionService;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Set;

@RestController
@RequestMapping("/api/directions/indoor")
public class IndoorDirectionsController {

    private static final String PARAM_BUILDING_ID = "buildingId";
    private static final String FLOOR_LABEL = " floor '";

    private static final Set<String> VALID_BUILDING_PREFIXES = Set.of(
            "Hall-", "VL-", "LB-", "MB-", "CC-", "VE-" );
    private static final Set<String> VALID_SHORT_CODES = Set.of(
            "H", "VL", "LB", "MB", "CC", "VE");

    private final IndoorDirectionService indoorDirectionService;

    public IndoorDirectionsController(IndoorDirectionService indoorDirectionService) {
        this.indoorDirectionService = indoorDirectionService;
    }

    @GetMapping
    public IndoorDirectionResponse getIndoorDirections(
            @RequestParam String buildingId,
            @RequestParam String origin,
            @RequestParam String destination,
            @RequestParam(required = false) String originFloor,
            @RequestParam(required = false) String destinationFloor,
            @RequestParam(required = false, defaultValue = "false") boolean avoidStairs) {

        validateNotBlank(buildingId, PARAM_BUILDING_ID);
        validateNotBlank(origin, "origin");
        validateNotBlank(destination, "destination");
        validateBuildingId(buildingId);

        String safeBuildingId = sanitize(buildingId);
        String safeOrigin = sanitize(origin);
        String safeDestination = sanitize(destination);
        String safeOriginFloor = sanitize(originFloor);
        String safeDestFloor = sanitize(destinationFloor);

        if (safeOrigin.trim().equalsIgnoreCase(safeDestination.trim())) {
            throw new InvalidIndoorRequestException(
                    "origin and destination must be different.");
        }

        IndoorDirectionResponse response = indoorDirectionService.getIndoorDirections(
                safeBuildingId, safeOrigin, safeDestination, safeOriginFloor, safeDestFloor, avoidStairs);

        if (response.getRoutePoints() == null || response.getRoutePoints().isEmpty()) {
            throw new IndoorResourceNotFoundException(
                    "No route found from '" + safeOrigin + "' to '" + safeDestination
                            + "' in building '" + safeBuildingId + "'.");
        }

        return response;
    }
    
    @GetMapping("/rooms")
    public List<String> getAvailableRooms(
            @RequestParam String buildingId,
            @RequestParam(required = false) String floor) {

        validateNotBlank(buildingId, PARAM_BUILDING_ID);
        validateBuildingId(buildingId);

        String safeBuildingId = sanitize(buildingId);
        String safeFloor = sanitize(floor);

        List<String> rooms = indoorDirectionService.getAvailableRooms(safeBuildingId, safeFloor);
        if (rooms.isEmpty()) {
            throw new IndoorResourceNotFoundException(
                    "No rooms found for building '" + safeBuildingId
                    + "'" + (safeFloor != null ? FLOOR_LABEL + safeFloor + "'" : "") + ".");
        }
        return rooms;
    }
    
    @GetMapping("/waypoints")
    public List<WaypointResponse> getWaypoints(
            @RequestParam String buildingId,
            @RequestParam(required = false) String floor) {

        validateNotBlank(buildingId, PARAM_BUILDING_ID);
        validateBuildingId(buildingId);

        String safeBuildingId = sanitize(buildingId);
        String safeFloor = sanitize(floor);

        List<WaypointResponse> waypoints = indoorDirectionService.getWaypoints(safeBuildingId, safeFloor);
        if (waypoints.isEmpty()) {
            throw new IndoorResourceNotFoundException(
                    "No waypoints found for building '" + safeBuildingId
                    + "'" + (safeFloor != null ? FLOOR_LABEL + safeFloor + "'" : "") + ".");
        }
        return waypoints;
    }
    
    @GetMapping("/room-points")
    public List<RoomPointResponse> getRoomPoints(
            @RequestParam String buildingId,
            @RequestParam(required = false) String floor) {

        validateNotBlank(buildingId, PARAM_BUILDING_ID);
        validateBuildingId(buildingId);

        String safeBuildingId = sanitize(buildingId);
        String safeFloor = sanitize(floor);

        List<RoomPointResponse> roomPoints = indoorDirectionService.getRoomPoints(safeBuildingId, safeFloor);
        if (roomPoints.isEmpty()) {
            throw new IndoorResourceNotFoundException(
                    "No room points found for building '" + safeBuildingId
                    + "'" + (safeFloor != null ? FLOOR_LABEL + safeFloor + "'" : "") + ".");
        }
        return roomPoints;
    }

    @GetMapping("/pois")
    public List<PoiResponse> getPointsOfInterest(
            @RequestParam String buildingId,
            @RequestParam(required = false) String floor) {

        validateNotBlank(buildingId, PARAM_BUILDING_ID);
        validateBuildingId(buildingId);

        return indoorDirectionService.getPointsOfInterest(sanitize(buildingId), sanitize(floor));
    }

    /** Strip newlines and control characters to prevent log injection. */
    private static String sanitize(String input) {
        if (input == null) return null;
        return input.replaceAll("[\\r\\n\\t]", "");
    }

    private void validateNotBlank(String value, String paramName) {
        if (value == null || value.trim().isEmpty()) {
            throw new InvalidIndoorRequestException(
                    "'" + paramName + "' must not be blank.");
        }
    }

    private void validateBuildingId(String buildingId) {
        if (VALID_SHORT_CODES.contains(buildingId)) {
            return;
        }
        for (String prefix : VALID_BUILDING_PREFIXES) {
            if (buildingId.startsWith(prefix)) {
                return;
            }
        }
        throw new InvalidIndoorRequestException(
                "Unknown building ID '" + buildingId
                + "'. Valid prefixes: " + VALID_BUILDING_PREFIXES
                + ", valid short codes: " + VALID_SHORT_CODES + ".");
    }
    
    public static class PoiResponse {
        private final double x;
        private final double y;
        private final String id;    // this id is to be able to differentiate between the different same name POIs
        private final String displayName;
        private final String type;

        public PoiResponse(double x, double y, String id, String displayName, String type) {
            this.x = x;
            this.y = y;
            this.id = id;
            this.displayName = displayName;
            this.type = type;
        }

        public double getX() { return x; }
        public double getY() { return y; }
        public String getId() { return id; }
        public String getDisplayName() { return displayName; }
        public String getType() { return type; }
    }

    public static class WaypointResponse {
        private final double x;
        private final double y;
        private final String id;

        public WaypointResponse(double x, double y, String id) {
            this.x = x;
            this.y = y;
            this.id = id;
        }

        public double getX() { return x; }
        public double getY() { return y; }
        public String getId() { return id; }
    }

    public static class RoomPointResponse {
        private final double x;
        private final double y;
        private final String id;

        public RoomPointResponse(double x, double y, String id) {
            this.x = x;
            this.y = y;
            this.id = id;
        }

        public double getX() { return x; }
        public double getY() { return y; }
        public String getId() { return id; }
    }
}