package jumper

import (
	"context"
	"fmt"

	"github.com/go-logr/logr"
	"tailscale.com/client/tailscale/v2"

	jmpapi "github.com/tidbcloud/devops/api/jump"
	jpent "github.com/tidbcloud/devops/entity/jump"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/dedicatedcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/mgmthttpcli"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/service/jump/jumppod/types"
)

// JumperDependency contains all dependencies for all Jumper.
type JumperDependency struct {
	Logger              logr.Logger
	Conf                config.JumpServiceConfig
	CoreSvc             *coresvccli.CoreSvcCli
	MgmtHTTP            mgmthttpcli.MgmtHTTPClient
	Infra               dedicatedcli.InfraAPIClient
	DestinationRegistry *DestinationRegistry
	Tailscale           *tailscale.Client
	K8sJumperFactory    *K8sJumperFactory
}

// JumpPodForJumper is used by Jumper to control JumpPod.
type JumpPodForJumper interface {
	JumpContext() *types.JumpContext

	StoreKubernetesCred(ctx context.Context, credID jpent.CredentialID, value []byte) error
	RemoveKubernetesCred(ctx context.Context, credID jpent.CredentialID) error

	TailscaleUp(ctx context.Context, opts jmpapi.TailscaleUpRequest) error
	TailscaleDown(ctx context.Context) error
	TailscaleStatus(ctx context.Context) (*jmpapi.TailscaleStatus, error)
}

// Jumper knows how to jump to a destination.
type Jumper interface {
	// PrepareJump make the jump pod ready to jump to the destination.
	PrepareJump(ctx context.Context, jp JumpPodForJumper, dest jpent.IDestination, param *types.PrepareJumpParam) (*types.JumpParam, error)
	// FinalizeJump clean up the jump pod after jump to the destination.
	FinalizeJump(ctx context.Context, jp JumpPodForJumper, dest jpent.IDestination, attr jpent.JumpAttributes) error
	// RefreshJump refresh the jump token.
	RefreshJump(ctx context.Context, jp JumpPodForJumper, dest jpent.IDestination, attr jpent.JumpAttributes, param *types.RefreshJumpParam) (*types.JumpParam, error)
	// SupportedDestinations returns the supported destination type and names.
	//
	// If the names is nil, it means destinations are dynamic.
	SupportedDestinations() (jpent.DestinationType, []string)
}

// Router is a Jumper that can route to different Jumpers based on destination type.
type Router struct {
	jumpers map[jpent.DestinationType]Jumper
}

func DefaultRouter(dep JumperDependency) (*Router, error) {
	r := &Router{
		jumpers: make(map[jpent.DestinationType]Jumper),
	}

	fns := []func(JumperDependency) (Jumper, error){
		NewDedicatedJumper,
		NewControlPlaneJumper,
		NewServerlessJumper,
		NewByocJumper,
		NewPremiumClusterJumper,
		NewPremiumResourcePoolJumper,
	}

	for _, fn := range fns {
		jumper, err := fn(dep)
		if err != nil {
			return nil, fmt.Errorf("failed to create jumper: %w", err)
		}

		typ, _ := jumper.SupportedDestinations()
		r.jumpers[typ] = jumper
	}

	return r, nil
}

func (j *Router) PrepareJump(ctx context.Context, jp JumpPodForJumper, dest jpent.IDestination, param *types.PrepareJumpParam) (*types.JumpParam, error) {
	jumper, ok := j.jumpers[dest.Type()]
	if !ok {
		return nil, fmt.Errorf("unsupported dest type %s", dest.Type())
	}

	return jumper.PrepareJump(ctx, jp, dest, param)
}

func (j *Router) FinalizeJump(ctx context.Context, jp JumpPodForJumper, dest jpent.IDestination, attr jpent.JumpAttributes) error {
	jumper, ok := j.jumpers[dest.Type()]
	if !ok {
		return fmt.Errorf("unsupported dest type %s", dest.Type())
	}

	return jumper.FinalizeJump(ctx, jp, dest, attr)
}

func (j *Router) RefreshJump(ctx context.Context, jp JumpPodForJumper, dest jpent.IDestination, attr jpent.JumpAttributes, param *types.RefreshJumpParam) (*types.JumpParam, error) {
	jumper, ok := j.jumpers[dest.Type()]
	if !ok {
		return nil, fmt.Errorf("unsupported dest type %s", dest.Type())
	}

	return jumper.RefreshJump(ctx, jp, dest, attr, param)
}

func (j *Router) SupportedDestinations(typ jpent.DestinationType) []string {
	jumper, ok := j.jumpers[typ]
	if !ok {
		return nil
	}
	_, names := jumper.SupportedDestinations()
	return names
}
