package common

import (
	"context"
	"fmt"
	"time"

	"k8s.io/apimachinery/pkg/types"

	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers/network/task"
	"github.com/tidbcloud/eks-provider/controllers/network/task/utils"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/account"
	accountsched "github.com/tidbcloud/infra-provider/pkg/account/schedule"
	accountschedpl "github.com/tidbcloud/infra-provider/pkg/account/schedule/plugin"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

// Account is a task to schedule an account and set account info for a network.
type Account struct {
	*task.BaseTask

	sched accountsched.Scheduler
}

func NewAccount(ctx *task.Context, scheduler accountsched.Scheduler) *Account {
	return &Account{
		BaseTask: &task.BaseTask{Context: ctx},
		sched:    scheduler,
	}
}

func (s *Account) Name() string {
	return "Account"
}

// Create schedule an account and update the account reference for a network.
func (s *Account) Create(ctx context.Context) deploy.Result {
	network := s.Network
	acctRef := network.Spec.AccountRef
	if acctRef != nil && acctRef.Name != "" {
		account := &cloudv1.Account{}
		err := s.Local.Get(ctx, types.NamespacedName{Name: acctRef.Name}, account)
		if err != nil {
			return deploy.Panic(err)
		}

		s.Account = account
		return deploy.Done()
	}

	acctList := &cloudv1.AccountList{}
	err := s.Local.List(ctx, acctList)
	if err != nil {
		return deploy.Panic(fmt.Errorf("list accounts failed: %v", err))
	}

	provider := util.NetworkProviderToCloudProvider(network.Spec.Provider)
	if provider == "" {
		return deploy.Panic(fmt.Errorf("not support provider %s", network.Spec.Provider))
	}
	accts := make([]*cloudv1.Account, 0)
	for i := range acctList.Items {
		acct := acctList.Items[i]
		if acct.Spec.Provider == provider {
			accts = append(accts, &acct)
		}
	}

	sctx := accountschedpl.NewSchedContext(ctx, s.Logger.WithName("account-scheduler"))
	selectedAcctName, err := s.sched.Schedule(sctx, network, accts)
	if err != nil {
		return deploy.Panic(fmt.Errorf("schedule account failed: %v", err))
	}
	if selectedAcctName == "" {
		return deploy.Panic(fmt.Errorf("selected account is empty"))
	}

	if s.Network.Spec.AccountRef != nil {
		s.Network.Spec.AccountRef.Name = selectedAcctName
	} else {
		s.Network.Spec.AccountRef = &cloudv1.AccountReference{
			Name: selectedAcctName,
		}
	}
	return deploy.Panic(fmt.Errorf("wait for the network to be updated in Create")).WithRequeuAfter(time.Millisecond) // wait for the network to be updated
}

// PostCreate set the account and role spec of a network.
func (s *Account) PostCreate(ctx context.Context) deploy.Result {
	if s.Account == nil {
		return deploy.Panic(fmt.Errorf("account is nil"))
	}

	switch s.Network.Spec.Provider {
	case consts.ProviderTypeEKS:
		// TODO: refactor this part after not update the eks network spec in tasks.
		if s.Account.Spec.AWS == nil || s.Account.Spec.AWS.AssumeRoleARN == "" {
			return deploy.Panic(fmt.Errorf("account %s has no aws account info", s.Account.Name))
		}
		needUpdate, err := setAccountSpecForEKSNetwork(s.Network, s.Account, s.AcctInfoGetter)
		if err != nil {
			return deploy.Panic(err)
		}
		if needUpdate {
			// set a small during to make it as RequeueAfter: somevalue instead of Requeue: true(will be ratelimit).
			// The ratelimit we using is too aggressive now, it will reach the maxlimit in a few retry.
			return deploy.Panic(fmt.Errorf("wait for the network to be updated in PostCreate")).WithRequeuAfter(time.Millisecond)
		}
		return deploy.Done()
	case consts.ProviderTypeGKE:
		if s.Account.Spec.GCP == nil || s.Account.Spec.GCP.ProjectID == "" {
			return deploy.Panic(fmt.Errorf("account %s has no gcp account info", s.Account.Name))
		}
		return s.setAccountSpecForGKENetwork()
	default:
		return deploy.Panic(fmt.Errorf("not support provider %s", s.Network.Spec.Provider))
	}
}

func setAccountSpecForEKSNetwork(
	network *cloudv1.Network,
	account *cloudv1.Account,
	infoGetter account.LegacyInfoGetter,
) (needUpdate bool, err error) {
	needUpdate, err = utils.MutateEKSNetworkSpec(network, func(spec *eksv1alpha1.EKSNetworkSpec) error {
		if spec.AssumeExternalId != "" && spec.AssumeRoleArn != "" {
			return nil
		}

		info, err := infoGetter.Network(network)
		if err != nil {
			return fmt.Errorf("failed to get network info: %w", err)
		}

		spec.AssumeRoleArn = account.Spec.AWS.AssumeRoleARN
		if account.Spec.AWS.ExternalID != nil {
			spec.AssumeExternalId = *account.Spec.AWS.ExternalID
		}
		spec.ServiceRoleArn = util.GetRoleARN(account.Spec.AWS.AccountID, info.ServiceRoleName)
		return nil
	})
	if err != nil {
		return false, err
	}

	// Set info for import. To trust the account.
	spec, err := eksv1alpha1.EKSNetworkSpecForNetwork(network)
	if err != nil {
		return false, err
	}
	if network.Status.IAM.AccountID == "" {
		// Xaccount will be set node role arn when creating the network.
		// Full manager will be set when after node role task completes.
		if spec.NodeRoleArn != "" {
			err = utils.SetIAMAccountID(spec.NodeRoleArn, network)
			if err != nil {
				return false, err
			}
			needUpdate = true
		}

		if network.Status.IAM.ExternalId == "" {
			network.Status.IAM.ExternalId = namer.GenExternalID(network.Namespace, network.Name)
			needUpdate = true
		}
	}
	return needUpdate, nil
}

func (s *Account) setAccountSpecForGKENetwork() deploy.Result {
	if s.GKENetworkSpec.ProjectID == "" {
		s.GKENetworkSpec.ProjectID = s.Account.Spec.GCP.ProjectID
		return deploy.Panic(nil).WithRequeuAfter(0) // wait for the network spec to be updated
	}

	return deploy.Done()
}
