package com.cameleer.server.app.controller;

import com.cameleer.common.model.ApplicationConfig;
import com.cameleer.server.app.dto.AppConfigResponse;
import com.cameleer.server.app.dto.CommandGroupResponse;
import com.cameleer.server.app.dto.ConfigUpdateResponse;
import com.cameleer.server.app.dto.TestExpressionRequest;
import com.cameleer.server.app.dto.TestExpressionResponse;
import com.cameleer.server.app.storage.PostgresApplicationConfigRepository;
import com.cameleer.server.app.web.EnvPath;
import com.cameleer.server.core.admin.AuditCategory;
import com.cameleer.server.core.admin.AuditResult;
import com.cameleer.server.core.admin.AuditService;
import com.cameleer.server.core.admin.SensitiveKeysConfig;
import com.cameleer.server.core.admin.SensitiveKeysMerger;
import com.cameleer.server.core.admin.SensitiveKeysRepository;
import com.cameleer.server.core.agent.AgentInfo;
import com.cameleer.server.core.agent.AgentRegistryService;
import com.cameleer.server.core.agent.AgentState;
import com.cameleer.server.core.agent.CommandReply;
import com.cameleer.server.core.agent.CommandType;
import com.cameleer.server.core.runtime.Environment;
import com.cameleer.server.core.storage.DiagramStore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Per-application configuration for UI/admin callers. Env comes from the path,
 * app comes from the path. Agents use {@link AgentConfigController} instead —
 * env is derived from the JWT there, not spoofable via URL.
 */
@RestController
@RequestMapping("/api/v1/environments/{envSlug}")
@Tag(name = "Application Config", description = "Per-application observability configuration (user-facing)")
public class ApplicationConfigController {

    private static final Logger log = LoggerFactory.getLogger(ApplicationConfigController.class);

    private final PostgresApplicationConfigRepository configRepository;
    private final AgentRegistryService registryService;
    private final ObjectMapper objectMapper;
    private final AuditService auditService;
    private final DiagramStore diagramStore;
    private final SensitiveKeysRepository sensitiveKeysRepository;

    public ApplicationConfigController(PostgresApplicationConfigRepository configRepository,
                                       AgentRegistryService registryService,
                                       ObjectMapper objectMapper,
                                       AuditService auditService,
                                       DiagramStore diagramStore,
                                       SensitiveKeysRepository sensitiveKeysRepository) {
        this.configRepository = configRepository;
        this.registryService = registryService;
        this.objectMapper = objectMapper;
        this.auditService = auditService;
        this.diagramStore = diagramStore;
        this.sensitiveKeysRepository = sensitiveKeysRepository;
    }

    @GetMapping("/config")
    @Operation(summary = "List application configs in this environment")
    @ApiResponse(responseCode = "200", description = "Configs returned")
    public ResponseEntity<List<ApplicationConfig>> listConfigs(@EnvPath Environment env,
                                                                 HttpServletRequest httpRequest) {
        auditService.log("view_app_configs", AuditCategory.CONFIG, null,
                Map.of("environment", env.slug()), AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.ok(configRepository.findByEnvironment(env.slug()));
    }

    @GetMapping("/apps/{appSlug}/config")
    @Operation(summary = "Get application config for this environment",
            description = "Returns stored config merged with global sensitive keys. "
                    + "Falls back to defaults if no row is persisted yet.")
    @ApiResponse(responseCode = "200", description = "Config returned")
    public ResponseEntity<AppConfigResponse> getConfig(@EnvPath Environment env,
                                                        @PathVariable String appSlug,
                                                        HttpServletRequest httpRequest) {
        auditService.log("view_app_config", AuditCategory.CONFIG, appSlug,
                Map.of("environment", env.slug()), AuditResult.SUCCESS, httpRequest);
        ApplicationConfig config = configRepository.findByApplicationAndEnvironment(appSlug, env.slug())
                .orElse(defaultConfig(appSlug, env.slug()));

        List<String> globalKeys = sensitiveKeysRepository.find()
                .map(SensitiveKeysConfig::keys)
                .orElse(null);
        List<String> merged = SensitiveKeysMerger.merge(globalKeys, extractSensitiveKeys(config));

        return ResponseEntity.ok(new AppConfigResponse(config, globalKeys, merged));
    }

    @PutMapping("/apps/{appSlug}/config")
    @Operation(summary = "Update application config for this environment",
            description = "Saves config. When apply=live (default), also pushes CONFIG_UPDATE to LIVE agents. "
                    + "When apply=staged, persists without a live push — the next successful deploy applies it.")
    @ApiResponse(responseCode = "200", description = "Config saved (and pushed if apply=live)")
    @ApiResponse(responseCode = "400", description = "Unknown apply value (must be 'staged' or 'live')")
    public ResponseEntity<ConfigUpdateResponse> updateConfig(@EnvPath Environment env,
                                                              @PathVariable String appSlug,
                                                              @Parameter(name = "apply",
                                                                      description = "When to apply: 'live' (default) saves and pushes CONFIG_UPDATE to live agents immediately; 'staged' saves without pushing — the next successful deploy applies it.")
                                                              @RequestParam(name = "apply", defaultValue = "live") String apply,
                                                              @RequestBody ApplicationConfig config,
                                                              Authentication auth,
                                                              HttpServletRequest httpRequest) {
        if (!"staged".equalsIgnoreCase(apply) && !"live".equalsIgnoreCase(apply)) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "Unknown apply value '" + apply + "' — must be 'staged' or 'live'");
        }

        String updatedBy = auth != null ? auth.getName() : "system";

        config.setApplication(appSlug);
        ApplicationConfig saved = configRepository.save(appSlug, env.slug(), config, updatedBy);

        List<String> globalKeys = sensitiveKeysRepository.find()
                .map(SensitiveKeysConfig::keys)
                .orElse(null);
        List<String> perAppKeys = extractSensitiveKeys(saved);
        List<String> mergedKeys = SensitiveKeysMerger.merge(globalKeys, perAppKeys);

        CommandGroupResponse pushResult;
        if ("staged".equalsIgnoreCase(apply)) {
            pushResult = new CommandGroupResponse(true, 0, 0, List.of(), List.of());
            log.info("Config v{} staged for '{}' (no live push)", saved.getVersion(), appSlug);
        } else {
            pushResult = pushConfigToAgentsWithMergedKeys(appSlug, env.slug(), saved, mergedKeys);
            log.info("Config v{} saved for '{}', pushed to {} agent(s), {} responded",
                    saved.getVersion(), appSlug, pushResult.total(), pushResult.responded());
        }

        auditService.log(
                "staged".equalsIgnoreCase(apply) ? "stage_app_config" : "update_app_config",
                AuditCategory.CONFIG, appSlug,
                Map.of("environment", env.slug(), "version", saved.getVersion(),
                        "apply", apply.toLowerCase(),
                        "agentsPushed", pushResult.total(),
                        "responded", pushResult.responded(),
                        "timedOut", pushResult.timedOut().size()),
                AuditResult.SUCCESS, httpRequest);

        return ResponseEntity.ok(new ConfigUpdateResponse(saved, pushResult));
    }

    @GetMapping("/apps/{appSlug}/processor-routes")
    @Operation(summary = "Get processor to route mapping for this environment",
            description = "Returns a map of processorId → routeId for all processors seen in this application + environment")
    @ApiResponse(responseCode = "200", description = "Mapping returned")
    public ResponseEntity<Map<String, String>> getProcessorRouteMapping(@EnvPath Environment env,
                                                                          @PathVariable String appSlug) {
        return ResponseEntity.ok(diagramStore.findProcessorRouteMapping(appSlug, env.slug()));
    }

    @PostMapping("/apps/{appSlug}/config/test-expression")
    @Operation(summary = "Test a tap expression against sample data via a live agent in this environment")
    @ApiResponse(responseCode = "200", description = "Expression evaluated successfully")
    @ApiResponse(responseCode = "404", description = "No live agent available for this application in this environment")
    @ApiResponse(responseCode = "504", description = "Agent did not respond in time")
    public ResponseEntity<TestExpressionResponse> testExpression(
            @EnvPath Environment env,
            @PathVariable String appSlug,
            @RequestBody TestExpressionRequest request) {
        AgentInfo agent = registryService.findByApplicationAndEnvironment(appSlug, env.slug()).stream()
                .filter(a -> a.state() == AgentState.LIVE)
                .findFirst()
                .orElse(null);

        if (agent == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .body(new TestExpressionResponse(null, "No live agent available for application: " + appSlug));
        }

        String payloadJson;
        try {
            payloadJson = objectMapper.writeValueAsString(Map.of(
                    "expression", request.expression() != null ? request.expression() : "",
                    "language", request.language() != null ? request.language() : "",
                    "body", request.body() != null ? request.body() : "",
                    "target", request.target() != null ? request.target() : ""
            ));
        } catch (JsonProcessingException e) {
            log.error("Failed to serialize test-expression payload", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(new TestExpressionResponse(null, "Failed to serialize request"));
        }

        CompletableFuture<CommandReply> future = registryService.addCommandWithReply(
                agent.instanceId(), CommandType.TEST_EXPRESSION, payloadJson);

        try {
            CommandReply reply = future.orTimeout(5, TimeUnit.SECONDS).join();
            if ("SUCCESS".equals(reply.status())) {
                return ResponseEntity.ok(new TestExpressionResponse(reply.data(), null));
            } else {
                return ResponseEntity.ok(new TestExpressionResponse(null, reply.message()));
            }
        } catch (CompletionException e) {
            if (e.getCause() instanceof TimeoutException) {
                return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
                        .body(new TestExpressionResponse(null, "Agent did not respond within 5 seconds"));
            }
            log.error("Error awaiting test-expression reply from agent {}", agent.instanceId(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(new TestExpressionResponse(null, "Internal error: " + e.getCause().getMessage()));
        }
    }

    private List<String> extractSensitiveKeys(ApplicationConfig config) {
        try {
            com.fasterxml.jackson.databind.JsonNode node = objectMapper.valueToTree(config);
            com.fasterxml.jackson.databind.JsonNode keysNode = node.get("sensitiveKeys");
            if (keysNode == null || keysNode.isNull() || !keysNode.isArray()) {
                return null;
            }
            return objectMapper.convertValue(keysNode, new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {});
        } catch (Exception e) {
            return null;
        }
    }

    private CommandGroupResponse pushConfigToAgentsWithMergedKeys(String application, String environment,
                                                                   ApplicationConfig config, List<String> mergedKeys) {
        String payloadJson;
        try {
            @SuppressWarnings("unchecked")
            Map<String, Object> configMap = objectMapper.convertValue(config, Map.class);
            configMap.put("sensitiveKeys", mergedKeys);
            payloadJson = objectMapper.writeValueAsString(configMap);
        } catch (Exception e) {
            log.error("Failed to serialize config with merged keys for push", e);
            return new CommandGroupResponse(false, 0, 0, List.of(), List.of());
        }

        Map<String, CompletableFuture<CommandReply>> futures =
                registryService.addGroupCommandWithReplies(application, environment, CommandType.CONFIG_UPDATE, payloadJson);

        if (futures.isEmpty()) {
            return new CommandGroupResponse(true, 0, 0, List.of(), List.of());
        }

        long deadline = System.currentTimeMillis() + 10_000;
        List<CommandGroupResponse.AgentResponse> responses = new ArrayList<>();
        List<String> timedOut = new ArrayList<>();

        for (var entry : futures.entrySet()) {
            long remaining = deadline - System.currentTimeMillis();
            if (remaining <= 0) {
                timedOut.add(entry.getKey());
                entry.getValue().cancel(false);
                continue;
            }
            try {
                CommandReply reply = entry.getValue().get(remaining, TimeUnit.MILLISECONDS);
                responses.add(new CommandGroupResponse.AgentResponse(
                        entry.getKey(), reply.status(), reply.message()));
            } catch (TimeoutException e) {
                timedOut.add(entry.getKey());
                entry.getValue().cancel(false);
            } catch (Exception e) {
                responses.add(new CommandGroupResponse.AgentResponse(
                        entry.getKey(), "ERROR", e.getMessage()));
            }
        }

        boolean allSuccess = timedOut.isEmpty() &&
                responses.stream().allMatch(r -> "SUCCESS".equals(r.status()));
        return new CommandGroupResponse(allSuccess, futures.size(), responses.size(), responses, timedOut);
    }

    static ApplicationConfig defaultConfig(String application, String environment) {
        ApplicationConfig config = new ApplicationConfig();
        config.setApplication(application);
        config.setEnvironment(environment);
        config.setVersion(0);
        config.setMetricsEnabled(true);
        config.setSamplingRate(1.0);
        config.setTracedProcessors(Map.of());
        config.setApplicationLogLevel("INFO");
        config.setAgentLogLevel("INFO");
        config.setEngineLevel("REGULAR");
        config.setPayloadCaptureMode("BOTH");
        return config;
    }
}
