package com.qaup.collision.road.service;

import com.qaup.collision.common.model.GeoPosition; // Assuming GeoPosition is in common.model
import com.qaup.collision.config.properties.AirportRoadsProperties;
import com.qaup.collision.config.properties.RoadProperties;
import com.qaup.collision.road.model.RoadDirectionality;
import com.qaup.collision.road.model.RoadInfo;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.index.strtree.STRtree;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * Service responsible for managing and providing access to the airport road network data.
 * It loads road configuration, processes it into runtime models with JTS geometry,
 * and provides query capabilities.
 */
@Service
@RequiredArgsConstructor // Lombok: Injects final fields via constructor
@Slf4j
public class RoadNetworkService {

    private final AirportRoadsProperties airportRoadsProperties;
    // Use ConcurrentHashMap for thread safety if needed in the future
    private final Map<String, RoadInfo> roadInfoMap = new ConcurrentHashMap<>();
    // WGS84 SRID is 4326. PrecisionModel can be adjusted if needed.
    private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
    // STRtree is a common choice for static spatial indexing in JTS
    private STRtree spatialIndex = new STRtree(); // Initialize empty

    @PostConstruct
    public void initialize() {
        log.info("Initializing Road Network Service...");
        if (airportRoadsProperties == null || airportRoadsProperties.getRoads() == null || airportRoadsProperties.getRoads().isEmpty()) {
            log.warn("Airport roads properties not loaded or empty. Road network will be inactive.");
            // Ensure spatialIndex is built even if empty, to avoid NullPointerExceptions later
            spatialIndex.build();
            return;
        }

        List<RoadInfo> loadedRoads = new ArrayList<>();
        for (RoadProperties props : airportRoadsProperties.getRoads()) {
            if (props.getId() == null) {
                log.warn("Skipping road definition due to missing ID: {}", props.getName());
                continue;
            }
            try {
                // 1. Create centerline LineString
                if (props.getGeometry() == null || props.getGeometry().getCoordinates() == null || props.getGeometry().getCoordinates().size() < 2) {
                    log.warn("Skipping road ID [{}] due to invalid geometry (less than 2 coordinates)", props.getId());
                    continue;
                }
                Coordinate[] coords = props.getGeometry().getCoordinates().stream()
                        .filter(coord -> coord != null && coord.size() >= 2)
                        .map(coord -> new Coordinate(coord.get(0), coord.get(1))) // lon, lat
                        .toArray(Coordinate[]::new);
                // Validate coordinate array length again after filtering nulls
                if (coords.length < 2) {
                     log.warn("Skipping road ID [{}] after filtering invalid coordinates, resulting in less than 2 valid points", props.getId());
                    continue;
                }
                LineString centerline = geometryFactory.createLineString(coords);

                // 2. Calculate boundary Polygon (using approximate buffering for now)
                double widthMeters = props.getWidth();
                Polygon boundary;
                if (widthMeters > 0) {
                    // TODO: Implement more accurate buffering (e.g., project to UTM)
                    double bufferDistanceDegrees = metersToApproximateDegrees(widthMeters / 2.0, centerline.getCentroid().getY());
                    // Use BufferParameters for end cap style if needed (default is round)
                    boundary = (Polygon) centerline.buffer(bufferDistanceDegrees);
                } else {
                    log.warn("Road ID [{}] has invalid or zero width. Boundary will not be calculated accurately.", props.getId());
                    // Create a minimal envelope or empty polygon as a fallback?
                    // For now, let's skip adding roads without a valid boundary to the index, or handle queries carefully.
                    boundary = geometryFactory.createPolygon(); // Create an empty polygon
                    // Or alternatively: boundary = (Polygon) centerline.getEnvelope(); - Not really the boundary
                }

                // 3. Unit conversions and attribute mapping
                Double speedLimitKph = props.getSpeedLimit();
                Double heightLimitM = props.getHeightLimit();
                Double widthLimitM = props.getWidthLimit();
                RoadDirectionality directionality = parseDirectionality(props.getDirectionality());
                boolean prohibited = props.getProhibited() != null && props.getProhibited();

                // 4. Build RoadInfo
                RoadInfo roadInfo = RoadInfo.builder()
                        .id(props.getId())
                        .name(props.getName())
                        .width(widthMeters)
                        .speedLimitKilometersPerHour(speedLimitKph)
                        .directionality(directionality)
                        .heightLimitMeters(heightLimitM)
                        .widthLimitMeters(widthLimitM)
                        .prohibited(prohibited)
                        .centerline(centerline)
                        .boundary(boundary)
                        .build();

                roadInfoMap.put(roadInfo.getId(), roadInfo);
                // Only add roads with valid boundaries to the spatial index
                if (!boundary.isEmpty()) {
                    loadedRoads.add(roadInfo);
                }

            } catch (Exception e) {
                log.error("Failed to process road properties with id [{}]: {}", props.getId(), e.getMessage(), e);
            }
        }

        // 5. Build spatial index
        if (!loadedRoads.isEmpty()) {
            spatialIndex = new STRtree(loadedRoads.size());
            for (RoadInfo road : loadedRoads) {
                spatialIndex.insert(road.getBoundary().getEnvelopeInternal(), road);
            }
        }
        // Build the index even if it remains empty
        spatialIndex.build();

        log.info("Road Network Service initialized. Loaded {} roads into map, {} roads indexed.", roadInfoMap.size(), loadedRoads.size());
        // Debug: Print loaded road IDs
        log.debug("Loaded road IDs in map: {}", roadInfoMap.keySet()); 
    }

    // --- Service Interface Methods ---

    /**
     * Gets road information by its unique ID.
     *
     * @param roadId The ID of the road.
     * @return An Optional containing the RoadInfo if found, otherwise empty.
     */
    public Optional<RoadInfo> getRoadById(String roadId) {
        // Debug: Log input and map check
        log.debug("getRoadById called with ID: '{}'", roadId);
        log.debug("roadInfoMap contains key '{}': {}", roadId, roadInfoMap.containsKey(roadId)); 
        return Optional.ofNullable(roadInfoMap.get(roadId));
    }

    /**
     * Finds all roads whose boundary polygon contains the given geographic position.
     *
     * @param geoPosition The geographic position (latitude, longitude).
     * @return A list of RoadInfo objects containing the point. May be empty.
     */
    public List<RoadInfo> findRoadsContainingPoint(GeoPosition geoPosition) {
        if (geoPosition == null) return Collections.emptyList();
        Point point = geometryFactory.createPoint(new Coordinate(geoPosition.getLongitude(), geoPosition.getLatitude()));
        Envelope searchEnvelope = point.getEnvelopeInternal();

        @SuppressWarnings("unchecked") // JTS STRtree query returns List
        List<RoadInfo> candidates = spatialIndex.query(searchEnvelope);

        // Filter candidates using precise 'covers' check
        return candidates.stream()
                .filter(road -> road.getBoundary().covers(point))
                .collect(Collectors.toList());
    }

    /**
     * Finds the most likely or 'dominant' road at a given geographic position.
     * This is useful when a point might fall within the overlapping boundaries of multiple roads
     * (e.g., near intersections or due to buffer approximations).
     * Current simple implementation returns the first one found.
     * // TODO: Implement a more sophisticated selection logic if needed (e.g., based on area, road type).
     *
     * @param geoPosition The geographic position.
     * @return An Optional containing the dominant RoadInfo if found, otherwise empty.
     */
    public Optional<RoadInfo> findDominantRoadAt(GeoPosition geoPosition) {
        // Simple strategy: return the first road found containing the point.
        return findRoadsContainingPoint(geoPosition).stream().findFirst();
    }

    /**
     * Gets the speed limit in kilometers per hour at a specific geographic position.
     * It finds the dominant road at the position and returns its speed limit.
     *
     * @param geoPosition The geographic position.
     * @return An Optional containing the speed limit (km/h) if the point is on a road
     *         with a defined limit, otherwise empty.
     */
    public Optional<Double> getSpeedLimitKilometersPerHourAt(GeoPosition geoPosition) {
        return findDominantRoadAt(geoPosition)
                .map(RoadInfo::getSpeedLimitKilometersPerHour)
                .filter(Objects::nonNull); // Ensure the speed limit itself is not null
    }

    // --- Private Helper Methods ---

    /**
     * Parses the directionality string from the configuration into the RoadDirectionality enum.
     */
    private RoadDirectionality parseDirectionality(String directionalityStr) {
        if (directionalityStr == null) {
            return RoadDirectionality.UNKNOWN;
        }
        String upperCaseDir = directionalityStr.trim().toUpperCase();
        switch (upperCaseDir) {
            case "ONE_WAY":
            case "ONEWAY":
                return RoadDirectionality.ONE_WAY;
            case "TWO_WAY":
            case "TWOWAY":
                return RoadDirectionality.TWO_WAY;
            default:
                log.trace("Unknown directionality string: '{}'. Defaulting to UNKNOWN.", directionalityStr);
                return RoadDirectionality.UNKNOWN;
        }
    }

    /**
     * Approximates the conversion from meters to decimal degrees at a given latitude.
     * This is a rough estimate suitable mainly for buffering operations near the equator
     * or for small distances. For accuracy, projection is needed.
     *
     * @param meters Distance in meters.
     * @param latitude Latitude in decimal degrees.
     * @return Approximate distance in decimal degrees.
     */
    private double metersToApproximateDegrees(double meters, double latitude) {
        // Based on the approximate length of a degree of latitude and longitude.
        // Using latitude degree length as a rough average estimate.
        // 1 degree latitude is approx 111,132 meters.
        return meters / 111132.0;
        // A more complex formula considering longitude:
        // double latRad = Math.toRadians(latitude);
        // double metersPerDegreeLon = 111320.0 * Math.cos(latRad);
        // double metersPerDegreeLat = 111132.0;
        // return meters / Math.min(metersPerDegreeLat, metersPerDegreeLon); // Use smaller for conservative buffer?
    }
} 