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

import com.cameleer.server.app.alerting.dto.AlertRuleRequest;
import com.cameleer.server.app.alerting.dto.AlertRuleResponse;
import com.cameleer.server.app.alerting.dto.RenderPreviewRequest;
import com.cameleer.server.app.alerting.dto.RenderPreviewResponse;
import com.cameleer.server.app.alerting.dto.TestEvaluateRequest;
import com.cameleer.server.app.alerting.dto.TestEvaluateResponse;
import com.cameleer.server.app.alerting.dto.WebhookBindingRequest;
import com.cameleer.server.app.alerting.eval.ConditionEvaluator;
import com.cameleer.server.app.alerting.eval.EvalContext;
import com.cameleer.server.app.alerting.eval.EvalResult;
import com.cameleer.server.app.alerting.eval.TickCache;
import com.cameleer.server.app.alerting.notify.MustacheRenderer;
import com.cameleer.server.app.web.EnvPath;
import com.cameleer.server.core.admin.AuditCategory;
import com.cameleer.server.core.admin.AuditResult;
import com.cameleer.server.core.admin.AuditService;
import com.cameleer.server.core.alerting.AlertCondition;
import com.cameleer.server.core.alerting.AlertRule;
import com.cameleer.server.core.alerting.AlertRuleRepository;
import com.cameleer.server.core.alerting.AlertRuleTarget;
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.alerting.WebhookBinding;
import com.cameleer.server.core.outbound.OutboundConnection;
import com.cameleer.server.core.outbound.OutboundConnectionService;
import com.cameleer.server.core.runtime.Environment;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern;

/**
 * REST controller for alert rules (env-scoped).
 * <p>
 * CRITICAL: {@link ExchangeMatchCondition#filter()} attribute KEYS are inlined into ClickHouse SQL.
 * They are validated here at save time to match {@code ^[a-zA-Z0-9._-]+$} before any SQL is built.
 */
@RestController
@RequestMapping("/api/v1/environments/{envSlug}/alerts/rules")
@Tag(name = "Alert Rules", description = "Alert rule management (env-scoped)")
@PreAuthorize("hasAnyRole('VIEWER','OPERATOR','ADMIN')")
public class AlertRuleController {

    /**
     * Attribute KEY allowlist. Keys are inlined into ClickHouse SQL via
     * {@code JSONExtractString(attributes, '<key>')}, so this pattern is a hard security gate.
     * Values are always parameter-bound and safe.
     */
    private static final Pattern ATTR_KEY = Pattern.compile("^[a-zA-Z0-9._-]+$");

    private final AlertRuleRepository ruleRepo;
    private final OutboundConnectionService connectionService;
    private final AuditService auditService;
    private final MustacheRenderer renderer;
    private final Map<ConditionKind, ConditionEvaluator<?>> evaluators;
    private final Clock clock;
    private final String tenantId;

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public AlertRuleController(AlertRuleRepository ruleRepo,
                               OutboundConnectionService connectionService,
                               AuditService auditService,
                               MustacheRenderer renderer,
                               List<ConditionEvaluator<?>> evaluatorList,
                               Clock alertingClock,
                               @Value("${cameleer.server.tenant.id:default}") String tenantId) {
        this.ruleRepo = ruleRepo;
        this.connectionService = connectionService;
        this.auditService = auditService;
        this.renderer = renderer;
        this.evaluators = new java.util.EnumMap<>(ConditionKind.class);
        for (ConditionEvaluator<?> e : evaluatorList) {
            this.evaluators.put(e.kind(), e);
        }
        this.clock = alertingClock;
        this.tenantId = tenantId;
    }

    // -------------------------------------------------------------------------
    // List / Get
    // -------------------------------------------------------------------------

    @GetMapping
    public List<AlertRuleResponse> list(@EnvPath Environment env) {
        return ruleRepo.listByEnvironment(env.id())
                .stream().map(AlertRuleResponse::from).toList();
    }

    @GetMapping("/{id}")
    public AlertRuleResponse get(@EnvPath Environment env, @PathVariable UUID id) {
        AlertRule rule = requireRule(id, env.id());
        return AlertRuleResponse.from(rule);
    }

    // -------------------------------------------------------------------------
    // Create / Update / Delete
    // -------------------------------------------------------------------------

    @PostMapping
    @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')")
    public ResponseEntity<AlertRuleResponse> create(
            @EnvPath Environment env,
            @Valid @RequestBody AlertRuleRequest req,
            HttpServletRequest httpRequest) {

        validateAttributeKeys(req.condition());
        validateBusinessRules(req);
        validateWebhooks(req.webhooks(), env.id());

        AlertRule draft = buildRule(null, env.id(), req, currentUserId());
        AlertRule saved = ruleRepo.save(draft);

        auditService.log("ALERT_RULE_CREATE", AuditCategory.ALERT_RULE_CHANGE,
                saved.id().toString(), Map.of("name", saved.name()), AuditResult.SUCCESS, httpRequest);

        return ResponseEntity.status(HttpStatus.CREATED).body(AlertRuleResponse.from(saved));
    }

    @PutMapping("/{id}")
    @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')")
    public AlertRuleResponse update(
            @EnvPath Environment env,
            @PathVariable UUID id,
            @Valid @RequestBody AlertRuleRequest req,
            HttpServletRequest httpRequest) {

        AlertRule existing = requireRule(id, env.id());
        validateAttributeKeys(req.condition());
        validateBusinessRules(req);
        validateWebhooks(req.webhooks(), env.id());

        AlertRule updated = buildRule(existing, env.id(), req, currentUserId());
        AlertRule saved = ruleRepo.save(updated);

        auditService.log("ALERT_RULE_UPDATE", AuditCategory.ALERT_RULE_CHANGE,
                id.toString(), Map.of("name", saved.name()), AuditResult.SUCCESS, httpRequest);

        return AlertRuleResponse.from(saved);
    }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')")
    public ResponseEntity<Void> delete(
            @EnvPath Environment env,
            @PathVariable UUID id,
            HttpServletRequest httpRequest) {

        requireRule(id, env.id());
        ruleRepo.delete(id);

        auditService.log("ALERT_RULE_DELETE", AuditCategory.ALERT_RULE_CHANGE,
                id.toString(), Map.of(), AuditResult.SUCCESS, httpRequest);

        return ResponseEntity.noContent().build();
    }

    // -------------------------------------------------------------------------
    // Enable / Disable
    // -------------------------------------------------------------------------

    @PostMapping("/{id}/enable")
    @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')")
    public AlertRuleResponse enable(
            @EnvPath Environment env,
            @PathVariable UUID id,
            HttpServletRequest httpRequest) {

        AlertRule rule = requireRule(id, env.id());
        AlertRule updated = withEnabled(rule, true);
        AlertRule saved = ruleRepo.save(updated);

        auditService.log("ALERT_RULE_ENABLE", AuditCategory.ALERT_RULE_CHANGE,
                id.toString(), Map.of("name", saved.name()), AuditResult.SUCCESS, httpRequest);

        return AlertRuleResponse.from(saved);
    }

    @PostMapping("/{id}/disable")
    @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')")
    public AlertRuleResponse disable(
            @EnvPath Environment env,
            @PathVariable UUID id,
            HttpServletRequest httpRequest) {

        AlertRule rule = requireRule(id, env.id());
        AlertRule updated = withEnabled(rule, false);
        AlertRule saved = ruleRepo.save(updated);

        auditService.log("ALERT_RULE_DISABLE", AuditCategory.ALERT_RULE_CHANGE,
                id.toString(), Map.of("name", saved.name()), AuditResult.SUCCESS, httpRequest);

        return AlertRuleResponse.from(saved);
    }

    // -------------------------------------------------------------------------
    // Render Preview
    // -------------------------------------------------------------------------

    @PostMapping("/{id}/render-preview")
    @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')")
    public RenderPreviewResponse renderPreview(
            @EnvPath Environment env,
            @PathVariable UUID id,
            @RequestBody RenderPreviewRequest req) {

        AlertRule rule = requireRule(id, env.id());
        Map<String, Object> ctx = req.context();
        String title   = renderer.render(rule.notificationTitleTmpl(),   ctx);
        String message = renderer.render(rule.notificationMessageTmpl(), ctx);
        return new RenderPreviewResponse(title, message);
    }

    // -------------------------------------------------------------------------
    // Test Evaluate
    // -------------------------------------------------------------------------

    @PostMapping("/{id}/test-evaluate")
    @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')")
    @SuppressWarnings({"rawtypes", "unchecked"})
    public TestEvaluateResponse testEvaluate(
            @EnvPath Environment env,
            @PathVariable UUID id,
            @RequestBody TestEvaluateRequest req) {

        AlertRule rule = requireRule(id, env.id());
        ConditionEvaluator evaluator = evaluators.get(rule.conditionKind());
        if (evaluator == null) {
            throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
                    "No evaluator registered for condition kind: " + rule.conditionKind());
        }

        EvalContext ctx = new EvalContext(tenantId, Instant.now(clock), new TickCache());
        EvalResult result = evaluator.evaluate(rule.condition(), rule, ctx);
        return TestEvaluateResponse.from(result);
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    /**
     * Cross-field business-rule validation for {@link AlertRuleRequest}.
     *
     * <p>PER_EXCHANGE rules: re-notify and for-duration are nonsensical (each fire is its own
     * exchange — there's no "still firing" window and nothing to re-notify about). Reject 400
     * if either is non-zero.
     *
     * <p>All rules: reject 400 if both webhooks and targets are empty — such a rule can never
     * notify anyone and is a pure footgun.
     */
    private void validateBusinessRules(AlertRuleRequest req) {
        if (req.condition() instanceof ExchangeMatchCondition ex
                && ex.fireMode() == FireMode.PER_EXCHANGE) {
            if (req.reNotifyMinutes() != null && req.reNotifyMinutes() != 0) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                        "reNotifyMinutes must be 0 for PER_EXCHANGE rules (re-notify does not apply)");
            }
            if (req.forDurationSeconds() != null && req.forDurationSeconds() != 0) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                        "forDurationSeconds must be 0 for PER_EXCHANGE rules");
            }
        }
        boolean noWebhooks = req.webhooks() == null || req.webhooks().isEmpty();
        boolean noTargets  = req.targets()  == null || req.targets().isEmpty();
        if (noWebhooks && noTargets) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "rule must have at least one webhook or target — otherwise it never notifies anyone");
        }
    }

    /**
     * Validates that all attribute keys in an {@link ExchangeMatchCondition} match
     * {@code ^[a-zA-Z0-9._-]+$}. Keys are inlined into ClickHouse SQL, making this
     * a mandatory SQL-injection prevention gate.
     */
    private void validateAttributeKeys(AlertCondition condition) {
        if (condition instanceof ExchangeMatchCondition emc && emc.filter() != null) {
            for (String key : emc.filter().attributes().keySet()) {
                if (!ATTR_KEY.matcher(key).matches()) {
                    throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
                            "Invalid attribute key (must match [a-zA-Z0-9._-]+): " + key);
                }
            }
        }
    }

    /**
     * Validates that each webhook outboundConnectionId exists and is allowed in this environment.
     */
    private void validateWebhooks(List<WebhookBindingRequest> webhooks, UUID envId) {
        for (WebhookBindingRequest wb : webhooks) {
            OutboundConnection conn;
            try {
                conn = connectionService.get(wb.outboundConnectionId());
            } catch (org.springframework.web.server.ResponseStatusException ex) {
                throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
                        "outboundConnectionId not found: " + wb.outboundConnectionId());
            } catch (Exception ex) {
                throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
                        "outboundConnectionId not found: " + wb.outboundConnectionId());
            }
            if (!conn.isAllowedInEnvironment(envId)) {
                throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
                        "outboundConnection " + wb.outboundConnectionId()
                                + " is not allowed in this environment");
            }
        }
    }

    private AlertRule requireRule(UUID id, UUID envId) {
        AlertRule rule = ruleRepo.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
                        "Alert rule not found: " + id));
        if (!rule.environmentId().equals(envId)) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND,
                    "Alert rule not found in this environment: " + id);
        }
        return rule;
    }

    private AlertRule buildRule(AlertRule existing, UUID envId, AlertRuleRequest req, String userId) {
        UUID id = existing != null ? existing.id() : UUID.randomUUID();
        Instant now = Instant.now(clock);
        Instant createdAt = existing != null ? existing.createdAt() : now;
        String createdBy  = existing != null ? existing.createdBy()  : userId;
        boolean enabled   = existing != null ? existing.enabled() : true;

        List<WebhookBinding> webhooks = req.webhooks().stream()
                .map(wb -> new WebhookBinding(
                        UUID.randomUUID(),
                        wb.outboundConnectionId(),
                        wb.bodyOverride(),
                        wb.headerOverrides()))
                .toList();

        List<AlertRuleTarget> targets = req.targets() == null ? List.of() : req.targets();

        int evalInterval = req.evaluationIntervalSeconds() != null
                ? req.evaluationIntervalSeconds() : 60;
        int forDuration = req.forDurationSeconds() != null
                ? req.forDurationSeconds() : 0;
        int reNotify = req.reNotifyMinutes() != null
                ? req.reNotifyMinutes() : 0;

        String titleTmpl   = req.notificationTitleTmpl()   != null ? req.notificationTitleTmpl()   : "";
        String messageTmpl = req.notificationMessageTmpl() != null ? req.notificationMessageTmpl() : "";

        return new AlertRule(
                id, envId, req.name(), req.description(),
                req.severity(), enabled,
                req.conditionKind(), req.condition(),
                evalInterval, forDuration, reNotify,
                titleTmpl, messageTmpl,
                webhooks, targets,
                now, null, null, Map.of(),
                createdAt, createdBy, now, userId);
    }

    private AlertRule withEnabled(AlertRule r, boolean enabled) {
        Instant now = Instant.now(clock);
        return new AlertRule(
                r.id(), r.environmentId(), r.name(), r.description(),
                r.severity(), enabled, r.conditionKind(), r.condition(),
                r.evaluationIntervalSeconds(), r.forDurationSeconds(), r.reNotifyMinutes(),
                r.notificationTitleTmpl(), r.notificationMessageTmpl(),
                r.webhooks(), r.targets(),
                r.nextEvaluationAt(), r.claimedBy(), r.claimedUntil(), r.evalState(),
                r.createdAt(), r.createdBy(), now, currentUserId());
    }

    private String currentUserId() {
        var auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || auth.getName() == null) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "No authentication");
        }
        String name = auth.getName();
        return name.startsWith("user:") ? name.substring(5) : name;
    }
}
