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

import com.cameleer.server.app.alerting.notify.MustacheRenderer;
import com.cameleer.server.app.alerting.notify.NotificationContextBuilder;
import com.cameleer.server.core.alerting.*;
import com.cameleer.server.core.runtime.Environment;
import com.cameleer.server.core.runtime.EnvironmentRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.Clock;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

/**
 * Applies a {@link EvalResult.Batch} result to persistent state inside a single
 * transaction: instance writes, notification enqueues, and the rule's cursor
 * advance + {@code releaseClaim} either all commit or all roll back together.
 * <p>
 * Lives in its own bean so the {@code @Transactional} annotation engages via the
 * Spring proxy when invoked from {@link AlertEvaluatorJob#tick()}; calling it as
 * {@code this.apply(...)} from {@code AlertEvaluatorJob} (a bean calling its own
 * method) would bypass the proxy and silently disable the transaction.
 * <p>
 * Phase 2 of the per-exchange exactly-once plan (see
 * {@code docs/superpowers/plans/2026-04-22-per-exchange-exactly-once.md}).
 */
@Component
public class BatchResultApplier {

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

    private final AlertRuleRepository ruleRepo;
    private final AlertInstanceRepository instanceRepo;
    private final AlertNotificationRepository notificationRepo;
    private final MustacheRenderer renderer;
    private final NotificationContextBuilder contextBuilder;
    private final EnvironmentRepository environmentRepo;
    private final ObjectMapper objectMapper;
    private final Clock clock;

    public BatchResultApplier(
            AlertRuleRepository ruleRepo,
            AlertInstanceRepository instanceRepo,
            AlertNotificationRepository notificationRepo,
            MustacheRenderer renderer,
            NotificationContextBuilder contextBuilder,
            EnvironmentRepository environmentRepo,
            ObjectMapper objectMapper,
            Clock alertingClock) {
        this.ruleRepo         = ruleRepo;
        this.instanceRepo     = instanceRepo;
        this.notificationRepo = notificationRepo;
        this.renderer         = renderer;
        this.contextBuilder   = contextBuilder;
        this.environmentRepo  = environmentRepo;
        this.objectMapper     = objectMapper;
        this.clock            = alertingClock;
    }

    /**
     * Atomically apply a Batch result for a single rule:
     * <ol>
     *   <li>persist a FIRING instance per firing + enqueue its notifications</li>
     *   <li>advance the rule's cursor ({@code evalState}) iff the batch supplied one</li>
     *   <li>release the claim with the new {@code nextRun} + {@code evalState}</li>
     * </ol>
     * Any exception thrown from the repo calls rolls back every write — including
     * the cursor advance — so the rule is replayable on the next tick.
     */
    @Transactional
    public void apply(AlertRule rule, EvalResult.Batch batch, Instant nextRun) {
        for (EvalResult.Firing f : batch.firings()) {
            applyBatchFiring(rule, f);
        }
        Map<String, Object> nextEvalState =
                batch.nextEvalState().isEmpty() ? rule.evalState() : batch.nextEvalState();
        ruleRepo.releaseClaim(rule.id(), nextRun, nextEvalState);
    }

    /**
     * Batch (PER_EXCHANGE) mode: always create a fresh FIRING instance per Firing entry.
     * No forDuration check — each exchange is its own event.
     */
    private void applyBatchFiring(AlertRule rule, EvalResult.Firing f) {
        Instant now = Instant.now(clock);
        AlertInstance instance = AlertStateTransitions.newInstance(rule, f, AlertState.FIRING, now)
                .withRuleSnapshot(snapshotRule(rule));
        AlertInstance enriched = enrichTitleMessage(rule, instance);
        AlertInstance persisted = instanceRepo.save(enriched);
        enqueueNotifications(rule, persisted, now);
    }

    private AlertInstance enrichTitleMessage(AlertRule rule, AlertInstance instance) {
        Environment env = environmentRepo.findById(rule.environmentId()).orElse(null);
        Map<String, Object> ctx = contextBuilder.build(rule, instance, env, null);
        String title   = renderer.render(rule.notificationTitleTmpl(), ctx);
        String message = renderer.render(rule.notificationMessageTmpl(), ctx);
        return instance.withTitleMessage(title, message);
    }

    private void enqueueNotifications(AlertRule rule, AlertInstance instance, Instant now) {
        for (WebhookBinding w : rule.webhooks()) {
            Map<String, Object> payload = buildPayload(rule, instance);
            notificationRepo.save(new AlertNotification(
                    UUID.randomUUID(),
                    instance.id(),
                    w.id(),
                    w.outboundConnectionId(),
                    NotificationStatus.PENDING,
                    0,
                    now,
                    null, null, null, null,
                    payload,
                    null,
                    now));
        }
    }

    private Map<String, Object> buildPayload(AlertRule rule, AlertInstance instance) {
        Environment env = environmentRepo.findById(rule.environmentId()).orElse(null);
        return contextBuilder.build(rule, instance, env, null);
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> snapshotRule(AlertRule rule) {
        try {
            Map<String, Object> raw = objectMapper.convertValue(rule, Map.class);
            // Map.copyOf (used in AlertInstance compact ctor) rejects null values —
            // strip them so the snapshot is safe to store.
            Map<String, Object> safe = new LinkedHashMap<>();
            raw.forEach((k, v) -> { if (v != null) safe.put(k, v); });
            return safe;
        } catch (Exception e) {
            log.warn("Failed to snapshot rule {}: {}", rule.id(), e.getMessage());
            return Map.of("id", rule.id().toString(), "name", rule.name());
        }
    }
}
