package main

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"strings"

	"git.dev.alexdunmow.com/block/calcomblock/internal/helpers"
)

// Notifier matches the existing CMS notifier callback signature
// (handlers/webhook_ingestion.go:34). The wiring lives in
// internal/server/server.go where the notification service is available.
type Notifier func(ctx context.Context, severity, title, message, uniqueKey string)

// notifier is a package-level callback set once at server startup via
// SetNotifyFn. nil-safe: dispatch() checks before invoking.
var notifier Notifier

// SetNotifyFn wires the CMS notification service into the cal.com webhook
// dispatcher. Called exactly once at server startup from server.go.
func SetNotifyFn(fn Notifier) {
	notifier = fn
}

// WebhookHandler accepts Cal.com webhook events, verifies their HMAC signature
// against the configured secret, and fans them out via the package-level
// notifier. Mirrors the Stripe pattern of returning 200 on app-level errors so
// the sender does not flood retries with junk.
type WebhookHandler struct {
	settings *SettingsManager
	logger   *slog.Logger
}

// NewWebhookHandler builds a webhook handler bound to the given settings
// store. The logger is used for security-relevant events (invalid signatures,
// missing configuration); body content is never logged.
func NewWebhookHandler(settings *SettingsManager, logger *slog.Logger) *WebhookHandler {
	if logger == nil {
		logger = slog.Default()
	}
	return &WebhookHandler{settings: settings, logger: logger}
}

// Handle is the HTTP handler for POST /webhook.
func (h *WebhookHandler) Handle(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	sourceIP := helpers.GetRealIP(r)

	body, err := io.ReadAll(io.LimitReader(r.Body, 10*1024*1024))
	if err != nil {
		h.logger.Warn("calcom webhook: read body", "error", err, "ip", sourceIP)
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	secret, err := h.settings.GetWebhookSecret(ctx)
	if err != nil {
		h.logger.Warn("calcom webhook: load secret", "error", err, "ip", sourceIP)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	if secret == "" {
		h.logger.Warn("calcom webhook: not configured", "ip", sourceIP)
		http.Error(w, "webhook not configured", http.StatusServiceUnavailable)
		return
	}

	sig := r.Header.Get("X-Cal-Signature-256")
	if sig == "" || !verifyCalcomSignature(body, sig, secret) {
		h.logger.Warn("calcom webhook: invalid signature", "ip", sourceIP)
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}

	var evt WebhookEvent
	if err := json.Unmarshal(body, &evt); err != nil {
		// Match the stripe pattern: ack invalid JSON so Cal.com doesn't keep retrying.
		h.logger.Warn("calcom webhook: bad json", "error", err)
		w.WriteHeader(http.StatusOK)
		return
	}

	// Run dispatch + last-event recording inside a recover so a panicking
	// notifier or settings store can't bubble a 500 to Cal.com and trigger a
	// retry storm. Cal.com retries 5xx responses; the Stripe pattern in the
	// rest of the CMS is to always ack 200 once the signature has verified.
	func() {
		defer func() {
			if rec := recover(); rec != nil {
				h.logger.Error("calcom webhook: panic in dispatch", "panic", rec)
			}
		}()
		h.dispatch(ctx, evt)
		if err := h.settings.UpdateLastEvent(ctx, evt.TriggerEvent, evt.CreatedAt); err != nil {
			h.logger.Warn("calcom webhook: update last event", "error", err)
		}
	}()
	w.WriteHeader(http.StatusOK)
}

// dispatch fans the parsed event out to the package notifier. Unknown trigger
// events are silently ignored — Cal.com adds new event types over time and we
// don't want noisy notifications for events we haven't taught the plugin yet.
func (h *WebhookHandler) dispatch(ctx context.Context, evt WebhookEvent) {
	if notifier == nil {
		return
	}

	attendeeName := ""
	if len(evt.Payload.Attendees) > 0 {
		attendeeName = evt.Payload.Attendees[0].Name
	}
	eventTitle := evt.Payload.EventType.Title
	start := evt.Payload.StartTime
	uniqueKey := fmt.Sprintf("calcom:%s:%s", evt.TriggerEvent, evt.Payload.UID)

	var title, message string
	switch evt.TriggerEvent {
	case "BOOKING_CREATED":
		title = "New booking"
		message = fmt.Sprintf("%s booked %s at %s", attendeeName, eventTitle, start)
	case "BOOKING_RESCHEDULED":
		title = "Booking rescheduled"
		message = fmt.Sprintf("%s moved %s to %s", attendeeName, eventTitle, start)
	case "BOOKING_CANCELLED":
		title = "Booking cancelled"
		message = fmt.Sprintf("%s cancelled %s at %s", attendeeName, eventTitle, start)
	default:
		return
	}
	notifier(ctx, "info", title, message, uniqueKey)
}

// verifyCalcomSignature checks a Cal.com webhook signature. Accepts both the
// canonical "sha256=<hex>" and bare "<hex>" formats to match the established
// in-house pattern from handlers/webhook_ingestion.go:592. All comparisons use
// constant-time hmac.Equal to avoid timing-leak side channels.
func verifyCalcomSignature(payload []byte, signature, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(payload)
	expectedHex := hex.EncodeToString(mac.Sum(nil))
	expectedSig := "sha256=" + expectedHex

	sig := strings.TrimSpace(signature)
	if hmac.Equal([]byte(expectedSig), []byte(sig)) {
		return true
	}
	if hmac.Equal([]byte(expectedHex), []byte(sig)) {
		return true
	}
	return false
}
