package server

import (
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"google.golang.org/grpc/codes"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

// Note: apiv1.AutoscalePolicySubjectType and apiv1.AutoscalePolicyDirection
// are type-aliased to entity.* in entity.go. The metric helpers below take the
// entity types so callers in this package (which already use entity.*) do not
// need a separate alias import.

var (
	autoscalePolicyResolveTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "autoscale_policy_resolve_total",
			Help: "Count of autoscale policy evaluations in regional service.",
		},
		[]string{"component", "capability", "trigger", "result"},
	)
	autoscalePolicyResolveDurationSeconds = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "autoscale_policy_resolve_duration_seconds",
			Help:    "Latency of autoscale policy resolution in regional service.",
			Buckets: prometheus.DefBuckets,
		},
		[]string{"component"},
	)
	autoscalePolicyBundleVersion = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "autoscale_policy_bundle_version",
			Help: "Resolved autoscale policy bundle version by subject type and component.",
		},
		[]string{"subject_type", "component"},
	)
	autoscaleRuntimeGuardActive = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "autoscale_runtime_guard_active",
			Help: "Active autoscale runtime guards.",
		},
		[]string{"scope_type", "guard_type", "component_type"},
	)
	autoscaleRuntimeGuardWriteTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "autoscale_runtime_guard_write_total",
			Help: "Count of autoscale runtime guard writes.",
		},
		[]string{"guard_type", "source", "result"},
	)
	// autoscalePolicyResolveSource: under storage-point exclusivity
	// (see design doc §Storage-point exclusivity), every action-layer
	// call must resolve via readRuntimeSpecs. {path="runtime_specs"}
	// is the only expected label value; any other value is a regression.
	autoscalePolicyResolveSource = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "autoscale_policy_resolve_source_total",
			Help: "Count of autoscale policy resolves by source path. Active design expects only path=\"runtime_specs\"; any other label value indicates a regression of the storage-point exclusivity invariant.",
		},
		[]string{"path"},
	)
	// autoscale_policy_write_total: one increment per CreateOrReplace /
	// Patch / Sync RPC with scope, operation, apply_to_all, result.
	// Cardinality ~4*3*2*4=96 series (within budget) — 4 scopes ×
	// 3 operations × 2 apply_to_all values × 4 results — see design
	// doc §Metrics.
	autoscalePolicyWriteTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "autoscale_policy_write_total",
			Help: "Count of policy RPC writes by scope, operation, apply_to_all, and result. Reflects autoscaler-engine's CURRENT view.",
		},
		[]string{"scope", "operation", "apply_to_all", "result"},
	)
	// autoscale_policy_write_duration_seconds: companion latency
	// histogram (default Prometheus buckets 5ms..10s).
	autoscalePolicyWriteDurationSeconds = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "autoscale_policy_write_duration_seconds",
			Help:    "Latency of v2 autoscale policy RPC writes by scope and operation. Reflects autoscaler-engine's CURRENT view.",
			Buckets: prometheus.DefBuckets,
		},
		[]string{"scope", "operation"},
	)
	// autoscale_policy_fanout_targets_total: per-target outcome of
	// TENANT-scope fanout (apply_to_all_shared_pools=true). Labeled by
	// operation and status only — no tenant_id, to keep cardinality
	// bounded (3 statuses × 3 operations = 9 series, fixed) and avoid an
	// unbounded per-tenant series leak with no delete path.
	autoscalePolicyFanoutTargetsTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "autoscale_policy_fanout_targets_total",
			Help: "Count of TENANT-scope fanout targets by operation and status. Reflects autoscaler-engine's CURRENT view.",
		},
		[]string{"operation", "status"},
	)
	// autoscale_policy_fanout_duration_seconds: tracked separately
	// from write_duration so dashboards can distinguish fanout
	// (multi-target) from single-target writes.
	autoscalePolicyFanoutDurationSeconds = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "autoscale_policy_fanout_duration_seconds",
			Help:    "Latency of TENANT-scope autoscale policy fanout by operation. Reflects autoscaler-engine's CURRENT view.",
			Buckets: prometheus.DefBuckets,
		},
		[]string{"operation"},
	)
)

// Note: this module's metric set is the regional-runtime view of the
// source-of-truth chain (see AGENTS.md §AUTOSCALE POLICY MODULE).
// There is no HPA-side gate decision to count — the action-layer is
// the only consumer of EvaluateAutoscalePolicy. See design doc
// §Metrics Design for the per-family cardinality contract.

func markRuntimeGuardGauge(guard *entity.AutoscaleRuntimeGuard, active bool) {
	if guard == nil {
		return
	}
	value := 0.0
	if active {
		value = 1
	}
	autoscaleRuntimeGuardActive.WithLabelValues(
		guard.GetScopeType().String(),
		guard.GetGuardType().String(),
		guard.GetComponentType().String(),
	).Set(value)
}

// policyWriteResult: outcome label set for autoscale_policy_write_total.
// Kept small on purpose — OK / InvalidArgument (pre-write reject) /
// Internal (post-validation failure) / Unimplemented.
const (
	policyWriteResultOK            = "ok"
	policyWriteResultInvalidArg    = "invalid_argument"
	policyWriteResultInternalError = "internal_error"
	policyWriteResultUnimplemented = "unimplemented"
)

// applyToAllLabel translates wrapperspb.BoolValue into a stable
// string label. nil and false collapse to "false" for cardinality
// (both mean "not a fanout write" from the storage perspective;
// semantic distinction is preserved at the call site).
func applyToAllLabel(applyToAllSharedPools *wrapperspb.BoolValue) string {
	if applyToAllSharedPools != nil && applyToAllSharedPools.Value {
		return "true"
	}
	return "false"
}

// policyWriteOperationLabel translates policyOperation into a stable
// string label for autoscale_policy_write_total and
// autoscale_policy_fanout_targets_total.
func policyWriteOperationLabel(op policyOperation) string {
	switch op {
	case operationCreateOrReplace:
		return "create_or_replace"
	case operationPatch:
		return "patch"
	case operationSync:
		return "sync"
	default:
		return "unknown"
	}
}

// fanoutStatusLabel translates AutoscalePolicyApplyStatus into the
// label for autoscale_policy_fanout_targets_total. Bounded set (3
// values × 3 operations = 9 series/tenant, well below the 20-series
// per-pool budget).
func fanoutStatusLabel(status apiv1.AutoscalePolicyApplyStatus) string {
	switch status {
	case apiv1.AutoscalePolicyApplyStatus_APPLY_STATUS_SYNCED:
		return "synced"
	case apiv1.AutoscalePolicyApplyStatus_APPLY_STATUS_FAILED:
		return "failed"
	case apiv1.AutoscalePolicyApplyStatus_APPLY_STATUS_SKIPPED:
		return "skipped"
	default:
		return "unknown"
	}
}

// markPolicyWrite records one write_total increment and observes the
// duration histogram. Call from every policy RPC entry point
// BEFORE returning, so the metric is incremented even on early
// validation rejects. The result label distinguishes reject vs success.
func markPolicyWrite(scope apiv1.AutoscalePolicyScope, op policyOperation, applyToAllSharedPools *wrapperspb.BoolValue, result string, startedAt time.Time) {
	scopeStr := scope.String()
	opStr := policyWriteOperationLabel(op)
	autoscalePolicyWriteTotal.WithLabelValues(scopeStr, opStr, applyToAllLabel(applyToAllSharedPools), result).Inc()
	if !startedAt.IsZero() {
		autoscalePolicyWriteDurationSeconds.WithLabelValues(scopeStr, opStr).Observe(time.Since(startedAt).Seconds())
	}
}

// markPolicyFanoutTarget records one fanout-target outcome. Call from
// inside the TENANT-scope fanout loop, once per sharedpool.
// Skipped/failed paths still increment so the metric reflects the
// actual target list, not just successes.
func markPolicyFanoutTarget(op policyOperation, status apiv1.AutoscalePolicyApplyStatus) {
	autoscalePolicyFanoutTargetsTotal.WithLabelValues(policyWriteOperationLabel(op), fanoutStatusLabel(status)).Inc()
}

// statusCodeToPolicyWriteResult maps a policy RPC gRPC status code
// to the small label set used by autoscale_policy_write_total.
// Unknown codes collapse to internal_error so a new gRPC code
// never panics.
func statusCodeToPolicyWriteResult(code codes.Code) string {
	switch code {
	case codes.OK:
		return policyWriteResultOK
	case codes.InvalidArgument:
		return policyWriteResultInvalidArg
	case codes.Unimplemented:
		return policyWriteResultUnimplemented
	default:
		return policyWriteResultInternalError
	}
}

// observePolicyFanoutDuration records the wall-clock latency of one
// TENANT-scope fanout (all sharedpools of the tenant). Call once per
// RPC, after the fanout loop returns.
func observePolicyFanoutDuration(op policyOperation, startedAt time.Time) {
	if !startedAt.IsZero() {
		autoscalePolicyFanoutDurationSeconds.WithLabelValues(policyWriteOperationLabel(op)).Observe(time.Since(startedAt).Seconds())
	}
}
