package server

import (
	"context"
	"time"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// validateRuntimeGuardScopeTypeWrite enforces the allowlist for write
// paths (Upsert / Delete): exactly REGION is accepted. UNSPECIFIED is
// rejected as "missing required field", and any other wire-format value
// (including unknown enum numbers carried by an out-of-date client) is
// rejected with InvalidArgument. Without this, unknown values would
// silently round-trip through scopeTypeToDB as the empty string and the
// runtime would never enforce the guard (silent no-op). See design doc
// §Runtime guards → Allowlist.
func validateRuntimeGuardScopeTypeWrite(scope entity.AutoscaleRuntimeGuardScopeType) error {
	switch scope {
	case apiv1.AutoscaleRuntimeGuard_REGION:
		return nil
	case apiv1.AutoscaleRuntimeGuard_SCOPE_TYPE_UNSPECIFIED:
		return status.Error(codes.InvalidArgument, "autoscale runtime guard scope type is unspecified")
	default:
		return status.Errorf(codes.InvalidArgument,
			"autoscale runtime guard scope type %v is not supported; only REGION is allowed",
			scope)
	}
}

// validateRuntimeGuardScopeTypeRead enforces the allowlist for the read
// path (List filter): UNSPECIFIED means "no scope filter" (returns all
// guards), REGION means "filter to REGION scope". Any other wire-format
// value is rejected with InvalidArgument.
func validateRuntimeGuardScopeTypeRead(scope entity.AutoscaleRuntimeGuardScopeType) error {
	switch scope {
	case apiv1.AutoscaleRuntimeGuard_SCOPE_TYPE_UNSPECIFIED,
		apiv1.AutoscaleRuntimeGuard_REGION:
		return nil
	default:
		return status.Errorf(codes.InvalidArgument,
			"autoscale runtime guard filter scope type %v is not supported; allowed: UNSPECIFIED (no filter), REGION",
			scope)
	}
}

func (s *serverImpl) UpsertAutoscaleRuntimeGuard(ctx context.Context, guard *entity.AutoscaleRuntimeGuard) (*entity.AutoscaleRuntimeGuard, error) {
	if guard == nil {
		return nil, status.Error(codes.InvalidArgument, "autoscale runtime guard is nil")
	}
	if err := validateRuntimeGuardScopeTypeWrite(guard.GetScopeType()); err != nil {
		return nil, err
	}
	if guard.GetScopeId() == "" {
		return nil, status.Error(codes.InvalidArgument, "autoscale runtime guard scope id is empty")
	}
	if guard.GetGuardType() == apiv1.AutoscaleRuntimeGuard_GUARD_TYPE_UNSPECIFIED {
		return nil, status.Error(codes.InvalidArgument, "autoscale runtime guard type is unspecified")
	}

	created, err := s.autoscaleRuntimeGuardRepo.UpsertGuard(ctx, guard)
	if err != nil {
		autoscaleRuntimeGuardWriteTotal.WithLabelValues(guard.GetGuardType().String(), guard.GetSource(), "error").Inc()
		return nil, status.Errorf(codes.Internal, "failed to upsert autoscale runtime guard: %s", err)
	}
	autoscaleRuntimeGuardWriteTotal.WithLabelValues(guard.GetGuardType().String(), guard.GetSource(), "success").Inc()
	markRuntimeGuardGauge(created, isRuntimeGuardActive(created, time.Now()))
	return created, nil
}

func (s *serverImpl) DeleteAutoscaleRuntimeGuard(
	ctx context.Context,
	scopeType entity.AutoscaleRuntimeGuardScopeType,
	scopeID string,
	guardType entity.AutoscaleRuntimeGuardType,
	componentType entity.ComponentType,
) error {
	if err := validateRuntimeGuardScopeTypeWrite(scopeType); err != nil {
		return err
	}
	if scopeID == "" {
		return status.Error(codes.InvalidArgument, "autoscale runtime guard scope id is empty")
	}
	if guardType == apiv1.AutoscaleRuntimeGuard_GUARD_TYPE_UNSPECIFIED {
		return status.Error(codes.InvalidArgument, "autoscale runtime guard type is unspecified")
	}
	if err := s.autoscaleRuntimeGuardRepo.DeleteGuard(ctx, scopeType, scopeID, guardType, componentType); err != nil {
		autoscaleRuntimeGuardWriteTotal.WithLabelValues(guardType.String(), "delete", "error").Inc()
		return status.Errorf(codes.Internal, "failed to delete autoscale runtime guard: %s", err)
	}
	autoscaleRuntimeGuardWriteTotal.WithLabelValues(guardType.String(), "delete", "success").Inc()
	markRuntimeGuardGauge(&entity.AutoscaleRuntimeGuard{
		ScopeType:     scopeType,
		ScopeId:       scopeID,
		GuardType:     guardType,
		ComponentType: componentType,
	}, false)
	return nil
}

func (s *serverImpl) ListAutoscaleRuntimeGuards(ctx context.Context, filter repo.AutoscaleRuntimeGuardFilter) (*entity.ListAutoscaleRuntimeGuardsResponse, error) {
	if err := validateRuntimeGuardScopeTypeRead(filter.ScopeType); err != nil {
		return nil, err
	}
	guards, err := s.autoscaleRuntimeGuardRepo.ListGuards(ctx, filter)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list autoscale runtime guards: %s", err)
	}
	now := time.Now()
	for _, guard := range guards {
		markRuntimeGuardGauge(guard, isRuntimeGuardActive(guard, now))
	}
	return &entity.ListAutoscaleRuntimeGuardsResponse{Guards: guards}, nil
}

// NOTE: Regional Runtime Guard is bypass/store-only this iteration. Guards are
// persisted and listed via Upsert/Delete/List above, but there is NO runtime
// enforcement path — EvaluateAutoscalePolicy does not consult them and no scale
// decision is influenced. See docs/design/tenant-aware-autoscale-policy-redesign.md
// §Regional Runtime Guard (Part II no-goal: schema + RPC only).
// TODO: when runtime enforcement lands, add a REGION FORBID_SCALE_IN check on the
// scale-in path and surface the operator-supplied guard.Reason (not just a fixed
// taxonomy string) in the response/log for on-call correlation.

func isRuntimeGuardActive(guard *entity.AutoscaleRuntimeGuard, now time.Time) bool {
	if guard == nil {
		return false
	}
	if guard.GetExpiresAt() == nil {
		return true
	}
	return guard.GetExpiresAt().AsTime().After(now)
}
