package com.cameleer.server.app.controller;

import com.cameleer.common.model.ApplicationConfig;
import com.cameleer.server.app.dto.CommandGroupResponse;
import com.cameleer.server.app.dto.SensitiveKeysRequest;
import com.cameleer.server.app.dto.SensitiveKeysResponse;
import com.cameleer.server.app.storage.PostgresApplicationConfigRepository;
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.AgentRegistryService;
import com.cameleer.server.core.agent.CommandReply;
import com.cameleer.server.core.agent.CommandType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

@RestController
@RequestMapping("/api/v1/admin/sensitive-keys")
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "Sensitive Keys Admin", description = "Global sensitive key masking configuration (ADMIN only)")
public class SensitiveKeysAdminController {

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

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

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

    @GetMapping
    @Operation(summary = "Get global sensitive keys configuration")
    public ResponseEntity<SensitiveKeysConfig> getSensitiveKeys(HttpServletRequest httpRequest) {
        auditService.log("view_sensitive_keys", AuditCategory.CONFIG, "sensitive_keys",
                null, AuditResult.SUCCESS, httpRequest);
        return sensitiveKeysRepository.find()
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.noContent().build());
    }

    @PutMapping
    @Operation(summary = "Update global sensitive keys configuration",
            description = "Saves the global sensitive keys. Optionally fans out merged keys to all live agents.")
    public ResponseEntity<SensitiveKeysResponse> updateSensitiveKeys(
            @Valid @RequestBody SensitiveKeysRequest request,
            @RequestParam(required = false, defaultValue = "false") boolean pushToAgents,
            Authentication auth,
            HttpServletRequest httpRequest) {

        String updatedBy = auth != null ? auth.getName() : "system";
        SensitiveKeysConfig config = new SensitiveKeysConfig(request.keys());
        sensitiveKeysRepository.save(config, updatedBy);

        CommandGroupResponse pushResult = null;
        if (pushToAgents) {
            pushResult = fanOutToAllAgents(config.keys());
            log.info("Sensitive keys saved and pushed to all applications, {} agent(s) responded",
                    pushResult.responded());
        } else {
            log.info("Sensitive keys saved ({} keys), push skipped", config.keys().size());
        }

        auditService.log("update_sensitive_keys", AuditCategory.CONFIG, "sensitive_keys",
                Map.of("keyCount", config.keys().size(), "pushToAgents", pushToAgents),
                AuditResult.SUCCESS, httpRequest);

        return ResponseEntity.ok(new SensitiveKeysResponse(config.keys(), pushResult));
    }

    /**
     * Fan out the merged (global + per-app) sensitive keys to all known applications.
     * Collects distinct application IDs from both stored configs and live agents.
     * Builds a minimal JSON payload carrying only the sensitiveKeys field so that
     * agents apply it as a partial config update.
     * <p>
     * Per-app keys are read via JsonNode rather than ApplicationConfig.getSensitiveKeys()
     * to remain compatible with older published versions of cameleer-common that may
     * not yet include that field accessor.
     */
    private CommandGroupResponse fanOutToAllAgents(List<String> globalKeys) {
        // Collect every (application, environment) slice we know about: persisted config rows
        // PLUS currently-registered live agents (which may have no stored config yet).
        // Global sensitive keys are server-wide, but per-app overrides live per env, so the
        // push is scoped per (app, env) so each slice gets its own merged keys.
        Set<AppEnv> slices = new LinkedHashSet<>();
        for (ApplicationConfig cfg : configRepository.findAll()) {
            if (cfg.getApplication() != null && !cfg.getApplication().isBlank()
                    && cfg.getEnvironment() != null && !cfg.getEnvironment().isBlank()) {
                slices.add(new AppEnv(cfg.getApplication(), cfg.getEnvironment()));
            }
        }
        registryService.findAll().stream()
                .filter(a -> a.applicationId() != null && !a.applicationId().isBlank()
                        && a.environmentId() != null && !a.environmentId().isBlank())
                .forEach(a -> slices.add(new AppEnv(a.applicationId(), a.environmentId())));

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

        // Shared 10-second deadline across all slices
        long deadline = System.currentTimeMillis() + 10_000;
        List<CommandGroupResponse.AgentResponse> allResponses = new ArrayList<>();
        List<String> allTimedOut = new ArrayList<>();
        int totalAgents = 0;

        for (AppEnv slice : slices) {
            // Load per-(app,env) sensitive keys via JsonNode to avoid dependency on
            // ApplicationConfig.getSensitiveKeys() which may not be in the published jar yet.
            List<String> perAppKeys = configRepository
                    .findByApplicationAndEnvironment(slice.application(), slice.environment())
                    .map(cfg -> extractSensitiveKeys(cfg))
                    .orElse(null);

            // Merge global + per-app keys
            List<String> mergedKeys = SensitiveKeysMerger.merge(globalKeys, perAppKeys);

            // Build a minimal payload map — only sensitiveKeys + application fields.
            Map<String, Object> payloadMap = new LinkedHashMap<>();
            payloadMap.put("application", slice.application());
            payloadMap.put("sensitiveKeys", mergedKeys);

            String payloadJson;
            try {
                payloadJson = objectMapper.writeValueAsString(payloadMap);
            } catch (JsonProcessingException e) {
                log.error("Failed to serialize sensitive keys push payload for {}/{}",
                        slice.application(), slice.environment(), e);
                continue;
            }

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

            totalAgents += futures.size();

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

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

    /**
     * Extracts the sensitiveKeys list from an ApplicationConfig by round-tripping through
     * JsonNode. This avoids a compile-time dependency on ApplicationConfig.getSensitiveKeys()
     * which may not be present in older published versions of cameleer-common.
     */
    private List<String> extractSensitiveKeys(ApplicationConfig config) {
        try {
            JsonNode node = objectMapper.valueToTree(config);
            JsonNode keysNode = node.get("sensitiveKeys");
            if (keysNode == null || keysNode.isNull() || !keysNode.isArray()) {
                return null;
            }
            return objectMapper.convertValue(keysNode, new TypeReference<List<String>>() {});
        } catch (Exception e) {
            log.warn("Failed to extract sensitiveKeys from ApplicationConfig", e);
            return null;
        }
    }

    /** (application, environment) slice used by the fan-out loop. */
    private record AppEnv(String application, String environment) {}
}
