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

import com.cameleer.server.app.alerting.config.AlertingProperties;
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.Comparator;
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;
    private final AlertingProperties alertingProperties;

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

    @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 composite cursor: (startTime, executionId)
        Instant cursorTs;
        String cursorId;
        Object raw = rule.evalState().get("lastExchangeCursor");
        if (raw instanceof String s && !s.isBlank()) {
            int pipe = s.indexOf('|');
            if (pipe < 0) {
                // Malformed — treat as first-run (with deploy-backlog-cap clamp).
                cursorTs = firstRunCursorTs(rule, ctx);
                cursorId = "";
            } else {
                cursorTs = Instant.parse(s.substring(0, pipe));
                cursorId = s.substring(pipe + 1);
            }
        } else {
            // First run — bounded by rule.createdAt, empty executionId so any real id sorts after it.
            // Clamp to deploy-backlog-cap to avoid backlog flooding for long-lived rules on first
            // post-deploy tick. Normal-advance path (valid cursor above) is intentionally unaffected.
            cursorTs = firstRunCursorTs(rule, ctx);
            cursorId = "";
        }

        var req = new SearchRequest(
                filter != null ? filter.status() : null,
                cursorTs,                        // timeFrom
                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
                cursorId.isEmpty() ? null : cursorId,  // afterExecutionId — null on first run enables >=
                envSlug
        );

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

        if (matches.isEmpty()) return EvalResult.Batch.empty();

        // Ensure deterministic ordering for cursor advance
        matches = new ArrayList<>(matches);
        matches.sort(Comparator
                .comparing(ExecutionSummary::startTime)
                .thenComparing(ExecutionSummary::executionId));

        ExecutionSummary last = matches.get(matches.size() - 1);
        String nextCursorSerialized = last.startTime().toString() + "|" + last.executionId();

        List<EvalResult.Firing> firings = new ArrayList<>();
        for (ExecutionSummary ex : matches) {
            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()));
            firings.add(new EvalResult.Firing(1.0, null, ctx2));
        }

        Map<String, Object> nextEvalState = new HashMap<>(rule.evalState());
        nextEvalState.put("lastExchangeCursor", nextCursorSerialized);
        return new EvalResult.Batch(firings, nextEvalState);
    }

    /**
     * First-run cursor timestamp: {@code rule.createdAt()}, clamped to
     * {@code now - perExchangeDeployBacklogCapSeconds} so a long-lived PER_EXCHANGE rule
     * doesn't scan from its creation date forward on first post-deploy tick.
     * <p>
     * Cap ≤ 0 disables the clamp (first-run falls back to {@code rule.createdAt()} verbatim).
     * Applied only on first-run / malformed-cursor paths — the normal-advance path is
     * intentionally unaffected so legitimate missed ticks are not silently skipped.
     */
    private Instant firstRunCursorTs(AlertRule rule, EvalContext ctx) {
        Instant cursorTs = rule.createdAt();
        int capSeconds = alertingProperties.effectivePerExchangeDeployBacklogCapSeconds();
        if (capSeconds > 0) {
            Instant capFloor = ctx.now().minusSeconds(capSeconds);
            if (cursorTs == null || cursorTs.isBefore(capFloor)) {
                cursorTs = capFloor;
            }
        }
        return cursorTs;
    }
}
