package com.cameleer.server.app.agent;

import com.cameleer.server.app.config.AgentRegistryConfig;
import com.cameleer.server.core.agent.AgentCommand;
import com.cameleer.server.core.agent.AgentEventListener;
import com.cameleer.server.core.agent.AgentRegistryService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Manages per-agent SSE connections and delivers commands via Server-Sent Events.
 * <p>
 * Implements {@link AgentEventListener} so the core {@link AgentRegistryService}
 * can notify this component when a command is ready for delivery, without depending
 * on Spring or SSE classes.
 */
@Component
public class SseConnectionManager implements AgentEventListener {

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

    private final ConcurrentHashMap<String, SseEmitter> emitters = new ConcurrentHashMap<>();
    private final AgentRegistryService registryService;
    private final AgentRegistryConfig config;
    private final SsePayloadSigner ssePayloadSigner;
    private final ObjectMapper objectMapper;

    public SseConnectionManager(AgentRegistryService registryService, AgentRegistryConfig config,
                                SsePayloadSigner ssePayloadSigner, ObjectMapper objectMapper) {
        this.registryService = registryService;
        this.config = config;
        this.ssePayloadSigner = ssePayloadSigner;
        this.objectMapper = objectMapper;
    }

    @PostConstruct
    void init() {
        registryService.setEventListener(this);
        log.info("SseConnectionManager registered as AgentEventListener");
    }

    /**
     * Create an SSE connection for the given agent.
     * Replaces any existing connection (completing the old emitter).
     *
     * @param agentId the agent identifier
     * @return the new SseEmitter
     */
    public SseEmitter connect(String agentId) {
        SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);

        SseEmitter old = emitters.put(agentId, emitter);
        if (old != null) {
            log.debug("Replacing existing SSE connection for agent {}", agentId);
            old.complete();
        }

        // Remove from map only if the emitter is still the current one (reference equality)
        emitter.onCompletion(() -> {
            emitters.remove(agentId, emitter);
            log.debug("SSE connection completed for agent {}", agentId);
        });
        emitter.onTimeout(() -> {
            emitters.remove(agentId, emitter);
            log.debug("SSE connection timed out for agent {}", agentId);
        });
        emitter.onError(ex -> {
            emitters.remove(agentId, emitter);
            log.debug("SSE connection error for agent {}: {}", agentId, ex.getMessage());
        });

        // Send an initial keepalive comment so Spring flushes the response
        // headers immediately. Without this, clients blocking on the first
        // body byte can hang for a full ping interval before observing the
        // connection — surface symptom in ITs that assert awaitConnection().
        try {
            emitter.send(SseEmitter.event().comment("connected"));
        } catch (IOException e) {
            log.debug("Initial keepalive failed for agent {}: {}", agentId, e.getMessage());
            emitters.remove(agentId, emitter);
        }

        log.info("SSE connection established for agent {}", agentId);

        return emitter;
    }

    /**
     * Send an event to a specific agent's SSE stream.
     *
     * @param agentId   the target agent
     * @param eventId   the event ID (for Last-Event-ID reconnection)
     * @param eventType the SSE event name
     * @param data      the event data (serialized as JSON)
     * @return true if the event was sent successfully, false if the agent is not connected or send failed
     */
    public boolean sendEvent(String agentId, String eventId, String eventType, Object data) {
        SseEmitter emitter = emitters.get(agentId);
        if (emitter == null) {
            return false;
        }

        try {
            emitter.send(SseEmitter.event()
                    .id(eventId)
                    .name(eventType)
                    .data(data, MediaType.APPLICATION_JSON));
            return true;
        } catch (IOException e) {
            log.debug("Failed to send SSE event to agent {}: {}", agentId, e.getMessage());
            emitters.remove(agentId, emitter);
            return false;
        }
    }

    /**
     * Send a ping keepalive comment to all connected agents.
     */
    public void sendPingToAll() {
        for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
            String agentId = entry.getKey();
            SseEmitter emitter = entry.getValue();
            try {
                emitter.send(SseEmitter.event().comment("ping"));
            } catch (IOException e) {
                log.debug("Ping failed for agent {}, removing connection", agentId);
                emitters.remove(agentId, emitter);
            }
        }
    }

    /**
     * Check if an agent has an active SSE connection.
     */
    public int getConnectionCount() {
        return emitters.size();
    }

    public boolean isConnected(String agentId) {
        return emitters.containsKey(agentId);
    }

    /**
     * Called by the registry when a command is ready for an agent.
     * Attempts to deliver via SSE; if successful, marks as DELIVERED.
     * If the agent is not connected, the command stays PENDING.
     */
    @Override
    public void onCommandReady(String agentId, AgentCommand command) {
        String eventType = command.type().name().toLowerCase().replace('_', '-');
        String signedPayload = ssePayloadSigner.signPayload(command.payload());
        // Parse to JsonNode so SseEmitter serializes the tree correctly (avoids double-quoting a raw string)
        Object data;
        try {
            data = objectMapper.readTree(signedPayload);
        } catch (Exception e) {
            log.warn("Failed to parse signed payload as JSON, sending raw string", e);
            data = signedPayload;
        }
        boolean sent = sendEvent(agentId, command.id(), eventType, data);
        if (sent) {
            registryService.markDelivered(agentId, command.id());
            log.debug("Command {} ({}) delivered to agent {} via SSE", command.id(), eventType, agentId);
        } else {
            log.debug("Agent {} not connected, command {} stays PENDING", agentId, command.id());
        }
    }

    /**
     * Scheduled ping keepalive to all connected agents.
     */
    @Scheduled(fixedDelayString = "${cameleer.server.agentregistry.ping-interval-ms:15000}")
    void pingAll() {
        if (!emitters.isEmpty()) {
            sendPingToAll();
        }
    }
}
