package serverlessaction

import (
	"context"
	"fmt"

	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
)

// Restart scope values for ParamRestartScope.
const (
	RestartScopeNamespace = "namespace"
	RestartScopeCluster   = "cluster"
)

var ActionServerlessOpsRestartTiDBPods = opent.ActionMetadata{
	Name: "ServerlessOpsRestartTiDBPods",
	Type: opent.ActionTypeAuto,
}

var _ actions.Action = &restartTiDBPods{}

type restartTiDBPods struct {
	actions.BaseAction
	comps *actions.Components
}

// NewRestartTiDBPods constructs an AutoAction that restarts TiDB pods in a
// serverless cluster via ops-service. The restart_scope param selects between:
//   - "namespace": restart all TiDB pods in a pool namespace. Pool-level op;
//     routing params (cloud_provider, region, kubernetes, namespace) are taken
//     directly from instance params. No tenant cluster lookup.
//   - "cluster":   restart TiDB pods assigned to a tenant cluster's keyspace.
//     Routing is derived from cluster_id via mgmt + serverless-service
//     GetCluster; keyspace_token is used (intentionally NOT keyspace_id).
func NewRestartTiDBPods(comps *actions.Components) actions.AutoAction {
	return &restartTiDBPods{comps: comps}
}

func (a *restartTiDBPods) Metadata() opent.ActionMetadata {
	return ActionServerlessOpsRestartTiDBPods
}

func (a *restartTiDBPods) PreCheck(ctx context.Context, instance *opent.Instance) error {
	scope, err := parseRestartScope(instance)
	if err != nil {
		return err
	}
	switch scope {
	case RestartScopeNamespace:
		_, err := a.preCheckByNamespace(instance)
		return err
	case RestartScopeCluster:
		_, err := a.preCheckByCluster(ctx, instance)
		return err
	}
	return fmt.Errorf("unreachable: invalid restart_scope %q", scope)
}

func (a *restartTiDBPods) Start(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	scope, err := parseRestartScope(instance)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	force := parseForce(instance)

	switch scope {
	case RestartScopeNamespace:
		p, err := a.preCheckByNamespace(instance)
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
		resp, err := a.comps.ServerlessCli.RestartTiDBPodsByNamespace(ctx,
			p.provider, p.region, p.kubernetes, p.namespace, force,
		)
		if err != nil {
			return actions.FailedActionResult(fmt.Errorf("restart tidb pods by namespace: %w", err)), nil
		}
		outputs := map[opent.OutputName]string{
			outputProvider:   p.provider,
			outputRegion:     p.region,
			outputKubernetes: p.kubernetes,
			outputNamespace:  p.namespace,
			outputForce:      fmt.Sprintf("%t", force),
		}
		return actions.RunningActionResult(fmt.Sprintf("sent restart by namespace request (force=%t): %s", force, resp.GetMessage()), outputs), nil

	case RestartScopeCluster:
		resolved, err := a.preCheckByCluster(ctx, instance)
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
		resp, err := a.comps.ServerlessCli.RestartTiDBPodsByCluster(ctx,
			resolved.Provider, resolved.Region, resolved.Kubernetes, resolved.KeyspaceToken, force,
		)
		if err != nil {
			return actions.FailedActionResult(fmt.Errorf("restart tidb pods by cluster: %w", err)), nil
		}
		outputs := map[opent.OutputName]string{
			outputProvider:      resolved.Provider,
			outputRegion:        resolved.Region,
			outputKubernetes:    resolved.Kubernetes,
			outputKeyspaceToken: resolved.KeyspaceToken,
			outputForce:         fmt.Sprintf("%t", force),
		}
		return actions.RunningActionResult(fmt.Sprintf("sent restart by cluster request (force=%t): %s", force, resp.GetMessage()), outputs), nil
	}

	return actions.FailedActionResult(fmt.Errorf("unreachable: invalid restart_scope %q", scope)), nil
}

func (a *restartTiDBPods) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	return actions.SucceededActionResult(nil), nil
}

// OnCancel is a no-op: deleted pods cannot be "un-deleted".
func (a *restartTiDBPods) OnCancel(ctx context.Context, instance *opent.Instance) (bool, error) {
	return false, nil
}

// namespaceParams carries the routing + target info for a pool-namespace restart.
type namespaceParams struct {
	provider, region, kubernetes, namespace string
}

// preCheckByNamespace parses and validates namespace-scope params and records
// outputs so approval reviewers can see the concrete target.
func (a *restartTiDBPods) preCheckByNamespace(instance *opent.Instance) (*namespaceParams, error) {
	provider, _ := instance.GetParam(opent.ParamProvider)
	region, _ := instance.GetParam(opent.ParamRegion)
	k8s, _ := instance.GetParam(opent.ParamKubernetes)
	ns, _ := instance.GetParam(opent.ParamNamespace)
	if provider == "" {
		return nil, fmt.Errorf("%s is required", opent.ParamProvider)
	}
	if region == "" {
		return nil, fmt.Errorf("%s is required", opent.ParamRegion)
	}
	if k8s == "" {
		return nil, fmt.Errorf("%s is required", opent.ParamKubernetes)
	}
	if ns == "" {
		return nil, fmt.Errorf("%s is required", opent.ParamNamespace)
	}
	if !isAllowedPoolNamespace(ns) {
		return nil, fmt.Errorf("namespace %q is not in the pool whitelist", ns)
	}
	p := &namespaceParams{provider: provider, region: region, kubernetes: k8s, namespace: ns}

	if instance.Outputs == nil {
		instance.Outputs = map[opent.OutputName]string{}
	}
	instance.Outputs[outputProvider] = p.provider
	instance.Outputs[outputRegion] = p.region
	instance.Outputs[outputKubernetes] = p.kubernetes
	instance.Outputs[outputNamespace] = p.namespace
	instance.Outputs[outputTargets] = fmt.Sprintf("all tidb pods in namespace %s on %s/%s/%s",
		p.namespace, p.provider, p.region, p.kubernetes)
	return p, nil
}

// preCheckByCluster resolves the cluster, validates KeyspaceToken, and records
// outputs.
func (a *restartTiDBPods) preCheckByCluster(ctx context.Context, instance *opent.Instance) (*resolvedCluster, error) {
	resolved, err := resolveServerlessCluster(ctx, a.comps, instance)
	if err != nil {
		return nil, err
	}
	if resolved.KeyspaceToken == "" {
		// Guard: ops-service RPC keys off KeyspaceToken (string, proto field 4),
		// NOT KeyspaceId (uint32, field 5). Passing KeyspaceId would silently
		// target the wrong keyspace.
		return nil, fmt.Errorf("cluster %s has empty KeyspaceToken (note: not KeyspaceId)", resolved.ClusterID)
	}

	if instance.Outputs == nil {
		instance.Outputs = map[opent.OutputName]string{}
	}
	instance.Outputs[outputProvider] = resolved.Provider
	instance.Outputs[outputRegion] = resolved.Region
	instance.Outputs[outputKubernetes] = resolved.Kubernetes
	instance.Outputs[outputKeyspaceToken] = resolved.KeyspaceToken
	instance.Outputs[outputTargets] = fmt.Sprintf("all tidb pods assigned to keyspace_token=%s on %s/%s/%s",
		resolved.KeyspaceToken, resolved.Provider, resolved.Region, resolved.Kubernetes)
	return resolved, nil
}

// parseForce returns true only when the force_restart param is literally
// "true". Any other value (including unset) defaults to graceful restart.
func parseForce(instance *opent.Instance) bool {
	v, _ := instance.GetParam(opent.ParamForceRestart)
	return v == "true"
}

func parseRestartScope(instance *opent.Instance) (string, error) {
	scope, _ := instance.GetParam(opent.ParamRestartScope)
	switch scope {
	case RestartScopeNamespace, RestartScopeCluster:
		return scope, nil
	case "":
		return "", fmt.Errorf("restart_scope is required (expected %q or %q)", RestartScopeNamespace, RestartScopeCluster)
	default:
		return "", fmt.Errorf("invalid restart_scope %q (expected %q or %q)", scope, RestartScopeNamespace, RestartScopeCluster)
	}
}
