package org.github.tess1o.geopulse.gps.rest;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import lombok.extern.slf4j.Slf4j;
import org.github.tess1o.geopulse.auth.service.CurrentUserService;
import org.github.tess1o.geopulse.gps.exceptions.GpsCoordinateDuplicateException;
import org.github.tess1o.geopulse.gps.model.*;
import org.github.tess1o.geopulse.gps.service.GpsPointService;
import org.github.tess1o.geopulse.gps.service.simplification.PathSimplificationService;
import org.github.tess1o.geopulse.gps.service.simplification.TimelineSegmentBoundary;
import org.github.tess1o.geopulse.gpssource.model.GpsSourceConfigEntity;
import org.github.tess1o.geopulse.gpssource.service.GpsSourceService;
import org.github.tess1o.geopulse.coverage.model.CoverageStatus;
import org.github.tess1o.geopulse.coverage.service.CoverageProcessingService;
import org.github.tess1o.geopulse.coverage.service.CoverageService;
import org.github.tess1o.geopulse.prometheus.GeoPulseWorkloadMetrics;
import org.github.tess1o.geopulse.shared.api.ApiResponse;
import jakarta.validation.Valid;
import org.github.tess1o.geopulse.shared.geo.GpsPoint;
import org.github.tess1o.geopulse.streaming.service.AsyncTimelineGenerationService;
import org.github.tess1o.geopulse.streaming.config.TimelineConfigurationProvider;
import org.github.tess1o.geopulse.streaming.config.TimelineConfig;
import org.github.tess1o.geopulse.user.model.DistanceUnit;
import org.github.tess1o.geopulse.shared.gps.GpsSourceType;
import org.github.tess1o.geopulse.user.model.UserEntity;
import org.jboss.resteasy.reactive.RestHeader;

import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

/**
 * REST resource for GPS point data.
 */
@Path("/api/gps")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Slf4j
@Tag(name = "User: GPS Data", description = "Ingest, query, update, export, and delete GPS points.")
public class GpsPointResource {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final String OK_RESPONSE = "OK";
    private static final int DEFAULT_RAW_MAP_POINTS_LIMIT = 10000;
    private static final int MAX_RAW_MAP_POINTS_LIMIT = 25000;

    private final GpsPointService gpsPointService;
    private final CurrentUserService currentUserService;
    private final PathSimplificationService pathSimplificationService;
    private final TimelineConfigurationProvider configurationProvider;
    private final GpsSourceService gpsSourceService;
    private final AsyncTimelineGenerationService asyncTimelineGenerationService;
    private final CoverageService coverageService;
    private final CoverageProcessingService coverageProcessingService;

    @Inject
    GeoPulseWorkloadMetrics workloadMetrics;

    @Inject
    public GpsPointResource(GpsPointService gpsPointService,
                            CurrentUserService currentUserService,
                            PathSimplificationService pathSimplificationService,
                            TimelineConfigurationProvider configurationProvider,
                            GpsSourceService gpsSourceService,
                            AsyncTimelineGenerationService asyncTimelineGenerationService,
                            CoverageService coverageService,
                            CoverageProcessingService coverageProcessingService) {
        this.gpsPointService = gpsPointService;
        this.currentUserService = currentUserService;
        this.pathSimplificationService = pathSimplificationService;
        this.configurationProvider = configurationProvider;
        this.gpsSourceService = gpsSourceService;
        this.asyncTimelineGenerationService = asyncTimelineGenerationService;
        this.coverageService = coverageService;
        this.coverageProcessingService = coverageProcessingService;
    }

    /**
     * Allows to store GPS points for a user sent by the mobile app.
     * This endpoint requires authentication.
     *
     * @param request Current locations from the mobile app.
     * @return The HTTP code, (200, 409, 500)
     */
    @POST
    @Path("/points")
    @Consumes(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response ingestMobileAppPoints(@Valid GpsPointsRetentionRequest request,
                                             @RestHeader("X-Device-Id") String xDeviceId) {
        var deviceId = xDeviceId == null ? "MOBILE APP" : xDeviceId;
        var points = request.getPoints() == null ? Collections.<GpsPointDTO>emptyList() : request.getPoints();

        try {
            UUID userId = currentUserService.getCurrentUserId();
            GpsSourceConfigEntity config = buildMobileAppDefaultConfig();

            gpsPointService.saveMobileAppGpsPoints(points, deviceId, userId, GpsSourceType.MOBILE_APP, config);
            return Response.ok(ApiResponse.success(OK_RESPONSE))
                    .build();
        } catch (GpsCoordinateDuplicateException ex) {
          return Response.status(Response.Status.CONFLICT)
                  .entity(ApiResponse.error("Duplicate point"))
                  .build();
        } catch (Exception e) {
            log.error("Failed to ingest mobile app GPS point", e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to ingest GPS point"))
                    .build();
        }
    }

    private GpsSourceConfigEntity buildMobileAppDefaultConfig() {
        GpsSourceConfigEntity config = new GpsSourceConfigEntity();
        config.setSourceType(GpsSourceType.MOBILE_APP);
        config.setActive(true);
        config.setFilterInaccurateData(gpsSourceService.isDefaultFilterInaccurateDataEnabled());
        config.setMaxAllowedAccuracy(gpsSourceService.getDefaultMaxAllowedAccuracy());
        config.setMaxAllowedSpeed(gpsSourceService.getDefaultMaxAllowedSpeed());
        config.setEnableDuplicateDetection(gpsSourceService.isDefaultDuplicateDetectionEnabled());
        config.setDuplicateDetectionThresholdMinutes(gpsSourceService.getDefaultDuplicateDetectionThresholdMinutes());
        return config;
    }

    /**
     * Get a GPS point path for a user within a specified time period.
     * This endpoint requires authentication.
     *
     * @param startTime The start of the time period (ISO-8601 format)
     * @param endTime   The end of the time period (ISO-8601 format)
     * @return The GPS point path
     */
    @GET
    @Path("/path")
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response getGpsPointPath(
            @QueryParam("startTime") String startTime,
            @QueryParam("endTime") String endTime) {
        UserEntity user = currentUserService.getCurrentUser();
        log.info("Received request to get GPS point path for user {} between {} and {}", user.getEmail(), startTime, endTime);

        try {
            Instant start = startTime != null ? Instant.parse(startTime) : Instant.EPOCH;
            Instant end = endTime != null ? Instant.parse(endTime) : Instant.now();
            GpsPointPathDTO path = gpsPointService.getGpsPointPath(user.getId(), start, end);
            TimelineConfig config = configurationProvider.getConfigurationForUser(user.getId());

            if (config.getPathSimplificationEnabled()) {
                path.setPoints(simplifyPath(user.getId(), start, end, path.getPoints(), config));
            }

            int gapThreshold = (config.getDataGapThresholdSeconds() != null && config.getDataGapThresholdSeconds() > 0)
                    ? config.getDataGapThresholdSeconds()
                    : 10800;
            path.setSegments(segmentPath(path.getPoints(), gapThreshold));

            return Response.ok(ApiResponse.success(path)).build();
        } catch (DateTimeParseException e) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(ApiResponse.error("Invalid time format. Use ISO-8601 format (e.g., 2023-01-01T00:00:00Z)"))
                    .build();
        } catch (Exception e) {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to retrieve GPS point path: " + e.getMessage()))
                    .build();
        }
    }

    @GET
    @Path("/map-points")
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response getRawGpsMapPoints(
            @QueryParam("startTime") String startTime,
            @QueryParam("endTime") String endTime,
            @QueryParam("limit") @DefaultValue("" + DEFAULT_RAW_MAP_POINTS_LIMIT) int limit) {
        UUID userId = currentUserService.getCurrentUserId();

        try {
            if (limit < 1 || limit > MAX_RAW_MAP_POINTS_LIMIT) {
                return Response.status(Response.Status.BAD_REQUEST)
                        .entity(ApiResponse.error("Limit must be between 1 and " + MAX_RAW_MAP_POINTS_LIMIT))
                        .build();
            }

            Instant start = startTime != null ? Instant.parse(startTime) : Instant.EPOCH;
            Instant end = endTime != null ? Instant.parse(endTime) : Instant.now();
            RawGpsPointMapResponseDTO result = gpsPointService.getRawGpsMapPoints(userId, start, end, limit);
            return Response.ok(ApiResponse.success(result)).build();
        } catch (DateTimeParseException e) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(ApiResponse.error("Invalid time format. Use ISO-8601 format (e.g., 2023-01-01T00:00:00Z)"))
                    .build();
        } catch (Exception e) {
            log.error("Failed to retrieve raw GPS map points for user {}", userId, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to retrieve raw GPS map points: " + e.getMessage()))
                    .build();
        }
    }

    @GET
    @Path("/points/{pointId}/location")
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response resolveRawGpsPointLocation(@PathParam("pointId") Long pointId) {
        UUID userId = currentUserService.getCurrentUserId();

        try {
            RawGpsPointLocationDTO result = gpsPointService.resolveRawGpsPointLocation(userId, pointId);
            return Response.ok(ApiResponse.success(result)).build();
        } catch (NotFoundException e) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(ApiResponse.error(e.getMessage()))
                    .build();
        } catch (ForbiddenException e) {
            return Response.status(Response.Status.FORBIDDEN)
                    .entity(ApiResponse.error("Access denied"))
                    .build();
        } catch (Exception e) {
            log.error("Failed to resolve raw GPS point location {} for user {}", pointId, userId, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to resolve GPS point location: " + e.getMessage()))
                    .build();
        }
    }

    /**
     * Splits a flat list of GPS points into contiguous segments by breaking wherever
     * two consecutive points are separated by more than {@code gapThresholdSeconds}.
     * Each segment is rendered as a separate polyline on the map, preventing phantom
     * lines across unrecorded periods (e.g. between two separate GPX imports).
     */
    private List<List<GpsPoint>> segmentPath(List<? extends GpsPoint> points, int gapThresholdSeconds) {
        if (points == null || points.isEmpty()) return List.of();

        List<List<GpsPoint>> segments = new ArrayList<>();
        List<GpsPoint> current = new ArrayList<>();

        for (int i = 0; i < points.size(); i++) {
            GpsPoint p = points.get(i);
            if (i > 0) {
                GpsPoint prev = points.get(i - 1);
                if (prev.getTimestamp() != null && p.getTimestamp() != null) {
                    long gapSeconds = Duration.between(prev.getTimestamp(), p.getTimestamp()).getSeconds();
                    if (gapSeconds > gapThresholdSeconds) {
                        segments.add(current);
                        current = new ArrayList<>();
                    }
                }
            }
            current.add(p);
        }
        if (!current.isEmpty()) segments.add(current);
        return segments;
    }

    private List<? extends GpsPoint> simplifyPath(UUID userId, Instant start, Instant end, List<? extends GpsPoint> points, TimelineConfig config) {
        // Apply segment-aware GPS path simplification
        List<TimelineSegmentBoundary> segments = pathSimplificationService.getTimelineSegments(userId, start, end);

        List<? extends GpsPoint> simplifiedPath;
        if (segments.isEmpty()) {
            // Fallback to legacy simplification if no timeline segments found
            log.debug("No timeline segments found, falling back to legacy simplification");
            simplifiedPath = pathSimplificationService.simplify(points, config);
        } else {
            // Use segment-aware simplification with per-segment minimum point guarantees
            log.debug("Using segment-aware simplification with {} segments", segments.size());
            simplifiedPath = pathSimplificationService.simplifyWithSegments(points, segments, config);
        }
        return simplifiedPath;
    }

    /**
     * Get summary statistics for GPS points with optional filters.
     * This endpoint requires authentication and uses the user's stored timezone.
     *
     * @return Summary statistics for the user's GPS points
     */
    @GET
    @Path("/summary")
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response getGpsPointSummary(
            @QueryParam("startTime") String startTime,
            @QueryParam("endTime") String endTime,
            @QueryParam("accuracyMin") Double accuracyMin,
            @QueryParam("accuracyMax") Double accuracyMax,
            @QueryParam("speedMin") Double speedMin,
            @QueryParam("speedMax") Double speedMax,
            @QueryParam("sourceTypes") String sourceTypes) {
        UserEntity user = currentUserService.getCurrentUser();
        log.info("Received request to get GPS point summary for user {} with timezone {}", user.getId(), user.getTimezone());

        try {
            // Parse filters
            GpsPointFilterDTO filters = buildFilters(startTime, endTime, accuracyMin, accuracyMax,
                    speedMin, speedMax, sourceTypes);

            GpsPointSummaryDTO summary;

            try {
                ZoneId userTimezone = ZoneId.of(user.getTimezone());
                summary = gpsPointService.getGpsPointSummaryWithFilters(user.getId(), userTimezone, filters);
            } catch (DateTimeException e) {
                log.warn("Invalid timezone '{}' for user {}, falling back to UTC", user.getTimezone(), user.getId());
                summary = gpsPointService.getGpsPointSummaryWithFilters(user.getId(), ZoneId.of("UTC"), filters);
            }

            return Response.ok(ApiResponse.success(summary)).build();
        } catch (Exception e) {
            log.error("Failed to retrieve GPS point summary for user {}", user.getId(), e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to retrieve GPS point summary: " + e.getMessage()))
                    .build();
        }
    }

    /**
     * Get paginated GPS points with optional filtering and sorting.
     * This endpoint requires authentication.
     *
     * @param page      Page number (default: 1)
     * @param limit     Number of items per page (default: 50)
     * @param startDate Start date filter (format: YYYY-MM-DD)
     * @param endDate   End date filter (format: YYYY-MM-DD)
     * @param sortBy    Field to sort by (default: timestamp)
     * @param sortOrder Sort order: asc or desc (default: desc)
     * @return Paginated GPS points
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response getGpsPoints(
            @QueryParam("page") @DefaultValue("1") int page,
            @QueryParam("limit") @DefaultValue("50") int limit,
            @QueryParam("startDate") String startDate,
            @QueryParam("endDate") String endDate,
            @QueryParam("startTime") String startTime,
            @QueryParam("endTime") String endTime,
            @QueryParam("sortBy") @DefaultValue("timestamp") String sortBy,
            @QueryParam("sortOrder") @DefaultValue("desc") String sortOrder,
            @QueryParam("accuracyMin") Double accuracyMin,
            @QueryParam("accuracyMax") Double accuracyMax,
            @QueryParam("speedMin") Double speedMin,
            @QueryParam("speedMax") Double speedMax,
            @QueryParam("sourceTypes") String sourceTypes) {
        UUID userId = currentUserService.getCurrentUserId();
        log.info("Received request to get GPS points for user {} - page: {}, limit: {}, filters: accuracyMin: {}, accuracyMax: {}, speedMin: {}, speedMax: {}",
                userId, page, limit, accuracyMin, accuracyMax, speedMin, speedMax);

        try {
            // Validate pagination parameters
            if (page < 1) {
                return Response.status(Response.Status.BAD_REQUEST)
                        .entity(ApiResponse.error("Page number must be greater than 0"))
                        .build();
            }
            if (limit < 1 || limit > 1000) {
                return Response.status(Response.Status.BAD_REQUEST)
                        .entity(ApiResponse.error("Limit must be between 1 and 1000"))
                        .build();
            }

            // Validate sort order
            if (!sortOrder.equalsIgnoreCase("asc") && !sortOrder.equalsIgnoreCase("desc")) {
                return Response.status(Response.Status.BAD_REQUEST)
                        .entity(ApiResponse.error("Sort order must be 'asc' or 'desc'"))
                        .build();
            }

            // Build filters
            GpsPointFilterDTO filters = buildFilters(startTime != null ? startTime : (startDate != null ? startDate : null),
                    endTime != null ? endTime : endDate,
                    accuracyMin, accuracyMax, speedMin, speedMax, sourceTypes);

            GpsPointPageDTO result = gpsPointService.getGpsPointsPageWithFilters(userId, filters, page, limit, sortBy, sortOrder);
            return Response.ok(ApiResponse.success(result)).build();
        } catch (DateTimeParseException e) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(ApiResponse.error("Invalid date/time format"))
                    .build();
        } catch (Exception e) {
            log.error("Failed to retrieve GPS points for user {}", userId, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to retrieve GPS points: " + e.getMessage()))
                    .build();
        }
    }

    /**
     * Export GPS points as CSV with streaming to prevent OOM.
     * This endpoint requires authentication and supports all filters.
     *
     * @param startDate Start date filter (format: YYYY-MM-DD)
     * @param endDate   End date filter (format: YYYY-MM-DD)
     * @return CSV file with GPS points (streamed)
     */
    @GET
    @Path("/export")
    @Produces("text/csv")
    @RolesAllowed({"USER", "ADMIN"})
    public Response exportGpsPoints(
            @QueryParam("startDate") String startDate,
            @QueryParam("endDate") String endDate,
            @QueryParam("startTime") String startTime,
            @QueryParam("endTime") String endTime,
            @QueryParam("accuracyMin") Double accuracyMin,
            @QueryParam("accuracyMax") Double accuracyMax,
            @QueryParam("speedMin") Double speedMin,
            @QueryParam("speedMax") Double speedMax,
            @QueryParam("sourceTypes") String sourceTypes,
            @QueryParam("ids") String ids) {
        UserEntity user = currentUserService.getCurrentUser();
        log.info("Received request to export GPS points for user {} with filters", user.getId());

        try {
            // Build filters
            GpsPointFilterDTO filters = buildFilters(
                    startTime != null ? startTime : startDate,
                    endTime != null ? endTime : endDate,
                    accuracyMin, accuracyMax, speedMin, speedMax, sourceTypes);

            // If IDs are provided, add them to filters (overrides other filters)
            if (ids != null && !ids.trim().isEmpty()) {
                try {
                    List<Long> gpsPointIds = Arrays.stream(ids.split(","))
                            .map(String::trim)
                            .filter(s -> !s.isEmpty())
                            .map(Long::parseLong)
                            .collect(Collectors.toList());
                    filters.setGpsPointIds(gpsPointIds);
                    log.info("Exporting {} specific GPS points by IDs", gpsPointIds.size());
                } catch (NumberFormatException e) {
                    log.warn("Invalid GPS point IDs format: {}", ids, e);
                    return Response.status(Response.Status.BAD_REQUEST)
                            .entity(ApiResponse.error("Invalid GPS point IDs format"))
                            .build();
                }
            }

            // Create streaming output to avoid loading all data into memory
            StreamingOutput stream = output -> {
                try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
                    // Write CSV header
                    if (user.getDistanceUnit() == DistanceUnit.KILOMETERS) {
                        writer.write("timestamp,latitude,longitude,accuracy,battery,velocity(km/h),altitude,sourceType,telemetry\n");
                    } else {
                        writer.write("timestamp,latitude,longitude,accuracy,battery,velocity(mph),altitude,sourceType,telemetry\n");
                    }

                    // Stream GPS points in batches of 1000
                    gpsPointService.streamGpsPointsForExport(user.getId(), filters, 1000, batch -> {
                        try {
                            for (GpsPointEntity point : batch) {
                                writer.write(formatCsvRow(point, user.getDistanceUnit()));
                            }
                            writer.flush(); // Flush after each batch
                        } catch (Exception e) {
                            throw new RuntimeException("Error writing CSV batch", e);
                        }
                    });
                }
            };

            String filename = String.format("gps-points-export-%s.csv",
                    startDate != null && endDate != null ? startDate + "_" + endDate : "all");

            return Response.ok(stream)
                    .header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
                    .header("Content-Type", "text/csv; charset=utf-8")
                    .build();
        } catch (DateTimeParseException e) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(ApiResponse.error("Invalid date/time format"))
                    .build();
        } catch (Exception e) {
            log.error("Failed to export GPS points for user {}", user.getId(), e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to export GPS points: " + e.getMessage()))
                    .build();
        }
    }

    /**
     * Parse date/time parameters with fallback support.
     * Prioritizes ISO-8601 timestamps over date-only strings.
     */
    private Instant parseDateTime(String timeStr, String dateStr, boolean isStartDate) {
        // First try to parse as ISO-8601 timestamp (e.g., 2025-08-10T00:00:00.000Z)
        if (timeStr != null && !timeStr.trim().isEmpty()) {
            try {
                return Instant.parse(timeStr.trim());
            } catch (DateTimeParseException e) {
                throw new DateTimeParseException("Invalid timestamp format. Use ISO-8601 format (e.g., 2025-08-10T00:00:00.000Z)", timeStr, 0);
            }
        }

        // Fallback to date-only parsing (e.g., 2025-08-10)
        return parseDate(dateStr, isStartDate);
    }

    /**
     * Parse date string to Instant.
     *
     * @param dateStr     Date string in YYYY-MM-DD format
     * @param isStartDate True if this is a start date (start of day), false for end date (end of day)
     * @return Parsed Instant or null if dateStr is null
     */
    private Instant parseDate(String dateStr, boolean isStartDate) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return isStartDate ? Instant.EPOCH : Instant.now();
        }

        LocalDate date = LocalDate.parse(dateStr.trim());
        return isStartDate ? date.atStartOfDay().toInstant(java.time.ZoneOffset.UTC)
                : date.atTime(23, 59, 59).toInstant(java.time.ZoneOffset.UTC);
    }

    /**
     * Format a single GPS point as a CSV row.
     *
     * @param point       GPS point entity
     * @param distanceUnit User's distance unit preference
     * @return CSV row string
     */
    private String formatCsvRow(GpsPointEntity point, DistanceUnit distanceUnit) {
        StringBuilder row = new StringBuilder();

        double velocity = point.getVelocity() != null ? point.getVelocity() : 0.0;
        if (distanceUnit == DistanceUnit.MILES) {
            velocity = velocity * 0.621371; // Convert km/h to mph
        }

        appendCsvValue(row, point.getTimestamp().toString());
        appendCsvValue(row, point.getLatitude());
        appendCsvValue(row, point.getLongitude());
        appendCsvValue(row, point.getAccuracy());
        appendCsvValue(row, point.getBattery());
        appendCsvValue(row, velocity);
        appendCsvValue(row, point.getAltitude());
        appendCsvValue(row, point.getSourceType() != null ? point.getSourceType().name() : "");
        appendCsvValue(row, telemetryToJson(point.getTelemetry()));
        row.append("\n");

        return row.toString();
    }

    private String telemetryToJson(Map<String, Object> telemetry) {
        if (telemetry == null || telemetry.isEmpty()) {
            return "";
        }

        try {
            return OBJECT_MAPPER.writeValueAsString(telemetry);
        } catch (JsonProcessingException e) {
            log.warn("Failed to serialize telemetry to JSON for CSV export", e);
            return "";
        }
    }

    private void appendCsvValue(StringBuilder row, Object value) {
        if (!row.isEmpty()) {
            row.append(",");
        }
        String raw = value != null ? value.toString() : "";
        row.append(escapeCsv(raw));
    }

    private String escapeCsv(String value) {
        boolean shouldQuote = value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r");
        if (!shouldQuote) {
            return value;
        }
        return "\"" + value.replace("\"", "\"\"") + "\"";
    }

    /**
     * Build filter DTO from query parameters.
     *
     * @return GpsPointFilterDTO with all filters
     */
    private GpsPointFilterDTO buildFilters(String startTime, String endTime,
                                           Double accuracyMin, Double accuracyMax,
                                           Double speedMin, Double speedMax,
                                           String sourceTypes) {
        GpsPointFilterDTO.GpsPointFilterDTOBuilder builder = GpsPointFilterDTO.builder();

        // Parse time range
        if (startTime != null && !startTime.trim().isEmpty()) {
            try {
                Instant start = Instant.parse(startTime.trim());
                builder.startTime(start);
            } catch (DateTimeParseException e) {
                // Try parsing as date
                try {
                    LocalDate date = LocalDate.parse(startTime.trim());
                    builder.startTime(date.atStartOfDay().toInstant(java.time.ZoneOffset.UTC));
                } catch (DateTimeParseException ex) {
                    log.warn("Failed to parse startTime: {}", startTime);
                }
            }
        }

        if (endTime != null && !endTime.trim().isEmpty()) {
            try {
                Instant end = Instant.parse(endTime.trim());
                builder.endTime(end);
            } catch (DateTimeParseException e) {
                // Try parsing as date
                try {
                    LocalDate date = LocalDate.parse(endTime.trim());
                    builder.endTime(date.atTime(23, 59, 59).toInstant(java.time.ZoneOffset.UTC));
                } catch (DateTimeParseException ex) {
                    log.warn("Failed to parse endTime: {}", endTime);
                }
            }
        }

        // Set filter values
        builder.accuracyMin(accuracyMin)
                .accuracyMax(accuracyMax)
                .speedMin(speedMin)
                .speedMax(speedMax);

        // Parse source types
        if (sourceTypes != null && !sourceTypes.trim().isEmpty()) {
            try {
                List<GpsSourceType> sourceTypeList = Arrays.stream(sourceTypes.split(","))
                        .map(String::trim)
                        .filter(s -> !s.isEmpty())
                        .map(GpsSourceType::valueOf)
                        .collect(Collectors.toList());
                builder.sourceTypes(sourceTypeList);
            } catch (IllegalArgumentException e) {
                log.warn("Invalid source type in: {}", sourceTypes, e);
            }
        }

        return builder.build();
    }

    @GET
    @Path("/status")
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response getGpsStatus() {
        UUID userId = currentUserService.getCurrentUserId();
        long startedAtNanos = workloadMetrics == null ? System.nanoTime() : workloadMetrics.start();
        String result = "success";
        log.info("Received request to get GPS status for user {}", userId);
        try {
            return Response.ok(ApiResponse.success(gpsPointService.getGpsStatus(userId))).build();
        } catch (Exception e) {
            result = "error";
            throw e;
        } finally {
            if (workloadMetrics != null) {
                workloadMetrics.recordTimer("geopulse.gps.ingest.duration", startedAtNanos,
                        "component", "gps",
                        "source", "STATUS",
                        "transport", "http",
                        "result", result);
            }
        }
    }

    /**
     * Update a GPS point.
     * This endpoint requires authentication.
     *
     * @param pointId The ID of the GPS point to update
     * @param editDto The update data
     * @return The updated GPS point
     */
    @PUT
    @Path("/{pointId}")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response updateGpsPoint(@PathParam("pointId") Long pointId, @Valid EditGpsPointDto editDto) {
        UUID userId = currentUserService.getCurrentUserId();
        log.info("Received request to update GPS point {} for user {}", pointId, userId);

        try {
            GpsPointDTO updatedPoint = gpsPointService.updateGpsPoint(pointId, editDto, userId);
            return Response.ok(ApiResponse.success(updatedPoint)).build();
        } catch (NotFoundException e) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(ApiResponse.error("GPS point not found"))
                    .build();
        } catch (ForbiddenException e) {
            return Response.status(Response.Status.FORBIDDEN)
                    .entity(ApiResponse.error("Access denied"))
                    .build();
        } catch (Exception e) {
            log.error("Failed to update GPS point {} for user {}", pointId, userId, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to update GPS point: " + e.getMessage()))
                    .build();
        }
    }

    /**
     * Delete ALL GPS points and timeline data for the authenticated user.
     * Uses efficient bulk SQL deletion - suitable for millions of records.
     * Also deletes timeline_stays, timeline_trips, and timeline_data_gaps.
     *
     * @return 200 OK if successful
     */
    @DELETE
    @Path("/all")
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response deleteAllGpsData() {
        UUID userId = currentUserService.getCurrentUserId();
        log.info("Received request to delete ALL GPS data for user {}", userId);

        try {
            gpsPointService.deleteAllGpsData(userId);
            return Response.ok(ApiResponse.success(Map.of("message", "All GPS data deleted successfully"))).build();
        } catch (Exception e) {
            log.error("Failed to delete all GPS data for user {}", userId, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to delete all GPS data: " + e.getMessage()))
                    .build();
        }
    }

    /**
     * Delete a GPS point.
     * This endpoint requires authentication.
     *
     * @param pointId The ID of the GPS point to delete
     * @return Delete result and background refresh scheduling status
     */
    @DELETE
    @Path("/{pointId}")
    @RolesAllowed({"USER", "ADMIN"})
    public Response deleteGpsPoint(@PathParam("pointId") Long pointId) {
        UUID userId = currentUserService.getCurrentUserId();
        log.info("Received request to delete GPS point {} for user {}", pointId, userId);

        try {
            GpsPointDeleteResult deleteResult = gpsPointService.deleteGpsPoint(pointId, userId);
            return Response.ok(ApiResponse.success(buildDeleteResponse(userId, deleteResult))).build();
        } catch (NotFoundException e) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(ApiResponse.error("GPS point not found"))
                    .build();
        } catch (ForbiddenException e) {
            return Response.status(Response.Status.FORBIDDEN)
                    .entity(ApiResponse.error("Access denied"))
                    .build();
        } catch (Exception e) {
            log.error("Failed to delete GPS point {} for user {}", pointId, userId, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to delete GPS point: " + e.getMessage()))
                    .build();
        }
    }

    /**
     * Delete multiple GPS points.
     * This endpoint requires authentication.
     *
     * @param bulkDeleteDto The bulk delete request containing GPS point IDs
     * @return Response with the number of deleted points
     */
    @POST
    @Path("/bulk")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response deleteGpsPoints(@Valid BulkDeleteGpsPointsDto bulkDeleteDto) {
        UUID userId = currentUserService.getCurrentUserId();
        log.info("Received request to delete {} GPS points for user {}",
                bulkDeleteDto.getGpsPointIds().size(), userId);

        try {
            GpsPointDeleteResult deleteResult = gpsPointService.deleteGpsPoints(bulkDeleteDto.getGpsPointIds(), userId);
            return Response.ok(ApiResponse.success(buildDeleteResponse(userId, deleteResult))).build();
        } catch (ForbiddenException e) {
            return Response.status(Response.Status.FORBIDDEN)
                    .entity(ApiResponse.error("Access denied"))
                    .build();
        } catch (Exception e) {
            log.error("Failed to delete GPS points for user {}", userId, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(ApiResponse.error("Failed to delete GPS points: " + e.getMessage()))
                    .build();
        }
    }

    private Map<String, Object> buildDeleteResponse(UUID userId, GpsPointDeleteResult deleteResult) {
        if (deleteResult.deletedCount() == 0) {
            Map<String, Object> response = new LinkedHashMap<>();
            response.put("deletedCount", 0);
            response.put("timelineJobId", null);
            response.put("timelineRegenerationScheduled", false);
            response.put("coverageRebuildScheduled", false);
            return response;
        }

        AsyncTimelineGenerationService.TimelineSchedulingResult timelineResult =
                asyncTimelineGenerationService.scheduleTimelineRegenerationFromTimestamp(
                        userId,
                        deleteResult.earliestAffectedTimestamp());

        boolean coverageRebuildScheduled = scheduleCoverageRebuildIfEnabled(userId);

        Map<String, Object> response = new LinkedHashMap<>();
        response.put("deletedCount", deleteResult.deletedCount());
        response.put("timelineJobId", timelineResult.jobId());
        response.put("timelineRegenerationScheduled", timelineResult.scheduled());
        response.put("coverageRebuildScheduled", coverageRebuildScheduled);
        return response;
    }

    private boolean scheduleCoverageRebuildIfEnabled(UUID userId) {
        try {
            CoverageStatus status = coverageService.getCoverageStatus(userId);
            if (!status.userEnabled()) {
                return false;
            }

            CoverageProcessingService.CoverageSchedulingResult result =
                    coverageProcessingService.requestFullRecalculationAsync(userId);
            return result.scheduled();
        } catch (Exception e) {
            log.warn("Failed to schedule coverage rebuild after GPS deletion for user {}: {}", userId, e.getMessage(), e);
            return false;
        }
    }

    @GET
    @Path("/last-known-position")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @RolesAllowed({"USER", "ADMIN"})
    public Response getLastKnownPosition() {
        UUID userId = currentUserService.getCurrentUserId();
        log.info("Received request to get last known position for user {}", userId);
        Optional<GpsPointDTO> lastPosition = gpsPointService.getLastKnownPosition(userId);
        return Response.ok(ApiResponse.success(lastPosition.orElse(null))).build();
    }
}
