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

import io.vertx.core.http.HttpServerRequest;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.github.tess1o.geopulse.admin.dto.BulkUpdateRequest;
import org.github.tess1o.geopulse.admin.dto.UpdateSettingRequest;
import org.github.tess1o.geopulse.admin.model.SettingInfo;
import org.github.tess1o.geopulse.admin.service.AuditLogService;
import org.github.tess1o.geopulse.admin.service.GeocodingValidationService;
import org.github.tess1o.geopulse.admin.service.SystemSettingsService;
import org.github.tess1o.geopulse.admin.service.WeatherValidationService;
import org.github.tess1o.geopulse.auth.security.SecurityRoles;
import org.github.tess1o.geopulse.auth.service.CurrentUserService;
import org.github.tess1o.geopulse.geofencing.client.AppriseClientResult;
import org.github.tess1o.geopulse.geofencing.model.dto.AppriseTestRequest;
import org.github.tess1o.geopulse.geofencing.service.AppriseNotificationService;
import org.github.tess1o.geopulse.mapmatching.service.MapMatchingConfiguration;
import org.github.tess1o.geopulse.mapmatching.service.MapMatchingWorker;
import org.github.tess1o.geopulse.shared.api.ApiResponse;
import org.github.tess1o.geopulse.shared.api.UserIpAddress;
import org.github.tess1o.geopulse.weather.dto.WeatherTestResponse;
import org.github.tess1o.geopulse.weather.service.WeatherService;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

/**
 * REST resource for admin settings management.
 */
@Path("/api/admin/settings")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Slf4j
@Tag(name = "Admin: System Settings", description = "View, update, reset, and test system-wide settings.")
public class AdminSettingsResource {

    @Context
    HttpServerRequest httpRequest;

    @Inject
    SystemSettingsService settingsService;

    @Inject
    AuditLogService auditLogService;

    @Inject
    CurrentUserService currentUserService;

    @Inject
    GeocodingValidationService geocodingValidationService;

    @Inject
    WeatherValidationService weatherValidationService;

    @Inject
    AppriseNotificationService appriseNotificationService;

    @Inject
    WeatherService weatherService;

    @Inject
    MapMatchingConfiguration mapMatchingConfiguration;

    @Inject
    MapMatchingWorker mapMatchingWorker;

    /**
     * Get all settings grouped by category.
     */
    @GET
    @RolesAllowed({SecurityRoles.ADMIN, SecurityRoles.DEMO_ADMIN_READ})
    public Response getAllSettings() {
        Map<String, List<SettingInfo>> settings = settingsService.getAllSettings();
        return Response.ok(settings).build();
    }

    /**
     * Get settings for a specific category.
     */
    @GET
    @Path("/{category}")
    @RolesAllowed({SecurityRoles.ADMIN, SecurityRoles.DEMO_ADMIN_READ})
    public Response getSettingsByCategory(@PathParam("category") String category) {
        List<SettingInfo> settings = settingsService.getSettingsByCategory(category);
        return Response.ok(settings).build();
    }

    /**
     * Update a setting value.
     * <p>
     * Note: Geocoding settings should use the bulk update endpoint to ensure
     * proper validation of interdependent settings. This endpoint is primarily
     * used for other categories (auth, ai, import, export).
     */
    @PUT
    @Path("/{key}")
    @RolesAllowed(SecurityRoles.ADMIN)
    public Response updateSetting(
            @PathParam("key") String key,
            UpdateSettingRequest request,
            @HeaderParam("X-Forwarded-For") String forwardedFor,
            @HeaderParam("X-Real-IP") String realIp) {

        UUID adminId = currentUserService.getCurrentUserId();
        String oldValue = settingsService.getString(key);

        settingsService.setValue(key, request.getValue(), adminId);

        // Audit log
        String ipAddress = UserIpAddress.resolve(httpRequest, forwardedFor, realIp);
        auditLogService.logSettingChange(adminId, key, oldValue, request.getValue(), ipAddress);

        return Response.ok(Map.of("success", true)).build();
    }

    /**
     * Reset a setting to default (delete from DB).
     */
    @DELETE
    @Path("/{key}")
    @RolesAllowed(SecurityRoles.ADMIN)
    public Response resetSetting(
            @PathParam("key") String key,
            @HeaderParam("X-Forwarded-For") String forwardedFor,
            @HeaderParam("X-Real-IP") String realIp) {

        UUID adminId = currentUserService.getCurrentUserId();
        String oldValue = settingsService.getString(key);

        settingsService.resetToDefault(key);

        // Audit log
        String ipAddress = UserIpAddress.resolve(httpRequest, forwardedFor, realIp);
        auditLogService.logSettingReset(adminId, key, oldValue, ipAddress);

        return Response.ok(Map.of("success", true, "defaultValue", settingsService.getDefaultValue(key))).build();
    }

    /**
     * Bulk update multiple settings atomically.
     * All settings are validated before any changes are made.
     * If any validation fails, no settings are updated (transaction rollback).
     * <p>
     * This endpoint is particularly important for geocoding settings, which have
     * interdependencies (e.g., enabling a provider requires both a flag and credentials).
     * Context-aware validation ensures all pending changes are considered together.
     */
    @POST
    @Path("/bulk")
    @Transactional
    @RolesAllowed(SecurityRoles.ADMIN)
    public Response bulkUpdateSettings(
            BulkUpdateRequest request,
            @HeaderParam("X-Forwarded-For") String forwardedFor,
            @HeaderParam("X-Real-IP") String realIp) {

        UUID adminId = currentUserService.getCurrentUserId();
        String ipAddress = UserIpAddress.resolve(httpRequest, forwardedFor, realIp);

        // 1. Validate ALL provider settings together with full context
        List<UpdateSettingRequest> geocodingSettings = request.getSettings().stream()
                .filter(s -> s.getKey().startsWith("geocoding."))
                .collect(Collectors.toList());

        if (!geocodingSettings.isEmpty()) {
            String validationError = geocodingValidationService.validateGeocodingChanges(geocodingSettings);
            if (validationError != null) {
                return Response.status(Response.Status.BAD_REQUEST)
                        .entity(Map.of("message", validationError))
                        .build();
            }
        }

        List<UpdateSettingRequest> weatherSettings = request.getSettings().stream()
                .filter(s -> s.getKey().startsWith("weather."))
                .collect(Collectors.toList());

        if (!weatherSettings.isEmpty()) {
            String validationError = weatherValidationService.validateWeatherChanges(weatherSettings);
            if (validationError != null) {
                return Response.status(Response.Status.BAD_REQUEST)
                        .entity(Map.of("message", validationError))
                        .build();
            }
        }

        // 2. Group settings by save order (same order as before for validation)
        List<UpdateSettingRequest> credentialSettings = request.getSettings().stream()
            .filter(s -> s.getKey().contains(".api-key") || s.getKey().contains(".access-token"))
            .collect(Collectors.toList());

        List<UpdateSettingRequest> enabledSettings = request.getSettings().stream()
            .filter(s -> s.getKey().contains(".enabled"))
            .collect(Collectors.toList());

        List<UpdateSettingRequest> providerSettings = request.getSettings().stream()
            .filter(s -> s.getKey().equals("geocoding.primary-provider") ||
                         s.getKey().equals("geocoding.fallback-provider") ||
                         s.getKey().equals("weather.primary-provider") ||
                         s.getKey().equals("weather.secondary-provider"))
            .collect(Collectors.toList());

        List<UpdateSettingRequest> otherSettings = request.getSettings().stream()
            .filter(s -> !s.getKey().contains(".api-key") &&
                         !s.getKey().contains(".access-token") &&
                         !s.getKey().contains(".enabled") &&
                         !s.getKey().equals("geocoding.primary-provider") &&
                         !s.getKey().equals("geocoding.fallback-provider") &&
                         !s.getKey().equals("weather.primary-provider") &&
                         !s.getKey().equals("weather.secondary-provider"))
            .collect(Collectors.toList());

        // 3. Save in correct order (within transaction)
        List<UpdateSettingRequest> orderedSettings = new ArrayList<>();
        orderedSettings.addAll(credentialSettings);
        orderedSettings.addAll(enabledSettings);
        orderedSettings.addAll(providerSettings);
        orderedSettings.addAll(otherSettings);

        for (UpdateSettingRequest settingUpdate : orderedSettings) {
            String oldValue = settingsService.getString(settingUpdate.getKey());
            settingsService.setValue(settingUpdate.getKey(), settingUpdate.getValue(), adminId);

            // Audit log each change
            auditLogService.logSettingChange(
                adminId,
                settingUpdate.getKey(),
                oldValue,
                settingUpdate.getValue(),
                ipAddress
            );
        }

        // 4. If we reach here, all saves succeeded (transaction commits)
        return Response.ok(Map.of("success", true, "updated", orderedSettings.size())).build();
    }

    /**
     * Test Apprise connectivity using current system settings.
     */
    @POST
    @Path("/system/notifications/apprise/test")
    @RolesAllowed(SecurityRoles.ADMIN)
    public Response testAppriseConnection(AppriseTestRequest request) {
        AppriseClientResult result = appriseNotificationService.testConnection(request);
        if (result == null) {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(Map.of(
                            "success", false,
                            "statusCode", 0,
                            "message", "Apprise test failed: no response from client"
                    ))
                    .build();
        }

        String message = result.getMessage() != null && !result.getMessage().isBlank()
                ? result.getMessage()
                : (result.isSuccess() ? "Apprise endpoint is reachable" : "Apprise test failed");

        Map<String, Object> responsePayload = new LinkedHashMap<>();
        responsePayload.put("success", result.isSuccess());
        responsePayload.put("statusCode", result.getStatusCode());
        responsePayload.put("message", message);

        if (result.isSuccess()) {
            return Response.ok(responsePayload).build();
        }

        return Response.status(Response.Status.BAD_REQUEST)
                .entity(responsePayload)
                .build();
    }

    @POST
    @Path("/weather/test")
    @RolesAllowed(SecurityRoles.ADMIN)
    public Response testWeatherConnection() {
        WeatherTestResponse result = weatherService.testProviderConnection();
        Map<String, Object> responsePayload = new LinkedHashMap<>();
        responsePayload.put("success", result.isSuccess());
        responsePayload.put("statusCode", result.getStatusCode());
        responsePayload.put("message", result.getMessage());
        responsePayload.put("provider", result.getProvider());
        responsePayload.put("url", result.getUrl());
        responsePayload.put("forecast", result.getForecast());
        responsePayload.put("archive", result.getArchive());

        if (result.isSuccess()) {
            return Response.ok(responsePayload).build();
        }

        return Response.status(Response.Status.BAD_REQUEST)
                .entity(responsePayload)
                .build();
    }

    @POST
    @Path("/map-matching/valhalla/test")
    @RolesAllowed(SecurityRoles.ADMIN)
    public Response testValhallaConnection() {
        Map<String, Object> responsePayload = new LinkedHashMap<>();
        responsePayload.put("provider", "valhalla");

        if (!mapMatchingConfiguration.valhallaConfigured()) {
            responsePayload.put("success", false);
            responsePayload.put("statusCode", 0);
            responsePayload.put("message", "Valhalla base URL is not configured");
            return Response.status(Response.Status.BAD_REQUEST).entity(responsePayload).build();
        }

        String baseUrl = mapMatchingConfiguration.valhallaBaseUrl();
        URI statusUri = URI.create(baseUrl.endsWith("/") ? baseUrl + "status" : baseUrl + "/status");
        responsePayload.put("url", statusUri.toString());

        try (HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(Math.max(1, mapMatchingConfiguration.getConnectTimeoutSeconds())))
                .build()) {
            HttpRequest request = HttpRequest.newBuilder(statusUri)
                    .timeout(Duration.ofSeconds(Math.max(1, mapMatchingConfiguration.getReadTimeoutSeconds())))
                    .GET()
                    .build();
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            responsePayload.put("statusCode", response.statusCode());
            responsePayload.put("message", response.statusCode() >= 200 && response.statusCode() < 300
                    ? "Valhalla endpoint is reachable"
                    : limitErrorBody(response.body()));
            responsePayload.put("success", response.statusCode() >= 200 && response.statusCode() < 300);

            if (response.statusCode() >= 200 && response.statusCode() < 300) {
                return Response.ok(responsePayload).build();
            }
            return Response.status(Response.Status.BAD_REQUEST).entity(responsePayload).build();
        } catch (Exception e) {
            responsePayload.put("success", false);
            responsePayload.put("statusCode", 0);
            responsePayload.put("message", e.getMessage() == null ? "Valhalla connection failed" : e.getMessage());
            return Response.status(Response.Status.BAD_REQUEST).entity(responsePayload).build();
        }
    }

    @GET
    @Path("/map-matching/status")
    @RolesAllowed({SecurityRoles.ADMIN, SecurityRoles.DEMO_ADMIN_READ})
    public Response mapMatchingStatus() {
        return Response.ok(ApiResponse.success(mapMatchingWorker.status())).build();
    }

    private String limitErrorBody(String body) {
        if (body == null || body.isBlank()) {
            return "Valhalla endpoint returned an error";
        }
        String trimmed = body.trim();
        return trimmed.length() <= 500 ? trimmed : trimmed.substring(0, 500);
    }
}
