package com.cameleer.server.app.controller;

import com.cameleer.server.app.agent.SseConnectionManager;
import com.cameleer.server.app.dto.CommandAckRequest;
import com.cameleer.server.app.dto.CommandBroadcastResponse;
import com.cameleer.server.app.dto.CommandGroupResponse;
import com.cameleer.server.app.dto.CommandRequest;
import com.cameleer.server.app.dto.CommandSingleResponse;
import com.cameleer.server.app.dto.ReplayRequest;
import com.cameleer.server.app.dto.ReplayResponse;
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.agent.AgentCommand;
import com.cameleer.server.core.agent.AgentEventService;
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.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
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 org.springframework.web.server.ResponseStatusException;

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

/**
 * Command push endpoints for sending commands to agents via SSE.
 * <p>
 * Supports three targeting levels:
 * <ul>
 *   <li>Single agent: POST /api/v1/agents/{id}/commands</li>
 *   <li>Group: POST /api/v1/agents/groups/{group}/commands</li>
 *   <li>Broadcast: POST /api/v1/agents/commands</li>
 * </ul>
 */
@RestController
@RequestMapping("/api/v1/agents")
@Tag(name = "Agent Commands", description = "Command push endpoints for agent communication")
public class AgentCommandController {

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

    private final AgentRegistryService registryService;
    private final SseConnectionManager connectionManager;
    private final ObjectMapper objectMapper;
    private final AgentEventService agentEventService;
    private final AuditService auditService;

    public AgentCommandController(AgentRegistryService registryService,
                                   SseConnectionManager connectionManager,
                                   ObjectMapper objectMapper,
                                   AgentEventService agentEventService,
                                   AuditService auditService) {
        this.registryService = registryService;
        this.connectionManager = connectionManager;
        this.objectMapper = objectMapper;
        this.agentEventService = agentEventService;
        this.auditService = auditService;
    }

    @PostMapping("/{id}/commands")
    @Operation(summary = "Send command to a specific agent",
            description = "Sends a command to the specified agent via SSE")
    @ApiResponse(responseCode = "202", description = "Command accepted")
    @ApiResponse(responseCode = "400", description = "Invalid command payload")
    @ApiResponse(responseCode = "404", description = "Agent not registered")
    public ResponseEntity<CommandSingleResponse> sendCommand(@PathVariable String id,
                                                              @RequestBody CommandRequest request,
                                                              HttpServletRequest httpRequest) throws JsonProcessingException {
        AgentInfo agent = registryService.findById(id);
        if (agent == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Agent not found: " + id);
        }

        CommandType type = mapCommandType(request.type());
        String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
        AgentCommand command = registryService.addCommand(id, type, payloadJson);

        String status = connectionManager.isConnected(id) ? "DELIVERED" : "PENDING";

        auditService.log("send_agent_command", AuditCategory.AGENT, id,
                java.util.Map.of("type", request.type(), "status", status),
                AuditResult.SUCCESS, httpRequest);

        return ResponseEntity.status(HttpStatus.ACCEPTED)
                .body(new CommandSingleResponse(command.id(), status));
    }

    @PostMapping("/groups/{group}/commands")
    @Operation(summary = "Send command to all agents in a group",
            description = "Sends a command to all LIVE agents in the specified group and waits for responses")
    @ApiResponse(responseCode = "200", description = "Commands dispatched and responses collected")
    @ApiResponse(responseCode = "400", description = "Invalid command payload")
    public ResponseEntity<CommandGroupResponse> sendGroupCommand(@PathVariable String group,
                                                                  @RequestParam(required = false) String environment,
                                                                  @RequestBody CommandRequest request,
                                                                  HttpServletRequest httpRequest) throws JsonProcessingException {
        CommandType type = mapCommandType(request.type());
        String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";

        Map<String, CompletableFuture<CommandReply>> futures =
                registryService.addGroupCommandWithReplies(group, environment, type, payloadJson);

        if (futures.isEmpty()) {
            auditService.log("broadcast_group_command", AuditCategory.AGENT, group,
                    java.util.Map.of("type", request.type(), "agentCount", 0),
                    AuditResult.SUCCESS, httpRequest);
            return ResponseEntity.ok(new CommandGroupResponse(true, 0, 0, List.of(), List.of()));
        }

        // Wait with shared 10-second deadline
        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()));

        auditService.log("broadcast_group_command", AuditCategory.AGENT, group,
                java.util.Map.of("type", request.type(), "agentCount", futures.size(),
                        "responded", responses.size(), "timedOut", timedOut.size()),
                AuditResult.SUCCESS, httpRequest);

        return ResponseEntity.ok(new CommandGroupResponse(
                allSuccess, futures.size(), responses.size(), responses, timedOut));
    }

    @PostMapping("/commands")
    @Operation(summary = "Broadcast command to all live agents",
            description = "Sends a command to all agents currently in LIVE state")
    @ApiResponse(responseCode = "202", description = "Commands accepted")
    @ApiResponse(responseCode = "400", description = "Invalid command payload")
    public ResponseEntity<CommandBroadcastResponse> broadcastCommand(@RequestParam(required = false) String environment,
                                                                       @RequestBody CommandRequest request,
                                                                       HttpServletRequest httpRequest) throws JsonProcessingException {
        CommandType type = mapCommandType(request.type());
        String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";

        List<AgentInfo> liveAgents = registryService.findByState(AgentState.LIVE);
        if (environment != null) {
            liveAgents = liveAgents.stream()
                    .filter(a -> environment.equals(a.environmentId()))
                    .toList();
        }

        List<String> commandIds = new ArrayList<>();
        for (AgentInfo agent : liveAgents) {
            AgentCommand command = registryService.addCommand(agent.instanceId(), type, payloadJson);
            commandIds.add(command.id());
        }

        auditService.log("broadcast_all_command", AuditCategory.AGENT, null,
                java.util.Map.of("type", request.type(), "agentCount", liveAgents.size()),
                AuditResult.SUCCESS, httpRequest);

        return ResponseEntity.status(HttpStatus.ACCEPTED)
                .body(new CommandBroadcastResponse(commandIds, liveAgents.size()));
    }

    @PostMapping("/{id}/commands/{commandId}/ack")
    @Operation(summary = "Acknowledge command receipt",
            description = "Agent acknowledges that it has received and processed a command, with result status and message")
    @ApiResponse(responseCode = "200", description = "Command acknowledged")
    @ApiResponse(responseCode = "404", description = "Command not found")
    public ResponseEntity<Void> acknowledgeCommand(@PathVariable String id,
                                                    @PathVariable String commandId,
                                                    @RequestBody(required = false) CommandAckRequest body) {
        boolean acknowledged = registryService.acknowledgeCommand(id, commandId);
        if (!acknowledged) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Command not found: " + commandId);
        }

        // Complete any pending reply future (for synchronous request-reply commands like TEST_EXPRESSION)
        registryService.completeReply(commandId,
                body != null ? body.status() : "SUCCESS",
                body != null ? body.message() : null,
                body != null ? body.data() : null);

        // Record command result in agent event log
        if (body != null && body.status() != null) {
            AgentInfo agent = registryService.findById(id);
            String application = agent != null ? agent.applicationId() : "unknown";
            String environment = agent != null ? agent.environmentId() : null;
            agentEventService.recordEvent(id, application, environment, "COMMAND_" + body.status(),
                    "Command " + commandId + ": " + body.message());
            log.debug("Command {} ack from agent {}: {} - {}", commandId, id, body.status(), body.message());
        }

        return ResponseEntity.ok().build();
    }

    @PostMapping("/{id}/replay")
    @Operation(summary = "Replay an exchange on a specific agent (synchronous)",
            description = "Sends a replay command and waits for the agent to complete the replay. "
                    + "Returns the replay result including status, replayExchangeId, and duration.")
    @ApiResponse(responseCode = "200", description = "Replay completed (check status for success/failure)")
    @ApiResponse(responseCode = "404", description = "Agent not found or not connected")
    @ApiResponse(responseCode = "504", description = "Agent did not respond in time")
    public ResponseEntity<ReplayResponse> replayExchange(@PathVariable String id,
                                                          @RequestBody ReplayRequest request,
                                                          HttpServletRequest httpRequest) {
        AgentInfo agent = registryService.findById(id);
        if (agent == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Agent not found: " + id);
        }

        // Build protocol-compliant replay payload
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("routeId", request.routeId());
        Map<String, Object> exchange = new LinkedHashMap<>();
        exchange.put("body", request.body() != null ? request.body() : "");
        exchange.put("headers", request.headers() != null ? request.headers() : Map.of());
        payload.put("exchange", exchange);
        if (request.originalExchangeId() != null) {
            payload.put("originalExchangeId", request.originalExchangeId());
        }
        payload.put("nonce", UUID.randomUUID().toString());

        String payloadJson;
        try {
            payloadJson = objectMapper.writeValueAsString(payload);
        } catch (JsonProcessingException e) {
            log.error("Failed to serialize replay payload", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(new ReplayResponse("FAILURE", "Failed to serialize request", null));
        }

        CompletableFuture<CommandReply> future = registryService.addCommandWithReply(
                id, CommandType.REPLAY, payloadJson);

        Map<String, Object> auditDetails = new LinkedHashMap<>();
        auditDetails.put("routeId", request.routeId());
        if (request.originalExchangeId() != null) {
            auditDetails.put("originalExchangeId", request.originalExchangeId());
        }

        try {
            CommandReply reply = future.orTimeout(30, TimeUnit.SECONDS).join();
            auditDetails.put("replyStatus", reply.status());
            auditDetails.put("replyMessage", reply.message() != null ? reply.message() : "");
            auditService.log("replay_exchange", AuditCategory.AGENT, id, auditDetails,
                    "SUCCESS".equals(reply.status()) ? AuditResult.SUCCESS : AuditResult.FAILURE, httpRequest);
            return ResponseEntity.ok(new ReplayResponse(reply.status(), reply.message(), reply.data()));
        } catch (CompletionException e) {
            if (e.getCause() instanceof TimeoutException) {
                auditDetails.put("error", "timeout");
                auditService.log("replay_exchange", AuditCategory.AGENT, id, auditDetails,
                        AuditResult.FAILURE, httpRequest);
                return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
                        .body(new ReplayResponse("FAILURE", "Agent did not respond within 30 seconds", null));
            }
            auditDetails.put("error", e.getCause().getMessage());
            auditService.log("replay_exchange", AuditCategory.AGENT, id, auditDetails,
                    AuditResult.FAILURE, httpRequest);
            log.error("Error awaiting replay reply from agent {}", id, e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(new ReplayResponse("FAILURE", "Internal error: " + e.getCause().getMessage(), null));
        }
    }

    private CommandType mapCommandType(String typeStr) {
        return switch (typeStr) {
            case "config-update" -> CommandType.CONFIG_UPDATE;
            case "deep-trace" -> CommandType.DEEP_TRACE;
            case "replay" -> CommandType.REPLAY;
            case "set-traced-processors" -> CommandType.SET_TRACED_PROCESSORS;
            case "test-expression" -> CommandType.TEST_EXPRESSION;
            case "route-control" -> CommandType.ROUTE_CONTROL;
            default -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "Invalid command type: " + typeStr + ". Valid: config-update, deep-trace, replay, set-traced-processors, test-expression, route-control");
        };
    }
}
