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

import com.cameleer.server.app.alerting.config.AlertingProperties;
import com.cameleer.server.app.alerting.metrics.AlertingMetrics;
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.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.stereotype.Component;

import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * Claim-polling evaluator job.
 * <p>
 * On each tick, claims a batch of due {@link AlertRule}s via {@code FOR UPDATE SKIP LOCKED},
 * invokes the matching {@link ConditionEvaluator}, applies the {@link AlertStateTransitions}
 * state machine, persists any new/updated {@link AlertInstance}, enqueues webhook
 * {@link AlertNotification}s on first-fire, and releases the claim.
 */
@Component
public class AlertEvaluatorJob implements SchedulingConfigurer {

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

    private final AlertingProperties props;
    private final AlertRuleRepository ruleRepo;
    private final AlertInstanceRepository instanceRepo;
    private final AlertNotificationRepository notificationRepo;
    private final Map<ConditionKind, ConditionEvaluator<?>> evaluators;
    private final PerKindCircuitBreaker circuitBreaker;
    private final MustacheRenderer renderer;
    private final NotificationContextBuilder contextBuilder;
    private final EnvironmentRepository environmentRepo;
    private final ObjectMapper objectMapper;
    private final BatchResultApplier batchResultApplier;
    private final String instanceId;
    private final String tenantId;
    private final Clock clock;
    private final AlertingMetrics metrics;

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public AlertEvaluatorJob(
            AlertingProperties props,
            AlertRuleRepository ruleRepo,
            AlertInstanceRepository instanceRepo,
            AlertNotificationRepository notificationRepo,
            List<ConditionEvaluator<?>> evaluatorList,
            PerKindCircuitBreaker circuitBreaker,
            MustacheRenderer renderer,
            NotificationContextBuilder contextBuilder,
            EnvironmentRepository environmentRepo,
            ObjectMapper objectMapper,
            BatchResultApplier batchResultApplier,
            @Qualifier("alertingInstanceId") String instanceId,
            @Value("${cameleer.server.tenant.id:default}") String tenantId,
            Clock alertingClock,
            AlertingMetrics metrics) {

        this.props              = props;
        this.ruleRepo           = ruleRepo;
        this.instanceRepo       = instanceRepo;
        this.notificationRepo   = notificationRepo;
        this.evaluators         = evaluatorList.stream()
                .collect(Collectors.toMap(ConditionEvaluator::kind, e -> e));
        this.circuitBreaker     = circuitBreaker;
        this.renderer           = renderer;
        this.contextBuilder     = contextBuilder;
        this.environmentRepo    = environmentRepo;
        this.objectMapper       = objectMapper;
        this.batchResultApplier = batchResultApplier;
        this.instanceId         = instanceId;
        this.tenantId           = tenantId;
        this.clock              = alertingClock;
        this.metrics            = metrics;
    }

    // -------------------------------------------------------------------------
    // SchedulingConfigurer — register the tick as a fixed-delay task
    // -------------------------------------------------------------------------

    @Override
    public void configureTasks(ScheduledTaskRegistrar registrar) {
        registrar.addFixedDelayTask(this::tick, props.effectiveEvaluatorTickIntervalMs());
    }

    // -------------------------------------------------------------------------
    // Tick — package-visible for same-package tests; also accessible cross-package for lifecycle ITs
    // -------------------------------------------------------------------------

    public void tick() {
        List<AlertRule> claimed = ruleRepo.claimDueRules(
                instanceId,
                props.effectiveEvaluatorBatchSize(),
                props.effectiveClaimTtlSeconds());

        if (claimed.isEmpty()) return;

        TickCache cache = new TickCache();
        EvalContext ctx = new EvalContext(tenantId, Instant.now(clock), cache);

        for (AlertRule rule : claimed) {
            Instant nextRun = Instant.now(clock).plusSeconds(rule.evaluationIntervalSeconds());
            if (circuitBreaker.isOpen(rule.conditionKind())) {
                log.debug("Circuit breaker open for {}; skipping rule {}", rule.conditionKind(), rule.id());
                reschedule(rule, nextRun);
                continue;
            }

            EvalResult result;
            try {
                result = metrics.evalDuration(rule.conditionKind())
                        .recordCallable(() -> evaluateSafely(rule, ctx));
            } catch (Exception e) {
                metrics.evalError(rule.conditionKind(), rule.id());
                circuitBreaker.recordFailure(rule.conditionKind());
                log.warn("Evaluator error for rule {} ({}): {}", rule.id(), rule.conditionKind(), e.toString());
                // Evaluation itself failed — release the claim so the rule can be
                // retried on the next tick. Cursor stays put.
                reschedule(rule, nextRun);
                continue;
            }

            if (result instanceof EvalResult.Batch b) {
                // Phase 2: the Batch path is atomic. The @Transactional apply() on
                // BatchResultApplier wraps instance writes, notification enqueues,
                // AND the cursor advance + releaseClaim into a single tx. A
                // mid-batch fault rolls everything back — including the cursor —
                // so the next tick replays the whole batch exactly once.
                try {
                    batchResultApplier.apply(rule, b, nextRun);
                    circuitBreaker.recordSuccess(rule.conditionKind());
                } catch (Exception e) {
                    metrics.evalError(rule.conditionKind(), rule.id());
                    circuitBreaker.recordFailure(rule.conditionKind());
                    log.warn("Batch apply failed for rule {} ({}): {} — rolling back; next tick will retry",
                            rule.id(), rule.conditionKind(), e.toString());
                    // The transaction rolled back. Do NOT call reschedule here —
                    // leaving claim + next_evaluation_at as they were means the
                    // claim TTL takes over and the rule becomes due on its own.
                    // Rethrowing is unnecessary for correctness — the cursor
                    // stayed put, so exactly-once-per-exchange is preserved.
                }
            } else {
                // Non-Batch path (FIRING / Clear / Error): classic apply + rule
                // reschedule. Not wrapped in a single tx — semantics unchanged
                // from pre-Phase-2.
                try {
                    applyResult(rule, result);
                    circuitBreaker.recordSuccess(rule.conditionKind());
                } catch (Exception e) {
                    metrics.evalError(rule.conditionKind(), rule.id());
                    circuitBreaker.recordFailure(rule.conditionKind());
                    log.warn("applyResult failed for rule {} ({}): {}",
                            rule.id(), rule.conditionKind(), e.toString());
                } finally {
                    reschedule(rule, nextRun);
                }
            }
        }

        sweepReNotify();
    }

    // -------------------------------------------------------------------------
    // Re-notification cadence sweep
    // -------------------------------------------------------------------------

    private void sweepReNotify() {
        Instant now = Instant.now(clock);
        List<AlertInstance> due = instanceRepo.listFiringDueForReNotify(now);
        for (AlertInstance i : due) {
            try {
                AlertRule rule = i.ruleId() == null ? null : ruleRepo.findById(i.ruleId()).orElse(null);
                if (rule == null || rule.reNotifyMinutes() <= 0) continue;
                enqueueNotifications(rule, i, now);
                instanceRepo.save(i.withLastNotifiedAt(now));
                log.debug("Re-notify enqueued for instance {} (rule {})", i.id(), i.ruleId());
            } catch (Exception e) {
                log.warn("Re-notify sweep error for instance {}: {}", i.id(), e.toString());
            }
        }
    }

    // -------------------------------------------------------------------------
    // Evaluation
    // -------------------------------------------------------------------------

    @SuppressWarnings({"rawtypes", "unchecked"})
    private EvalResult evaluateSafely(AlertRule rule, EvalContext ctx) {
        ConditionEvaluator evaluator = evaluators.get(rule.conditionKind());
        if (evaluator == null) {
            throw new IllegalStateException("No evaluator registered for " + rule.conditionKind());
        }
        return evaluator.evaluate(rule.condition(), rule, ctx);
    }

    // -------------------------------------------------------------------------
    // State machine application
    // -------------------------------------------------------------------------

    private void applyResult(AlertRule rule, EvalResult result) {
        // Note: the Batch path is handled by BatchResultApplier (transactional) —
        // tick() routes Batch results there directly and never calls applyResult
        // for them. This method only handles FIRING / Clear / Error state-machine
        // transitions for the classic (non-PER_EXCHANGE) path.
        AlertInstance current = instanceRepo.findOpenForRule(rule.id()).orElse(null);
        Instant now = Instant.now(clock);

        AlertStateTransitions.apply(current, result, rule, now).ifPresent(next -> {
            // Determine whether this is a newly created instance transitioning to FIRING
            boolean isFirstFire = current == null && next.state() == AlertState.FIRING;
            boolean promotedFromPending = current != null
                    && current.state() == AlertState.PENDING
                    && next.state() == AlertState.FIRING;

            AlertInstance withSnapshot = next.withRuleSnapshot(snapshotRule(rule));
            AlertInstance enriched = enrichTitleMessage(rule, withSnapshot);
            AlertInstance persisted = instanceRepo.save(enriched);

            if (isFirstFire || promotedFromPending) {
                enqueueNotifications(rule, persisted, now);
            }
        });
    }

    // -------------------------------------------------------------------------
    // Title / message rendering
    // -------------------------------------------------------------------------

    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);
    }

    // -------------------------------------------------------------------------
    // Notification enqueue
    // -------------------------------------------------------------------------

    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);
    }

    // -------------------------------------------------------------------------
    // Claim release
    // -------------------------------------------------------------------------

    private void reschedule(AlertRule rule, Instant nextRun) {
        ruleRepo.releaseClaim(rule.id(), nextRun, rule.evalState());
    }

    // -------------------------------------------------------------------------
    // Rule snapshot helper (used by tests / future extensions)
    // -------------------------------------------------------------------------

    @SuppressWarnings("unchecked")
    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 java.util.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());
        }
    }

    // -------------------------------------------------------------------------
    // Visible for testing
    // -------------------------------------------------------------------------

    /** Returns the evaluator map (for inspection in tests). */
    Map<ConditionKind, ConditionEvaluator<?>> evaluators() {
        return evaluators;
    }
}
