package com.cameleer.server.app.alerting.eval;

import com.cameleer.server.app.search.ClickHouseSearchIndex;
import com.cameleer.server.core.alerting.AlertMatchSpec;
import com.cameleer.server.core.alerting.AlertRule;
import com.cameleer.server.core.alerting.ConditionKind;
import com.cameleer.server.core.alerting.ExchangeMatchCondition;
import com.cameleer.server.core.alerting.FireMode;
import com.cameleer.server.core.runtime.EnvironmentRepository;
import com.cameleer.server.core.search.ExecutionSummary;
import com.cameleer.server.core.search.SearchRequest;
import com.cameleer.server.core.search.SearchResult;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class ExchangeMatchEvaluator implements ConditionEvaluator<ExchangeMatchCondition> {

    private final ClickHouseSearchIndex searchIndex;
    private final EnvironmentRepository envRepo;

    public ExchangeMatchEvaluator(ClickHouseSearchIndex searchIndex, EnvironmentRepository envRepo) {
        this.searchIndex = searchIndex;
        this.envRepo     = envRepo;
    }

    @Override
    public ConditionKind kind() { return ConditionKind.EXCHANGE_MATCH; }

    @Override
    public EvalResult evaluate(ExchangeMatchCondition c, AlertRule rule, EvalContext ctx) {
        String envSlug = envRepo.findById(rule.environmentId())
                .map(e -> e.slug())
                .orElse(null);

        return switch (c.fireMode()) {
            case COUNT_IN_WINDOW -> evaluateCount(c, rule, ctx, envSlug);
            case PER_EXCHANGE    -> evaluatePerExchange(c, rule, ctx, envSlug);
        };
    }

    // ── COUNT_IN_WINDOW ───────────────────────────────────────────────────────

    private EvalResult evaluateCount(ExchangeMatchCondition c, AlertRule rule,
                                     EvalContext ctx, String envSlug) {
        String appSlug = c.scope() != null ? c.scope().appSlug() : null;
        String routeId = c.scope() != null ? c.scope().routeId() : null;
        ExchangeMatchCondition.ExchangeFilter filter = c.filter();

        var spec = new AlertMatchSpec(
                ctx.tenantId(),
                envSlug,
                appSlug,
                routeId,
                filter != null ? filter.status() : null,
                filter != null ? filter.attributes() : Map.of(),
                ctx.now().minusSeconds(c.windowSeconds()),
                ctx.now(),
                null
        );

        long count = searchIndex.countExecutionsForAlerting(spec);
        if (count <= c.threshold()) return EvalResult.Clear.INSTANCE;

        return new EvalResult.Firing(
                (double) count,
                c.threshold().doubleValue(),
                Map.of(
                        "app",   Map.of("slug", appSlug == null ? "" : appSlug),
                        "route", Map.of("id",   routeId == null ? "" : routeId)
                )
        );
    }

    // ── PER_EXCHANGE ──────────────────────────────────────────────────────────

    private EvalResult evaluatePerExchange(ExchangeMatchCondition c, AlertRule rule,
                                           EvalContext ctx, String envSlug) {
        String appSlug = c.scope() != null ? c.scope().appSlug() : null;
        String routeId = c.scope() != null ? c.scope().routeId() : null;
        ExchangeMatchCondition.ExchangeFilter filter = c.filter();

        // Resolve cursor from evalState
        Instant cursor = null;
        Object raw = rule.evalState().get("lastExchangeTs");
        if (raw instanceof String s && !s.isBlank()) {
            try { cursor = Instant.parse(s); } catch (Exception ignored) {}
        } else if (raw instanceof Instant i) {
            cursor = i;
        }

        // Build SearchRequest — use cursor as timeFrom so we only see exchanges after last run
        var req = new SearchRequest(
                filter != null ? filter.status() : null,
                cursor,                          // timeFrom = cursor (or null for first run)
                ctx.now(),                       // timeTo
                null, null, null,                // durationMin/Max, correlationId
                null, null, null, null,          // text variants
                routeId,
                null,                            // instanceId
                null,                            // processorType
                appSlug,
                null,                            // instanceIds
                0,
                50,
                "startTime",
                "asc",                           // asc so we process oldest first
                envSlug
        );

        SearchResult<ExecutionSummary> result = searchIndex.search(req);
        List<ExecutionSummary> matches = result.data();

        if (matches.isEmpty()) return new EvalResult.Batch(List.of());

        // Find the latest startTime across all matches — becomes the next cursor
        Instant latestTs = matches.stream()
                .map(ExecutionSummary::startTime)
                .max(Instant::compareTo)
                .orElse(ctx.now());

        List<EvalResult.Firing> firings = new ArrayList<>();
        for (int i = 0; i < matches.size(); i++) {
            ExecutionSummary ex = matches.get(i);
            Map<String, Object> ctx2 = new HashMap<>();
            ctx2.put("exchange", Map.of(
                    "id",        ex.executionId(),
                    "routeId",   ex.routeId() == null   ? "" : ex.routeId(),
                    "status",    ex.status()  == null   ? "" : ex.status(),
                    "startTime", ex.startTime() == null ? "" : ex.startTime().toString()
            ));
            ctx2.put("app", Map.of("slug", ex.applicationId() == null ? "" : ex.applicationId()));

            // Attach the next-cursor to the last firing so the job can extract it
            if (i == matches.size() - 1) {
                ctx2.put("_nextCursor", latestTs);
            }

            firings.add(new EvalResult.Firing(1.0, null, ctx2));
        }

        return new EvalResult.Batch(firings);
    }
}
