package webhook

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"time"

	"github.com/tidbcloud/alertstore/pkg/alert"
	"github.com/tidbcloud/alertstore/pkg/classify"
	"github.com/tidbcloud/alertstore/pkg/clinic"
	"github.com/tidbcloud/alertstore/pkg/devops"
	"github.com/tidbcloud/alertstore/pkg/event"
	"github.com/tidbcloud/alertstore/pkg/instance"
	"github.com/tidbcloud/alertstore/pkg/metrics"
	"github.com/tidbcloud/alertstore/pkg/mute"
	"github.com/tidbcloud/alertstore/pkg/receipt"
	"github.com/tidbcloud/alertstore/pkg/subscription"
)

// eventDedupWindow must sit well above alertmanager's retry span (minutes)
// and well below repeat_interval (hours): redeliveries of the same
// notification collapse into one event row, repeat notifications don't.
const eventDedupWindow = 15 * time.Minute

// Handler processes incoming alertmanager webhooks.
type Handler struct {
	eventStore    *event.Store
	receiptStore  *receipt.Store
	instanceStore *instance.Store
	alertService  *alert.Service
	classifier    *classify.Classifier
	clinic        *clinic.Client
	devops        *devops.Client
	muteStore     *mute.Store
	logger        *slog.Logger

	// ownEverything ignores the per-rule canary label. See
	// alert.OwnedByAlertstore.
	ownEverything bool

	// Callbacks for the delivery layer. They only enqueue outbox tasks
	// (idempotent), so failures are safe to surface as webhook errors —
	// alertmanager's retry repairs the enqueue.
	OnAlertCreated     func(ctx context.Context, a *alert.Alert) error
	OnInstanceResolved func(ctx context.Context, a *alert.Alert, inst *instance.Instance, eventID int64, remainingFiring int) error
	OnInstanceFiring   func(ctx context.Context, a *alert.Alert, inst *instance.Instance, eventID int64, totalFiring int) error
	// OnAlertInstancesChanged fires in both directions, so the card's
	// occurrence list does not go stale on recovery the way it would if only
	// arrivals refreshed it.
	OnAlertInstancesChanged func(ctx context.Context, a *alert.Alert, eventID int64) error
	// OnRecordedEvent announces one stored observation to whoever subscribed to
	// the raw stream. Called for every event the lifecycle did not mute —
	// firing and resolved, every classification — because the questions this
	// stream answers are the ones an alert aggregates away.
	OnRecordedEvent func(ctx context.Context, ev *event.Event, attrs *event.EventAttrs, notice event.Notice) error
}

func NewHandler(
	eventStore *event.Store,
	receiptStore *receipt.Store,
	instanceStore *instance.Store,
	alertService *alert.Service,
	classifier *classify.Classifier,
	clinicClient *clinic.Client,
	devopsClient *devops.Client,
	muteStore *mute.Store,
	ownEverything bool,
	logger *slog.Logger,
) *Handler {
	return &Handler{
		ownEverything: ownEverything,
		eventStore:    eventStore,
		receiptStore:  receiptStore,
		instanceStore: instanceStore,
		alertService:  alertService,
		classifier:    classifier,
		clinic:        clinicClient,
		devops:        devopsClient,
		muteStore:     muteStore,
		logger:        logger,
	}
}

// HandleAlertmanager processes a full alertmanager webhook payload.
// All alerts are attempted; if any fail, an error is returned so the caller
// responds 5xx and alertmanager retries the payload. Retries are safe:
// event inserts dedup within eventDedupWindow, alert creation dedups on
// fingerprint, and delivery enqueues are idempotent.
func (h *Handler) HandleAlertmanager(ctx context.Context, payload *AlertmanagerPayload) error {
	rawPayload, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshal webhook receipt: %w", err)
	}
	r, err := h.receiptStore.Insert(ctx, &receipt.NewReceipt{
		GroupKey:           event.NormalizeGroupKey(payload.GroupKey),
		Receiver:           payload.Receiver,
		Status:             payload.Status,
		GroupLabels:        payload.GroupLabels,
		CommonLabels:       payload.CommonLabels,
		CommonAnnotations:  payload.CommonAnnotations,
		ExternalURL:        payload.ExternalURL,
		TruncatedAlerts:    payload.TruncatedAlerts,
		NotificationReason: payload.NotificationReason,
		RawPayload:         rawPayload,
	})
	if err != nil {
		return err
	}

	var failed int
	for i := range payload.Alerts {
		am := &payload.Alerts[i]
		if err := h.processAlert(ctx, r.ID, payload.GroupKey, am); err != nil {
			failed++
			h.logger.Error("process alert", "err", err, "fingerprint", am.Fingerprint)
			// Continue processing other alerts.
		}
	}
	if failed > 0 {
		return fmt.Errorf("%d/%d alerts failed processing", failed, len(payload.Alerts))
	}
	return nil
}

func (h *Handler) processAlert(ctx context.Context, receiptID int64, groupKey string, am *AlertmanagerAlert) error {
	// 1. Classify. The alertmanager rule carries the intended response tier in
	// the `response_class` label; fall back to deriving it from severity when
	// the rule hasn't set it yet.
	rawClass := alert.ResponseClass(am.Labels)
	if rawClass == "" {
		rawClass = classificationFromSeverity(am.Labels["severity"])
	}

	clusterID := alert.Fact(alert.FactClusterID, am.Labels)
	tenantID := alert.Fact(alert.FactTenantID, am.Labels)
	region := alert.Fact(alert.FactRegion, am.Labels)
	startsAt := am.StartsAt
	if startsAt.IsZero() {
		startsAt = time.Now().UTC()
	}

	var cluster clinic.Cluster
	var clinicURL string
	if h.clinic != nil {
		clinicURL = h.clinic.PortalURL(clusterID)
		if h.clinic.ClusterLookupEnabled() && clusterID != "" {
			var err error
			cluster, err = h.clinic.GetCluster(ctx, clusterID)
			if err != nil {
				// Cluster enrichment is useful context, not an ingestion gate.
				h.logger.Warn("enrich alert from clinic", "err", err, "cluster_id", clusterID)
			} else if tenantID == "" {
				tenantID = cluster.TenantID
			}
		}
	}

	// Whose account this is, which only the ops portal knows. It runs after
	// clinic because clinic is what recovers the tenant ID for an alert whose
	// labels carry only the cluster, and enrichment with no tenant to ask about
	// is no enrichment at all.
	var tenant devops.Tenant
	if h.devops.Enabled() && tenantID != "" {
		var err error
		tenant, err = h.devops.GetTenant(ctx, tenantID)
		if err != nil {
			// Fail-open, like clinic above: a header row is not worth an alert.
			h.logger.Warn("enrich alert from devops", "err", err, "tenant_id", tenantID)
		}
	}

	visibility := alert.NormalizeVisibility(am.Labels)
	result := h.classifier.Classify(
		ctx,
		am.Labels["alertname"],
		rawClass,
		visibility,
		tenantID,
		clusterID,
		startsAt,
	)

	// 2. Build event attrs.
	attrs := &event.EventAttrs{
		Alertname:              am.Labels["alertname"],
		Classification:         result.Classification,
		OriginalClassification: result.Original,
		ClusterID:              clusterID,
		ClusterName:            cluster.Name,
		ClusterVersion:         cluster.Version,
		DeployType:             cluster.DeployType,
		ClinicURL:              clinicURL,
		TenantID:               tenantID,
		TenantName:             tenant.Name,
		BizRegion:              tenant.BizRegion,
		TenantKind:             tenant.Kind,
		CustomerType:           tenant.Segmentation,
		Region:                 region,
		Component:              alert.Fact(alert.FactComponent, am.Labels),
		Visibility:             visibility,
		Labels:                 am.Labels,
		Annotations:            am.Annotations,
		GeneratorURL:           am.GeneratorURL,
	}

	attrsJSON, err := json.Marshal(attrs)
	if err != nil {
		return fmt.Errorf("marshal event attrs: %w", err)
	}

	// 3. Resolve the occurrence, then store an immutable observation.
	snapshot := &instance.Snapshot{
		Fingerprint:    am.Fingerprint,
		Alertname:      attrs.Alertname,
		Classification: result.Classification,
		Labels:         am.Labels,
		Annotations:    am.Annotations,
		GeneratorURL:   am.GeneratorURL,
		StartsAt:       startsAt,
	}
	inst, _, err := h.instanceStore.GetOrCreate(ctx, snapshot)
	if err != nil {
		return fmt.Errorf("get or create instance: %w", err)
	}

	var endsAt *time.Time
	if !am.EndsAt.IsZero() && am.Status == "resolved" {
		endsAt = &am.EndsAt
	}

	ev := &event.Event{
		ReceiptID:   &receiptID,
		InstanceID:  &inst.ID,
		GroupKey:    event.NormalizeGroupKey(groupKey),
		Fingerprint: am.Fingerprint,
		Status:      am.Status,
		Attrs:       attrsJSON,
		StartsAt:    startsAt,
		EndsAt:      endsAt,
	}
	inserted, err := h.eventStore.InsertDedup(ctx, ev, eventDedupWindow)
	if err != nil {
		return fmt.Errorf("store event: %w", err)
	}
	if inserted {
		metrics.EventsIngested.WithLabelValues(am.Status, result.Classification).Inc()
	} else {
		metrics.EventsDeduplicated.Inc()
	}

	observationType, applied, err := h.instanceStore.ApplyObservation(
		ctx, ev.ID, inst.ID, am.Status, endsAt, ev.ReceivedAt, snapshot,
	)
	if err != nil {
		return fmt.Errorf("apply instance observation: %w", err)
	}

	// 4. Run the alert lifecycle, then announce the observation itself to
	// whoever subscribed to the raw stream. Announcing last is what lets the
	// notice name the alert this observation belongs to: for a repeat firing
	// the binding exists already, but the occurrence that opens or joins an
	// alert is bound by the routing below.
	outcome, err := h.route(ctx, inst, ev, am, attrs, result, applied)
	if err != nil {
		return err
	}
	return h.notifyRecorded(ctx, ev, attrs, result.Classification, observationType, outcome)
}

// routeOutcome is what the alert lifecycle did with an observation: the alert
// it landed on, or the mute rule that stopped one from opening. Both explain
// the same thing to a reader of the raw stream — why this observation did or
// did not become somebody's problem — and neither is recoverable afterwards
// without repeating the work.
type routeOutcome struct {
	alert *alert.Alert
	muted *mute.Rule
}

// route applies the human-alert lifecycle to one observation. It returns
// rather than delivers, so a caller can act on what happened.
func (h *Handler) route(
	ctx context.Context,
	inst *instance.Instance,
	ev *event.Event,
	am *AlertmanagerAlert,
	attrs *event.EventAttrs,
	result classify.Result,
	applied bool,
) (routeOutcome, error) {
	if !applied && am.Status == instance.StateResolved {
		// The state/audit projection already committed, but a previous attempt
		// may have failed before enqueueing the idempotent resolved delivery.
		a, err := h.handleResolved(ctx, inst, ev.ID, false)
		return routeOutcome{alert: a}, err
	}

	// A resolved observation always updates an existing bound Alert regardless
	// of how current classification rules would classify the same labels.
	if am.Status == instance.StateResolved {
		a, err := h.handleResolved(ctx, inst, ev.ID, true)
		return routeOutcome{alert: a}, err
	}
	if am.Status != instance.StateFiring {
		h.logger.Warn("unknown alert status", "status", am.Status)
		return routeOutcome{}, nil
	}

	// Logging instances remain queryable but do not create a human Alert.
	if result.Classification == alert.ClassLogging {
		return routeOutcome{}, nil
	}

	// Mute is only reachable from here, so it only ever suppresses a response:
	// an alert, and everything downstream of one. It deliberately does not
	// reach the raw observation stream.
	//
	// A rule is written from inside one alert's topic and scoped to an
	// alertname and cluster — never to a channel — so letting it silence the
	// stream would mean an on-caller quieting their own page also blinds
	// whichever other group subscribed to those observations, without either
	// side knowing. For customer-facing alerts it would be worse than blind:
	// muting cannot stop cloud_server telling the tenant, so the customer would
	// keep being notified while our copy went quiet — the exact divergence the
	// notice stream exists to prevent.
	//
	// A stream that is too noisy is a routing problem, and subscriptions are
	// the knob for it: per channel, owned by whoever receives them.
	muteRule, err := h.muteStore.IsMuted(ctx, subscription.TargetFromEvent(attrs))
	if err != nil {
		h.logger.Error("check mute", "err", err)
	}
	if muteRule != nil {
		metrics.EventsMuted.Inc()
		h.logger.Info("event muted by rule", "rule_id", muteRule.ID, "fingerprint", am.Fingerprint)
		return routeOutcome{muted: muteRule}, nil
	}

	a, err := h.handleFiring(ctx, inst, ev.ID, am, attrs, result)
	return routeOutcome{alert: a}, err
}

// notifyRecorded announces one stored observation to whoever subscribed to the
// raw stream.
//
// Every classification reaches here, not only the ones that open no alert. An
// alert is an aggregate: it collapses an occurrence's repeat firings into
// nothing at all — a re-firing of an already-bound occurrence produces no
// reply and no card refresh — which is correct for someone being paged and
// useless for someone asking whether a page has been re-firing hourly for
// three days. That question is what this stream answers, and it is only
// answerable at the granularity Alertmanager actually notifies at.
//
// A muted observation is announced like any other, and says it was muted. Mute
// suppresses the response, not the record — "stored as event only" is what a
// mute rule has always promised — so this is the one surface that can show what
// an active rule is currently hiding. Silencing it here would make mute
// unobservable by the very stream it spares.
func (h *Handler) notifyRecorded(
	ctx context.Context,
	ev *event.Event,
	attrs *event.EventAttrs,
	classification, observationType string,
	outcome routeOutcome,
) error {
	if h.OnRecordedEvent == nil {
		return nil
	}

	notice := event.Notice{
		Classification:  classification,
		ObservationType: observationType,
	}
	if outcome.alert != nil {
		notice.AlertID = outcome.alert.ID
	}
	if outcome.muted != nil {
		notice.MuteRuleID = outcome.muted.ID
		notice.MuteReason = outcome.muted.Reason
	}
	if err := h.OnRecordedEvent(ctx, ev, attrs, notice); err != nil {
		return fmt.Errorf("enqueue recorded-event notice: %w", err)
	}
	return nil
}

func (h *Handler) handleFiring(ctx context.Context, inst *instance.Instance, eventID int64, am *AlertmanagerAlert, eventAttrs *event.EventAttrs, classResult classify.Result) (*alert.Alert, error) {
	alertAttrs := &alert.AlertAttrs{
		Fingerprint:    am.Fingerprint,
		Alertname:      eventAttrs.Alertname,
		Classification: classResult.Classification,
		ClusterID:      eventAttrs.ClusterID,
		ClusterName:    eventAttrs.ClusterName,
		ClusterVersion: eventAttrs.ClusterVersion,
		DeployType:     eventAttrs.DeployType,
		ClinicURL:      eventAttrs.ClinicURL,
		TenantID:       eventAttrs.TenantID,
		TenantName:     eventAttrs.TenantName,
		BizRegion:      eventAttrs.BizRegion,
		TenantKind:     eventAttrs.TenantKind,
		CustomerType:   eventAttrs.CustomerType,
		Region:         eventAttrs.Region,
		Component:      eventAttrs.Component,
		Namespace:      alert.Fact(alert.FactNamespace, am.Labels),
		BizType:        alert.Fact(alert.FactBizType, am.Labels),
		Environment:    alert.Fact(alert.FactEnvironment, am.Labels),
		Summary:        am.Annotations["summary"],
		Message:        am.Annotations["message"],
		Description:    am.Annotations["description"],
		Labels:         am.Labels,
		Annotations:    am.Annotations,
		GeneratorURL:   am.GeneratorURL,
		// Decided at ingest and frozen on the alert; see alert.OwnedByAlertstore.
		Owned: alert.OwnedByAlertstore(am.Labels, h.ownEverything),
	}

	// Owner resolution runs in the delivery worker, off this path: see
	// delivery.Worker.resolveOwner.
	assignedTo := ""

	a, outcome, err := h.alertService.HandleFiringEvent(ctx, alertAttrs, inst.ID, assignedTo, nil, nil)
	if err != nil {
		return nil, fmt.Errorf("handle firing: %w", err)
	}

	if outcome.AlertCreated {
		metrics.AlertsCreated.WithLabelValues(classResult.Classification).Inc()
	} else {
		metrics.AlertsDeduplicated.Inc()
	}

	// An occurrence that joined an already-posted alert is announced in the
	// topic, mirroring handleResolved. The card cannot carry it: it is
	// immutable, and it describes whichever occurrence opened the alert.
	if outcome.InstanceAppended && h.OnInstanceFiring != nil {
		current, err := h.instanceStore.GetByID(ctx, inst.ID)
		if err != nil {
			return nil, fmt.Errorf("load appended instance: %w", err)
		}
		firing, err := h.instanceStore.CountFiringByAlertID(ctx, a.ID)
		if err != nil {
			return nil, fmt.Errorf("count firing instances: %w", err)
		}
		if err := h.OnInstanceFiring(ctx, a, current, eventID, firing); err != nil {
			return nil, fmt.Errorf("enqueue instance firing reply: %w", err)
		}
	}

	if outcome.InstanceAppended && h.OnAlertInstancesChanged != nil {
		if err := h.OnAlertInstancesChanged(ctx, a, eventID); err != nil {
			return nil, fmt.Errorf("enqueue card refresh: %w", err)
		}
	}

	// Called for both new and deduped alerts: the enqueue is idempotent,
	// so re-firing events repair a delivery enqueue that was lost earlier.
	if h.OnAlertCreated != nil {
		if err := h.OnAlertCreated(ctx, a); err != nil {
			return nil, fmt.Errorf("enqueue delivery: %w", err)
		}
	}

	return a, nil
}

// handleResolved projects a recovery onto the alert the occurrence is bound to,
// and returns that alert so the caller can name it. A nil alert is the ordinary
// case for an observation nobody responds to.
func (h *Handler) handleResolved(ctx context.Context, inst *instance.Instance, eventID int64, recordAudit bool) (*alert.Alert, error) {
	if recordAudit {
		if err := h.alertService.HandleResolvedInstance(ctx, inst.ID, inst.Fingerprint); err != nil {
			return nil, fmt.Errorf("handle resolved: %w", err)
		}
	}

	a, err := h.alertService.FindOpenByInstanceID(ctx, inst.ID)
	if err != nil {
		return nil, fmt.Errorf("find open alert for resolved event: %w", err)
	}
	if a == nil {
		return nil, nil
	}

	// Notify in the IM topic about this occurrence, not a blanket "alert
	// recovered": other occurrences in the aggregate may still be firing.
	if h.OnInstanceResolved != nil {
		current, err := h.instanceStore.GetByID(ctx, inst.ID)
		if err != nil {
			return nil, fmt.Errorf("load resolved instance: %w", err)
		}
		remaining, err := h.instanceStore.CountFiringByAlertID(ctx, a.ID)
		if err != nil {
			return nil, fmt.Errorf("count remaining firing instances: %w", err)
		}
		if err := h.OnInstanceResolved(ctx, a, current, eventID, remaining); err != nil {
			return nil, fmt.Errorf("enqueue instance resolved reply: %w", err)
		}
		if h.OnAlertInstancesChanged != nil {
			if err := h.OnAlertInstancesChanged(ctx, a, eventID); err != nil {
				return nil, fmt.Errorf("enqueue card refresh: %w", err)
			}
		}
	}

	return a, nil
}

// classificationFromSeverity maps the legacy severity label to a classification.
// Remove once alertmanager rules include a classification label.
func classificationFromSeverity(severity string) string {
	switch severity {
	case "critical":
		return alert.ClassPage
	case "major":
		return alert.ClassTicket
	default:
		return alert.ClassLogging
	}
}
