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

import com.cameleer.server.app.alerting.config.AlertingProperties;
import com.cameleer.server.app.outbound.crypto.SecretCipher;
import com.cameleer.server.core.alerting.AlertInstance;
import com.cameleer.server.core.alerting.AlertNotification;
import com.cameleer.server.core.alerting.AlertRule;
import com.cameleer.server.core.alerting.NotificationStatus;
import com.cameleer.server.core.alerting.WebhookBinding;
import com.cameleer.server.core.http.OutboundHttpClientFactory;
import com.cameleer.server.core.http.OutboundHttpRequestContext;
import com.cameleer.server.core.outbound.OutboundConnection;
import com.cameleer.server.core.outbound.OutboundMethod;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpPatch;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Renders, signs, and dispatches webhook notifications over HTTP.
 * <p>
 * Classification:
 * <ul>
 *   <li>2xx → {@link NotificationStatus#DELIVERED}</li>
 *   <li>4xx → {@link NotificationStatus#FAILED} (retry won't help)</li>
 *   <li>5xx / network / timeout → {@code null} status (caller retries up to max attempts)</li>
 * </ul>
 */
@Component
public class WebhookDispatcher {

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

    /** baseDelay that callers multiply by attempt count: 30s, 60s, 90s, … */
    static final Duration BASE_RETRY_DELAY = Duration.ofSeconds(30);

    private static final int SNIPPET_LIMIT = 512;
    private static final String DEFAULT_CONTENT_TYPE = "application/json";

    private final OutboundHttpClientFactory clientFactory;
    private final SecretCipher secretCipher;
    private final MustacheRenderer renderer;
    private final AlertingProperties props;
    private final ObjectMapper objectMapper;

    public WebhookDispatcher(OutboundHttpClientFactory clientFactory,
                             SecretCipher secretCipher,
                             MustacheRenderer renderer,
                             AlertingProperties props,
                             ObjectMapper objectMapper) {
        this.clientFactory = clientFactory;
        this.secretCipher  = secretCipher;
        this.renderer      = renderer;
        this.props         = props;
        this.objectMapper  = objectMapper;
    }

    public record Outcome(
            NotificationStatus status,
            int httpStatus,
            String snippet,
            Duration retryAfter) {}

    /**
     * Dispatch a single webhook notification.
     *
     * @param notif    the outbox record (contains webhookId used to find per-rule overrides)
     * @param rule     the alert rule (may be null when rule was deleted)
     * @param instance the alert instance
     * @param conn     the resolved outbound connection
     * @param context  the Mustache rendering context
     */
    public Outcome dispatch(AlertNotification notif,
                            AlertRule rule,
                            AlertInstance instance,
                            OutboundConnection conn,
                            Map<String, Object> context) {
        try {
            // 1. Determine per-binding overrides
            WebhookBinding binding = findBinding(rule, notif);

            // 2. Render URL
            String url = renderer.render(conn.url(), context);

            // 3. Build body
            String body = buildBody(conn, binding, context);

            // 4. Build headers
            Map<String, String> headers = buildHeaders(conn, binding, context);

            // 5. HMAC sign if configured
            if (conn.hmacSecretCiphertext() != null) {
                String secret = secretCipher.decrypt(conn.hmacSecretCiphertext());
                String sig = new HmacSigner().sign(secret, body.getBytes(StandardCharsets.UTF_8));
                headers.put("X-Cameleer-Signature", sig);
            }

            // 6. Build HTTP request
            Duration timeout = Duration.ofMillis(props.effectiveWebhookTimeoutMs());
            OutboundHttpRequestContext ctx = new OutboundHttpRequestContext(
                    conn.tlsTrustMode(), conn.tlsCaPemPaths(), timeout, timeout);

            var client = clientFactory.clientFor(ctx);
            HttpUriRequestBase request = buildRequest(conn.method(), url);
            for (var e : headers.entrySet()) {
                request.setHeader(e.getKey(), e.getValue());
            }
            request.setEntity(new StringEntity(body, StandardCharsets.UTF_8));

            // 7. Execute and classify
            try (var response = client.execute(request)) {
                int code = response.getCode();
                String snippet = snippet(response.getEntity() != null
                        ? EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)
                        : "");

                if (code >= 200 && code < 300) {
                    return new Outcome(NotificationStatus.DELIVERED, code, snippet, null);
                } else if (code >= 400 && code < 500) {
                    return new Outcome(NotificationStatus.FAILED, code, snippet, null);
                } else {
                    return new Outcome(null, code, snippet, BASE_RETRY_DELAY);
                }
            }

        } catch (Exception e) {
            log.warn("WebhookDispatcher: network/timeout error dispatching notification {}: {}",
                    notif.id(), e.getMessage());
            return new Outcome(null, 0, snippet(e.getMessage()), BASE_RETRY_DELAY);
        }
    }

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

    private WebhookBinding findBinding(AlertRule rule, AlertNotification notif) {
        if (rule == null || notif.webhookId() == null) return null;
        return rule.webhooks().stream()
                .filter(w -> w.id().equals(notif.webhookId()))
                .findFirst()
                .orElse(null);
    }

    private String buildBody(OutboundConnection conn, WebhookBinding binding, Map<String, Object> context) {
        // Priority: per-binding override > connection default > built-in JSON envelope
        String tmpl = null;
        if (binding != null && binding.bodyOverride() != null) {
            tmpl = binding.bodyOverride();
        } else if (conn.defaultBodyTmpl() != null) {
            tmpl = conn.defaultBodyTmpl();
        }

        if (tmpl != null) {
            return renderer.render(tmpl, context);
        }

        // Built-in default: serialize the entire context map as JSON
        try {
            return objectMapper.writeValueAsString(context);
        } catch (Exception e) {
            log.warn("WebhookDispatcher: failed to serialize context as JSON, using empty object", e);
            return "{}";
        }
    }

    private Map<String, String> buildHeaders(OutboundConnection conn, WebhookBinding binding,
                                             Map<String, Object> context) {
        Map<String, String> headers = new LinkedHashMap<>();

        // Default content-type
        headers.put("Content-Type", DEFAULT_CONTENT_TYPE);

        // Connection-level default headers (keys are literal, values are Mustache-rendered)
        for (var e : conn.defaultHeaders().entrySet()) {
            headers.put(e.getKey(), renderer.render(e.getValue(), context));
        }

        // Per-binding overrides (also Mustache-rendered values)
        if (binding != null) {
            for (var e : binding.headerOverrides().entrySet()) {
                headers.put(e.getKey(), renderer.render(e.getValue(), context));
            }
        }

        return headers;
    }

    private HttpUriRequestBase buildRequest(OutboundMethod method, String url) {
        if (method == null) method = OutboundMethod.POST;
        return switch (method) {
            case PUT   -> new HttpPut(url);
            case PATCH -> new HttpPatch(url);
            default    -> new HttpPost(url);
        };
    }

    private String snippet(String text) {
        if (text == null) return "";
        return text.length() <= SNIPPET_LIMIT ? text : text.substring(0, SNIPPET_LIMIT);
    }
}
