package net.siegeln.cameleer.saas.vendor;

import net.siegeln.cameleer.saas.identity.ServerApiClient;
import net.siegeln.cameleer.saas.identity.ServerApiClient.MetricsQueryResponse;
import net.siegeln.cameleer.saas.identity.ServerApiClient.MetricsPoint;
import net.siegeln.cameleer.saas.identity.ServerApiClient.MetricsSeries;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

@Service
public class TenantMetricsService {

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

    private final ServerApiClient serverApiClient;

    public TenantMetricsService(ServerApiClient serverApiClient) {
        this.serverApiClient = serverApiClient;
    }

    // --- Response records ---

    public record MetricsSummary(
        String collectedAt,
        AgentMetrics agents,
        IngestionMetrics ingestion,
        ServerJvmMetrics server,
        HttpMetrics http,
        double authFailuresPerMinute
    ) {}

    public record AgentMetrics(int live, int stale, int dead, int shutdown) {}

    public record IngestionMetrics(long bufferDepth, double dropsPerMinute) {}

    public record ServerJvmMetrics(
        double cpuUsage,
        long heapUsedBytes,
        long heapMaxBytes,
        long uptimeSeconds,
        int threadCount
    ) {}

    public record HttpMetrics(double requestsPerMinute, double errorRate) {}

    /**
     * Query a tenant's server for key metrics and assemble a summary snapshot.
     * Fires multiple queries concurrently (one per metric group) over the last 5 minutes.
     */
    public MetricsSummary getMetricsSummary(String serverEndpoint) {
        Instant to = Instant.now();
        Instant from = to.minus(5, ChronoUnit.MINUTES);
        String fromStr = from.toString();
        String toStr = to.toString();

        // Fire all queries concurrently
        var agentsFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "cameleer.agents.connected", "value", fromStr, toStr, "avg", "raw", List.of("state"), null));
        var cpuFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "process.cpu.usage", "value", fromStr, toStr, "avg", "raw", null, null));
        var heapUsedFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "jvm.memory.used", "value", fromStr, toStr, "sum", "raw", null, Map.of("area", "heap")));
        var heapMaxFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "jvm.memory.max", "value", fromStr, toStr, "sum", "raw", null, Map.of("area", "heap")));
        var uptimeFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "process.uptime", "value", fromStr, toStr, "latest", "raw", null, null));
        var threadsFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "jvm.threads.live", "value", fromStr, toStr, "avg", "raw", null, null));
        var dropsFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "cameleer.ingestion.drops", "count", fromStr, toStr, "sum", "delta", null, null));
        var bufferFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "cameleer.ingestion.buffer.size", "value", fromStr, toStr, "sum", "raw", null, null));
        var httpTotalFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "http.server.requests", "count", fromStr, toStr, "sum", "delta", null, null));
        var http5xxFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "http.server.requests", "count", fromStr, toStr, "sum", "delta", null, Map.of("outcome", "SERVER_ERROR")));
        var authFuture = CompletableFuture.supplyAsync(() ->
            query(serverEndpoint, "cameleer.auth.failures", "count", fromStr, toStr, "sum", "delta", null, null));

        try {
            // Extract latest values from each response
            var agentsResp = agentsFuture.join();
            int live = agentStateValue(agentsResp, "live");
            int stale = agentStateValue(agentsResp, "stale");
            int dead = agentStateValue(agentsResp, "dead");
            int shutdown = agentStateValue(agentsResp, "shutdown");

            double cpu = latestValue(cpuFuture.join());
            long heapUsed = (long) latestValue(heapUsedFuture.join());
            long heapMax = (long) latestValue(heapMaxFuture.join());
            long uptimeMs = (long) latestValue(uptimeFuture.join());
            int threads = (int) latestValue(threadsFuture.join());

            double dropsTotal = sumLatestValues(dropsFuture.join());
            long bufferDepth = (long) latestValue(bufferFuture.join());

            double httpTotal = sumLatestValues(httpTotalFuture.join());
            double http5xx = sumLatestValues(http5xxFuture.join());
            double errorRate = httpTotal > 0 ? http5xx / httpTotal : 0.0;
            // stepSeconds=300 (5min window), so total is per-5-min; convert to per-minute
            double httpPerMin = httpTotal / 5.0;

            double authTotal = sumLatestValues(authFuture.join());
            double authPerMin = authTotal / 5.0;

            return new MetricsSummary(
                toStr,
                new AgentMetrics(live, stale, dead, shutdown),
                new IngestionMetrics(bufferDepth, dropsTotal / 5.0),
                new ServerJvmMetrics(cpu, heapUsed, heapMax, uptimeMs / 1000, threads),
                new HttpMetrics(httpPerMin, errorRate),
                authPerMin
            );
        } catch (Exception e) {
            log.warn("Failed to assemble metrics summary for {}: {}", serverEndpoint, e.getMessage());
            return null;
        }
    }

    private MetricsQueryResponse query(String endpoint, String metric, String statistic,
                                       String from, String to, String aggregation, String mode,
                                       List<String> groupByTags, Map<String, String> filterTags) {
        Map<String, Object> body = new HashMap<>();
        body.put("metric", metric);
        body.put("statistic", statistic);
        body.put("from", from);
        body.put("to", to);
        body.put("stepSeconds", 300);
        body.put("aggregation", aggregation);
        body.put("mode", mode);
        if (groupByTags != null) body.put("groupByTags", groupByTags);
        if (filterTags != null) body.put("filterTags", filterTags);
        return serverApiClient.queryServerMetrics(endpoint, body);
    }

    /** Extract the latest value from the first (or only) series. */
    private double latestValue(MetricsQueryResponse resp) {
        if (resp == null || resp.series() == null || resp.series().isEmpty()) return 0.0;
        List<MetricsPoint> points = resp.series().getFirst().points();
        if (points == null || points.isEmpty()) return 0.0;
        return points.getLast().v();
    }

    /** Sum the latest value across all series (for metrics with groupByTags or multiple series). */
    private double sumLatestValues(MetricsQueryResponse resp) {
        if (resp == null || resp.series() == null || resp.series().isEmpty()) return 0.0;
        double sum = 0.0;
        for (MetricsSeries series : resp.series()) {
            if (series.points() != null && !series.points().isEmpty()) {
                sum += series.points().getLast().v();
            }
        }
        return sum;
    }

    /** Extract the latest value for a specific agent state tag. */
    private int agentStateValue(MetricsQueryResponse resp, String state) {
        if (resp == null || resp.series() == null) return 0;
        for (MetricsSeries series : resp.series()) {
            if (series.tags() != null && state.equals(series.tags().get("state"))) {
                if (series.points() != null && !series.points().isEmpty()) {
                    return (int) series.points().getLast().v();
                }
            }
        }
        return 0;
    }
}
