package imports

import (
	"context"
	"fmt"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	gprovider "github.com/tidbcloud/infra-api/controllers/provider/gardener"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/gardener"
	"github.com/tidbcloud/infra-api/pkg/k8s"
	"github.com/tidbcloud/infra-api/pkg/nodegroup"
	"github.com/tidbcloud/infra-api/pkg/util"

	"github.com/pingcap/errors"
	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// EnvBuilder defines the interfaces to create an import Env.
type EnvBuilder interface {
	// BuildEnvFromImport builds an import Env from an import API object.
	BuildEnvFromImport(ctx context.Context, i *cloudv1.Import) (Env, error)
}

// Env defines the operations in a TiDB Cluster K8S Environment for an import.
type Env interface {
	// TCName get the tidbcluster name create by the specified cluster.
	TCName(clusterName string) string
	// StorageClassName used to create pv as the local storage for lighting.
	StorageClassName() string
	// PreCreateJobHook is call before we create the job.
	PreCreateJobHook(job *batchv1.Job) error
	// Credentials gets the credential file content for import.
	Credentials(ctx context.Context) ([]byte, error)
	// CreateNodeGroup creates a node group in the K8S cluster.
	CreateNodeGroup(ctx context.Context, ngs *nodegroup.Set) (map[string]string, error)
	// DeleteNodeGroup deletes the node group in the K8S cluster.
	DeleteNodeGroup(ctx context.Context, ngs *nodegroup.Set) error
	// GetControllerClient gets the controller runtime client for the K8S cluster.
	GetControllerClient(ctx context.Context) (client.Client, error)
	// GetKubeConfig gets the kubeconfig for the K8S cluster.
	GetKubeConfig(ctx context.Context) (*rest.Config, error)
	// GetImportObjectGroup gets the related API objects of the import environment as an `ImportAPIObjectGroup`.
	GetImportObjectGroup() *ImportAPIObjectGroup
}

// GardenerShootEnv is an Env implementation for Gardener Shoot Cluster.
type GardenerShootEnv struct {
	gardenerEnv       *gardener.GardEnvironment
	importObjectGroup *ImportAPIObjectGroup
	shootName         string
	shootCtx          *gardener.ShootContext
	infraClusterCli   client.Client
}

// NewGardenerShootEnv creates an import env based on gardener shoot K8S cluster.
func NewGardenerShootEnv(ctx context.Context, gardenerEnv *gardener.GardEnvironment, importObjectGroup *ImportAPIObjectGroup, infraClusterCli client.Client) (*GardenerShootEnv, error) {
	networkStatus, err := cloudv1.GardenerNetworkStatusFromNetwork(importObjectGroup.Network)
	if err != nil {
		return nil, errors.Trace(err)
	}
	shootName := networkStatus.ShootName
	shootCtx, err := gardenerEnv.GetShootContext(ctx, shootName)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return &GardenerShootEnv{
		gardenerEnv:       gardenerEnv,
		importObjectGroup: importObjectGroup,
		shootName:         shootName,
		shootCtx:          shootCtx,
		infraClusterCli:   infraClusterCli,
	}, nil
}

// TCName get the tidbcluster name create by the specified cluster.
// It implements the Env interface.
func (e *GardenerShootEnv) TCName(_ string) string {
	return "db"
}

// GetControllerClient gets the controller runtime client for the K8S cluster.
// It implements the Env interface.
func (e *GardenerShootEnv) GetControllerClient(ctx context.Context) (client.Client, error) {
	return e.shootCtx.Manager.GetClient(), nil
}

// GetKubeConfig gets the kubeconfig for the K8S cluster.
// It implements the Env interface.
func (e *GardenerShootEnv) GetKubeConfig(ctx context.Context) (*rest.Config, error) {
	return e.shootCtx.Manager.GetConfig(), nil
}

// StorageClassName used to create pv as the local storage for lighting.
// It implements the Env interface.
func (e *GardenerShootEnv) StorageClassName() string {
	switch e.GetImportObjectGroup().Provider.Spec.Type {
	case consts.ProviderTypeAWS:
		return k8s.ImportGp3StorageClassName
	default:
		return DefaultStorageClassName
	}
}

// PreCreateJobHook is call before we create the job.
// It implements the Env interface.
func (e *GardenerShootEnv) PreCreateJobHook(_ *batchv1.Job) error {
	return nil
}

// Credentials gets the credential file content for import.
// It implements the Env interface.
func (e *GardenerShootEnv) Credentials(ctx context.Context) ([]byte, error) {
	iog := e.GetImportObjectGroup()
	switch iog.Provider.Spec.Type {
	case consts.ProviderTypeAWS:
		return awsCredentials(ctx, iog.Network, iog.Import)
	case consts.ProviderTypeGCP:
		return GCPCredentials(ctx, e.infraClusterCli, iog.Network)
	default:
		return nil, errors.Errorf("provider '%s' is not supported yet", iog.Provider.Spec.Type)
	}
}

func awsCredentials(ctx context.Context, network *cloudv1.Network, i *cloudv1.Import) ([]byte, error) {
	if i.Spec.AccessConfig.RoleArn != "" {
		return awsCrossAccountCredentials(network, i), nil
	}

	roleARN, err := GetImportIamIdentity(network)
	if err != nil {
		return nil, err
	}

	// with AWS_SDK_LOAD_CONFIG setting as true
	// this file ~/.aws/credentials will make the SDK
	// get credentials from ec2 meta and than assume the specified role_arn
	format := `[default]
role_arn = %s
credential_source = Ec2InstanceMetadata
`
	return []byte(fmt.Sprintf(format, roleARN)), nil
}

func awsCrossAccountCredentials(network *cloudv1.Network, i *cloudv1.Import) []byte {
	// role_arn is the ARN of the role to assume
	// external_id is the external ID used to assume the role
	// With the config file, AWS CLI will assume the role specified with the external ID.
	format := `[default]
role_arn = %s
external_id = %s
credential_source = Ec2InstanceMetadata
	`
	return []byte(fmt.Sprintf(format, i.Spec.AccessConfig.RoleArn, network.Status.IAM.ExternalId))
}

func GCPCredentials(ctx context.Context, cli client.Client, network *cloudv1.Network) ([]byte, error) {
	saName := gprovider.GetGCPAccessServiceAccountName(network.Spec.Region, network.GetProjectId())
	keyName := gprovider.GetGCPAccessServiceAccountKeyName(saName)
	secretName := types.NamespacedName{Namespace: network.Namespace, Name: keyName}

	var keySecret corev1.Secret
	if err := cli.Get(ctx, secretName, &keySecret); err != nil {
		return nil, err
	}
	return keySecret.Data[gcpCredentialsKey], nil
}

// CreateNodeGroup creates a node group in the K8S cluster.
// It implements the Env interface.
func (e *GardenerShootEnv) CreateNodeGroup(ctx context.Context, ngs *nodegroup.Set) (map[string]string, error) {
	if _, err := e.gardenerEnv.ApplyNodeGroupSet(ctx, e.shootName, ngs, gardener.NodeGroupPolicyScaleOnly); err != nil {
		return nil, errors.Trace(err)
	}
	ng := ngs.NodeGroups[0]
	i := e.GetImportObjectGroup().Import
	return map[string]string{
		nodegroup.GardenerWorkerNodeLabelName: nodegroup.NodeGroupName(util.FnvHash(i.ClusterScopeName()), TiDBLightningComName, ng.Zones[0]),
	}, nil
}

// DeleteNodeGroup deletes the node group in the K8S cluster.
// It implements the Env interface.
func (e *GardenerShootEnv) DeleteNodeGroup(ctx context.Context, ngs *nodegroup.Set) error {
	if _, err := e.gardenerEnv.ApplyNodeGroupSet(ctx, e.shootName, ngs, gardener.NodeGroupPolicyScaleOnly); err != nil {
		return errors.Trace(err)
	}
	return nil
}

// GetImportObjectGroup gets the related API objects of the import environment as an `ImportAPIObjectGroup`.
// It implements the Env interface.
func (e *GardenerShootEnv) GetImportObjectGroup() *ImportAPIObjectGroup {
	return e.importObjectGroup
}

// GardenerShootEnvBuilder is a EnvBuilder implementation for building a gardener-shoot-based import environment.
type GardenerShootEnvBuilder struct {
	gardenerEnv     *gardener.GardEnvironment
	infraClusterCli client.Client
}

// NewGardenerShootEnvBuilder creates a `GardenerShootEnvBuilder` instance.
func NewGardenerShootEnvBuilder(infraClusterCli client.Client, g *gardener.GardEnvironment) *GardenerShootEnvBuilder {
	return &GardenerShootEnvBuilder{
		gardenerEnv:     g,
		infraClusterCli: infraClusterCli,
	}
}

// BuildEnvFromImport builds an import Env from an import API object.
// It implements the EnvBuilder interface.
// It creates an import env based on a gardener-shoot cluster.
func (b *GardenerShootEnvBuilder) BuildEnvFromImport(ctx context.Context, i *cloudv1.Import) (Env, error) {
	iog, err := GetImportObjectGroup(ctx, b.infraClusterCli, i)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return NewGardenerShootEnv(ctx, b.gardenerEnv, iog, b.infraClusterCli)
}
