package subscription

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"strings"

	"github.com/tidbcloud/alertstore/pkg/alert"
	"github.com/tidbcloud/alertstore/pkg/instance"
)

func ValidateMode(mode string) error {
	switch mode {
	case ModeTopicForward, ModeCardOnly, ModeEvent, ModeCustomer:
		return nil
	default:
		return fmt.Errorf("unsupported subscription mode %q", mode)
	}
}

// modeClassifications lists the classifications a mode can actually deliver.
// Naming one a mode cannot carry has no useful reading — it would store a
// subscription that looks configured and delivers nothing — so it is refused at
// write time rather than discovered by its silence.
//
// The alert modes deliver an alert, which only page and ticket ever open.
// ModeEvent takes all three: it delivers observations, and every alert is made
// of those. Narrowing it to logging would have left the most useful question it
// can answer unaskable — whether a page has been re-firing, which the alert
// itself aggregates into silence.
//
// ModeCustomer is the exception, and not by choice: a customer-facing alert is
// forced to logging by the classifier, so it is the only value one can carry.
var modeClassifications = map[string][]string{
	ModeTopicForward: {alert.ClassPage, alert.ClassTicket},
	ModeCardOnly:     {alert.ClassPage, alert.ClassTicket},
	ModeEvent:        {alert.ClassPage, alert.ClassTicket, alert.ClassLogging},
	ModeCustomer:     {alert.ClassLogging},
}

// ValidateFilter checks the filter's shape and, since the two are only
// meaningful together, its consistency with the delivery mode.
func ValidateFilter(mode string, raw json.RawMessage) error {
	filter, err := decodeFilter(raw)
	if err != nil {
		return err
	}

	deliverable := modeClassifications[mode]
	if c := filter.Classification; c != nil {
		for _, class := range c.Values {
			if !contains(deliverable, class) {
				return fmt.Errorf("mode %s cannot deliver classification %s: it delivers %s",
					mode, class, strings.Join(deliverable, "/"))
			}
		}
	}

	if ot := filter.ObservationType; ot != nil && len(ot.Values) > 0 {
		if mode != ModeEvent && mode != ModeCustomer {
			return fmt.Errorf("observation_type can only be used with %s or %s mode",
				ModeEvent, ModeCustomer)
		}
	}
	return nil
}

// ValidateMuteFilter checks a filter with no mode behind it. A mute rule
// narrows over the same fields a subscription does — it is the same type — but
// suppresses rather than delivers, so there is no mode for its classifications
// to be consistent with.
//
// Shape is checked here rather than left to the matcher because the matcher is
// forgiving by design: an unreadable filter is skipped, and a filter that names
// nothing matches everything. A typo would otherwise be stored as a rule that
// silently mutes the fleet.
func ValidateMuteFilter(raw json.RawMessage) error {
	filter, err := decodeFilter(raw)
	if err != nil {
		return err
	}

	// Mute reaches page and ticket, nothing else: its check runs on the path
	// that opens an alert, after the logging short-circuit
	// (webhook.Handler.route), so naming logging alone matches nothing and
	// naming it beside page widens nothing.
	//
	// This once stood, on the reasoning that a rule has no mode and so nothing
	// to be consistent with. It is refused now for the reason the tenant-kind
	// check above refuses `customer` — a no-op that looks configured is the
	// silence these checks exist to prevent. Nothing stored is invalidated:
	// mute filters are validated on create only, and a rule carries an expiry.
	if c := filter.Classification; c != nil {
		for _, class := range c.Values {
			if class == alert.ClassLogging {
				return fmt.Errorf("a mute rule cannot match classification %s: it suppresses alerts, and a logging observation opens none",
					alert.ClassLogging)
			}
		}
	}
	if ot := filter.ObservationType; ot != nil && len(ot.Values) > 0 {
		return fmt.Errorf("a mute rule cannot filter by observation_type: it suppresses alerts, not individual observations")
	}
	return nil
}

// decodeFilter parses a filter and checks everything about it that holds
// independently of how it will be used.
func decodeFilter(raw json.RawMessage) (SubscriptionFilter, error) {
	var filter SubscriptionFilter

	trimmed := bytes.TrimSpace(raw)
	if len(trimmed) == 0 || trimmed[0] != '{' {
		return filter, fmt.Errorf("filter must be a JSON object")
	}

	decoder := json.NewDecoder(bytes.NewReader(trimmed))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&filter); err != nil {
		return filter, fmt.Errorf("invalid subscription filter: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		if err == nil {
			return filter, fmt.Errorf("invalid subscription filter: multiple JSON values")
		}
		return filter, fmt.Errorf("invalid subscription filter: %w", err)
	}

	// A label entry naming no label, or naming one without saying which values
	// count, would widen the filter to every alert carrying that label — the
	// opposite of what someone adding a constraint is asking for.
	for name, m := range filter.Labels {
		if strings.TrimSpace(name) == "" {
			return filter, fmt.Errorf("label filter needs a label name")
		}
		if m.empty() {
			return filter, fmt.Errorf("label filter %q needs at least one value", name)
		}
		for _, value := range m.Values {
			if strings.TrimSpace(value) == "" {
				return filter, fmt.Errorf("label filter %q has an empty value", name)
			}
		}
	}

	// These enums are one of a fixed set of values, so they equal one or they do
	// not — no operator over them means anything a plain value could not state.
	for _, name := range []string{"classification", "observation_type", "tenant_kind", "customer_type", "biz_region"} {
		if m := filter.fields()[name]; m != nil && m.Op != OpEq {
			return filter, fmt.Errorf("%s supports only =: it takes one of a fixed set of values", name)
		}
	}

	// An id is an exact value: `=` and `!=` are both meaningful, but a regex over
	// one would only obscure what it is.
	for _, name := range []string{"cluster_id", "tenant_id"} {
		if err := noRegexOp(name, filter.fields()[name]); err != nil {
			return filter, err
		}
	}

	if c := filter.Classification; c != nil {
		for _, class := range c.Values {
			switch class {
			case alert.ClassPage, alert.ClassTicket, alert.ClassLogging:
			default:
				return filter, fmt.Errorf("unsupported classification %q", class)
			}
		}
	}

	if ot := filter.ObservationType; ot != nil {
		for _, v := range ot.Values {
			switch v {
			case instance.ObservationFirstFiring, instance.ObservationRepeatFiring, instance.ObservationResolved:
			default:
				return filter, fmt.Errorf("unsupported observation_type %q", v)
			}
		}
	}

	// A tenant is ours or a customer's, and nothing else — the ops portal has
	// exactly these two kinds. A rule filtering on "customer" or "prod" would
	// store as configured and match nothing, the same silence the classification
	// check above exists to prevent.
	//
	// biz_region is deliberately not checked against a list: that enum belongs
	// to the ops portal, which can add a region without telling alertstore, and
	// refusing a legitimate new one would be worse than accepting a typo.
	if k := filter.TenantKind; k != nil {
		for _, kind := range k.Values {
			if !strings.EqualFold(kind, alert.TenantKindInternal) && !strings.EqualFold(kind, alert.TenantKindExternal) {
				return filter, fmt.Errorf("unsupported tenant kind %q: it is %s or %s",
					kind, alert.TenantKindInternal, alert.TenantKindExternal)
			}
		}
	}
	return filter, nil
}

// noRegexOp refuses a regex operator on a field where a pattern has no place,
// naming the field so the message lands where the mistake was made.
func noRegexOp(field string, m *FieldMatch) error {
	if m != nil && m.isRegexOp() {
		return fmt.Errorf("%s does not support regex", field)
	}
	return nil
}

func contains(list []string, value string) bool {
	for _, v := range list {
		if v == value {
			return true
		}
	}
	return false
}
