package alert

import (
	"encoding/json"
	"strings"
	"time"

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

// State constants. The lifecycle is three states: open, acked, resolved.
// "Resolved" is the terminal state for every closing path — human close,
// self-recovery, noise dismissal, and automatic expiry. The resolution field
// (handled / self_recovered / noise / expired) records how it got there.
const (
	StateOpen     = "open"
	StateAcked    = "acked"
	StateResolved = "resolved"
)

// Classification constants (from alertmanager rule labels).
const (
	ClassPage    = "page"
	ClassTicket  = "ticket"
	ClassLogging = "logging"
)

// Tenant kinds, as the DevOps ops portal spells them: an account is ours or a
// customer's, and there is no third answer. Stored lowercased by ingestion, so
// everything comparing against these compares against one spelling.
const (
	TenantKindInternal = "internal"
	TenantKindExternal = "external"
)

// InternalTenant reports whether the alert belongs to one of our own accounts
// rather than a customer's. Empty is not internal: an alert whose tenant the
// ops portal could not be asked about — no tenant label, an unnumbered one, a
// lookup that failed — is treated as a customer's, which is the assumption that
// fails safe.
func (a *AlertAttrs) InternalTenant() bool {
	return strings.EqualFold(strings.TrimSpace(a.TenantKind), TenantKindInternal)
}

// SourceIssueTracker marks a transition that arrived from Jira. Load-bearing:
// the Jira mirror suppresses its own writes for these, so it does not echo a
// change back to where it came from.
const SourceIssueTracker = "issue_tracker"

// ResponseClassLabel is the alert rule label naming the response a rule asks
// for: page, ticket or logging. It is also the ownership signal — a rule that
// names its response tier is one alertstore answers, and jiralert reads the
// same label to stop responding to it.
//
// One label, because the two questions turned out to be the same one asked
// twice: a rule that has been written to say what response it wants is a rule
// whose response alertstore is running. A second label meant two things to keep
// in step, and a rule carrying one but not the other was answered twice or not
// at all.
//
// A constant, not a setting: a configurable name would put a cross-repository
// contract behind a typo.
const ResponseClassLabel = "response_class"

// OwnedByAlertstore reports whether alertstore is the primary responder for an
// alert, or merely recording jiralert's copy. A shadow alert is stored and
// queryable but gets no owner, no subscription forward and no Jira issue, since
// jiralert and jiraport are still doing all three. See docs/design.md.
//
// A rule hands itself back by dropping the label, which is also how it stops
// declaring a tier: the two go together now, and a rule that names no response
// is not one this service is running. The severity fallback still classifies
// such an alert, so it is recorded and visible — as jiralert's copy.
func OwnedByAlertstore(labels map[string]string, ownEverything bool) bool {
	if ownEverything {
		return true
	}
	return ResponseClass(labels) != ""
}

// ResponseClass returns the response tier a rule declares, empty when it
// declares none. The single reader of the label, so ownership and
// classification cannot come to different conclusions about what it says.
func ResponseClass(labels map[string]string) string {
	return strings.TrimSpace(labels[ResponseClassLabel])
}

// Resolution constants. Four values, because a resolution is only worth
// recording if different people close the same alert the same way — a wider
// taxonomy (this set began as seven values) had boundaries nobody could pick
// consistently, which made the quality metrics built on it random. Each value
// answers the two questions those metrics ask: should this alert have fired
// (noise rate), and did it need a human (self-recovery rate).
//
// self_recovered is the resolver's verdict that the problem went away on its
// own — not that Alertmanager sent resolved. The two diverge on rate-of-change
// alerts (a p999-increase rule always recovers once the value plateaus), which
// is why the monitor signal lives on the am_status track and never picks a
// resolution. Anything finer — fixed vs mitigated, which kind of noise — goes
// in the free-text note until some report actually consumes the distinction.
const (
	ResolutionHandled       = "handled"
	ResolutionSelfRecovered = "self_recovered"
	ResolutionNoise         = "noise"
	ResolutionExpired       = "expired"
)

// Alert represents a record requiring human response (page or ticket only).
type Alert struct {
	ID               string          `db:"id" json:"id" validate:"required"` // ALT-2607-42
	State            string          `db:"state" json:"state" validate:"required"`
	Attrs            json.RawMessage `db:"attrs" json:"attrs" swaggertype:"object" validate:"required"`
	AssignedTo       string          `db:"assigned_to" json:"assigned_to" validate:"required"`
	AckedBy          string          `db:"acked_by" json:"acked_by" validate:"required"`
	AckSource        string          `db:"ack_source" json:"ack_source" validate:"required"`
	Resolution       string          `db:"resolution" json:"resolution" validate:"required"`
	ResolutionNote   string          `db:"resolution_note" json:"resolution_note" validate:"required"`
	CallDelayUntil   *time.Time      `db:"call_delay_until" json:"call_delay_until,omitempty"`
	ResponseDeadline *time.Time      `db:"response_deadline" json:"response_deadline,omitempty"`
	RecoveryDeadline *time.Time      `db:"recovery_deadline" json:"recovery_deadline,omitempty"`
	IMThreadID       string          `db:"im_thread_id" json:"im_thread_id" validate:"required"`
	IMMessageID      string          `db:"im_message_id" json:"im_message_id" validate:"required"`
	IMChannelID      string          `db:"im_channel_id" json:"im_channel_id" validate:"required"`
	ExternalIssueKey string          `db:"external_issue_key" json:"external_issue_key" validate:"required"`
	// OpenFingerprint marks the first member's fingerprint while open, NULL
	// after resolve. Representative only; dedup now keys on the agg key.
	OpenFingerprint *string `db:"open_fingerprint" json:"-"`
	// AggKey is the aggregation-key hash (permanent). AggKeyVersion is the
	// recipe it was hashed with. Detached releases the dedup slot without
	// resolving (staleness). The staleness window is anchored on CreatedAt.
	AggKey        string `db:"agg_key" json:"-"`
	AggKeyVersion int    `db:"agg_key_version" json:"-"`
	Detached      bool   `db:"detached" json:"-"`
	// OpenSlot holds AggKey while the alert is the active aggregate (open and
	// not detached), NULL otherwise. App-maintained (set on create, nulled on
	// resolve/detach); backs the unique dedup index. Not part of the API.
	OpenSlot   *string    `db:"open_slot" json:"-"`
	CreatedAt  time.Time  `db:"created_at" json:"created_at" validate:"required"`
	UpdatedAt  time.Time  `db:"updated_at" json:"updated_at" validate:"required"`
	AckedAt    *time.Time `db:"acked_at" json:"acked_at,omitempty"`
	ResolvedAt *time.Time `db:"resolved_at" json:"resolved_at,omitempty"`

	// Generated columns (read-only, populated by DB).
	Fingerprint    string `db:"fingerprint" json:"fingerprint" validate:"required"`
	Alertname      string `db:"alertname" json:"alertname" validate:"required"`
	Classification string `db:"classification" json:"classification" validate:"required"`
	ClusterID      string `db:"cluster_id" json:"cluster_id" validate:"required"`
	TenantID       string `db:"tenant_id" json:"tenant_id" validate:"required"`
	Region         string `db:"region" json:"region" validate:"required"`
	BizRegion      string `db:"biz_region" json:"biz_region" validate:"required"`
	TenantKind     string `db:"tenant_kind" json:"tenant_kind" validate:"required"`
	CustomerType   string `db:"customer_type" json:"customer_type" validate:"required"`
}

// AlertAttrs contains the business fields stored in the attrs JSON column.
type AlertAttrs struct {
	Fingerprint    string `json:"fingerprint"`
	Alertname      string `json:"alertname"`
	Classification string `json:"classification"`
	ClusterID      string `json:"cluster_id"`
	ClusterName    string `json:"cluster_name,omitempty"`
	ClusterVersion string `json:"cluster_version,omitempty"`
	DeployType     string `json:"deploy_type,omitempty"`
	ClinicURL      string `json:"clinic_url,omitempty"`
	TenantID       string `json:"tenant_id"`
	// TenantName, BizRegion and TenantKind come from the DevOps ops portal
	// (pkg/devops), which owns the commercial view of the account. BizRegion is
	// the sales region — NA, EMEA, APAC — as opposed to Region below, which is
	// the cloud region the alert's own labels carry: the first says whose
	// account this is, the second where it runs. TenantKind is `internal` or
	// `external`, the distinction that tells an alert on one of our own test
	// accounts from one a customer is living through.
	//
	// CustomerType is the account's sales tier — SKA, KA, SMB, UNKNOWN — which
	// the ops portal calls segmentation. The name is alertstore's own and
	// predates the source: the field and its filter were designed with the
	// service and had nothing to populate them until the portal lookup arrived,
	// so a subscription naming it matched nothing at all.
	//
	// Like the clinic-enriched fields these are resolved once, at creation, and
	// have no occurrence-level counterpart — an occurrence's labels never carry
	// them, so alert.CommonFact has nothing to resolve them across.
	TenantName   string            `json:"tenant_name,omitempty"`
	BizRegion    string            `json:"biz_region,omitempty"`
	TenantKind   string            `json:"tenant_kind,omitempty"`
	CustomerType string            `json:"customer_type"`
	Region       string            `json:"region"`
	Component    string            `json:"component"`
	Namespace    string            `json:"namespace"`
	BizType      string            `json:"biz_type"`
	Environment  string            `json:"environment"`
	Summary      string            `json:"summary"`
	Message      string            `json:"message"`
	Description  string            `json:"description"`
	Labels       map[string]string `json:"labels"`
	Annotations  map[string]string `json:"annotations"`
	GeneratorURL string            `json:"generator_url"`
	// Owned is frozen at creation, not re-evaluated: an alert recorded silently
	// must not start paging people hours later because its rule was handed over
	// meanwhile, and a rollback must not orphan one someone is working.
	Owned bool `json:"owned"`
	// AggKey is the human-readable aggregation key (see AggKeyDesc). Stored for
	// debugging; the dedup index keys on its hash.
	AggKey string `json:"agg_key,omitempty"`
}

type InstanceCounts struct {
	Firing   int `json:"firing" validate:"required"`
	Resolved int `json:"resolved" validate:"required"`
	Unknown  int `json:"unknown" validate:"required"`
}

type InstanceDetail struct {
	instance.Instance
	Observations []event.Event `json:"observations" validate:"required"`
}

// AlertDetail is the API response for GET /alerts/{id}, includes computed fields.
type AlertDetail struct {
	Alert
	AMStatus         string              `json:"am_status" validate:"required"` // firing / resolved / unknown
	ExternalIssueURL string              `json:"external_issue_url,omitempty"`
	OpsPortalURL     string              `json:"ops_portal_url,omitempty"`
	InstanceCounts   InstanceCounts      `json:"instance_counts" validate:"required"`
	LastAMObservedAt *time.Time          `json:"last_am_observed_at,omitempty"`
	Instances        []InstanceDetail    `json:"instances" validate:"required"`
	Receipts         []event.ReceiptInfo `json:"receipts,omitempty"`
	Audit            []AuditEntry        `json:"audit" validate:"required"`
	// CommonFacts holds the label-derived header fields resolved across every
	// occurrence (see CommonFact), so a UI does not have to present the frozen
	// attrs of the occurrence that opened the alert as the aggregate's own.
	CommonFacts map[string]string `json:"common_facts,omitempty"`
	// CommonLabels holds the labels every occurrence agrees on (see
	// CommonLabels), printed once for the whole alert. Each occurrence in
	// Instances carries only its distinct labels (see DistinctLabels), so the
	// shared set is not repeated on every row; merge the two to recover an
	// occurrence's full label set.
	CommonLabels map[string]string `json:"common_labels,omitempty"`
}

// AuditEntry is defined here to avoid circular imports; actual type in audit package.
type AuditEntry struct {
	ID        int64           `db:"id" json:"id" validate:"required"`
	AlertID   string          `db:"alert_id" json:"alert_id" validate:"required"`
	Event     string          `db:"event" json:"event" validate:"required"`
	OldState  string          `db:"old_state" json:"old_state" validate:"required"`
	NewState  string          `db:"new_state" json:"new_state" validate:"required"`
	Actor     string          `db:"actor" json:"actor" validate:"required"`
	Source    string          `db:"source" json:"source" validate:"required"`
	Detail    json.RawMessage `db:"detail" json:"detail" swaggertype:"object" validate:"required"`
	CreatedAt time.Time       `db:"created_at" json:"created_at" validate:"required"`
}
