package account

import (
	"context"
	"fmt"

	rsconfig "github.com/tidbcloud/infra-provider/regional-server/pkg/config"

	"github.com/pingcap/log"
	regionalapiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/constants"
	bizerr "github.com/tidbcloud/infra-provider/regional-server/pkg/common/errors"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/models"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/account"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/vpc"
	"go.uber.org/zap"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type ServiceInterface interface {
	AssignAccount(ctx context.Context, vpcName string, tenantID string, projectID string) (accountInfo models.DataPlaneAccountInfo, providerConfigName string, err error)
	PreAssignAccount(ctx context.Context, req *regionalapiv1.AssignAccountRequest) (resp *regionalapiv1.Account, err error)
	GetAccount(ctx context.Context, req *regionalapiv1.GetAccountRequest) (resp *regionalapiv1.Account, err error)
}

type Service struct {
	accountRW account.AccountReaderWriter
	vpcR      vpc.VPCReader
	config    rsconfig.RootConfig
}

func NewService(accountRW account.AccountReaderWriter, vpcR vpc.VPCReader, config rsconfig.RootConfig) *Service {
	return &Service{
		accountRW: accountRW,
		vpcR:      vpcR,
		config:    config,
	}
}

// AssignAccount Assign an account to a vpc
func (a Service) AssignAccount(ctx context.Context, vpcName string, tenantID string, projectID string) (models.DataPlaneAccountInfo, string, error) {
	bindingAccount, _, err := a.findBindingAccount(ctx, []models.AccountBindingTarget{
		{
			TargetType: constants.AccountBindingVPC,
			TargetKey:  vpcName,
		},
		{
			TargetType: constants.AccountBindingProject,
			TargetKey:  projectID,
		},
		{
			TargetType: constants.AccountBindingTenantID,
			TargetKey:  tenantID,
		},
	})
	if err != nil {
		// todo handle error
		return models.DataPlaneAccountInfo{}, "", err
	}
	if len(bindingAccount.GetKey()) > 0 {
		return bindingAccount, bindingAccount.GetKey(), nil
	}

	defaultAccount, err := a.accountRW.GetDefault(ctx)
	if err != nil {
		// todo handle error
		return models.DataPlaneAccountInfo{}, "", err
	}

	err = a.accountRW.Bind(ctx, defaultAccount, models.AccountBindingTarget{
		TargetType: constants.AccountBindingVPC,
		TargetKey:  vpcName,
	})

	if err != nil {
		// todo handle error
		return models.DataPlaneAccountInfo{}, "", err
	}
	// create provider config
	return defaultAccount, defaultAccount.GetKey(), nil
}

func (a Service) findBindingAccount(ctx context.Context, targets []models.AccountBindingTarget) (models.DataPlaneAccountInfo, string, error) {
	for _, target := range targets {
		bindingAccount, err := a.accountRW.GetBinding(ctx, target)
		if err != nil {
			return models.DataPlaneAccountInfo{}, "", err
		}
		if len(bindingAccount.GetKey()) > 0 {
			return bindingAccount, bindingAccount.GetKey(), nil
		}
	}
	return models.DataPlaneAccountInfo{}, "", nil
}

// PreAssignAccount pre assign an account to a project
func (s Service) PreAssignAccount(ctx context.Context, req *regionalapiv1.AssignAccountRequest) (*regionalapiv1.Account, error) {
	resp := &regionalapiv1.Account{}
	account, err := s.preAssignAccount(ctx, req.ProjectId, req.CmekEnabled)
	if err != nil {
		log.Error("failed to pre assign account", zap.Error(err))
		return nil, bizerr.New(bizerr.InternalServerError, "failed to pre assign account")
	}

	resp.AccountId = account.GetAccountID()

	if s.config.Provider == consts.ProviderTypeAKS {
		resp.AzureAppRegistration = s.getAzureAppRegistration(req.ProjectId)
	}

	return resp, nil
}

func (s Service) preAssignAccount(ctx context.Context, projectID string, cmekEnabled bool) (models.DataPlaneAccountInfo, error) {
	bindingAccount, _, err := s.findBindingAccount(ctx, []models.AccountBindingTarget{
		{
			TargetType: constants.AccountBindingProject,
			TargetKey:  projectID,
		},
	})
	if err != nil {
		return models.DataPlaneAccountInfo{}, err
	}

	if len(bindingAccount.GetKey()) > 0 {
		return bindingAccount, nil
	}

	accounts, err := s.accountRW.List(ctx, models.AccountFilter{CMEKEnabled: cmekEnabled})
	if err != nil {
		return models.DataPlaneAccountInfo{}, err
	}

	if len(accounts) == 0 {
		return models.DataPlaneAccountInfo{}, fmt.Errorf("no available account")
	}

	defaultAccount, err := s.accountRW.GetDefault(ctx)
	if err != nil {
		return models.DataPlaneAccountInfo{}, err
	}

	// if default account is not in the list, use the first one,
	// otherwise use the default one
	targetAccount := accounts[0]
	for _, account := range accounts {
		if account.GetKey() == defaultAccount.GetKey() {
			targetAccount = account
			break
		}
	}

	err = s.accountRW.Bind(ctx, targetAccount, models.AccountBindingTarget{
		TargetType: constants.AccountBindingProject,
		TargetKey:  projectID,
	})
	if err != nil {
		return models.DataPlaneAccountInfo{}, err
	}

	return targetAccount, nil
}

// GetAccount returns the account information associated with a given project.
// If a VPC exists for the project, it returns the account assigned to that VPC.
// If no such VPC is found, it retrieves the account from the project's account binding information.
func (s Service) GetAccount(ctx context.Context, req *regionalapiv1.GetAccountRequest) (*regionalapiv1.Account, error) {
	resp := &regionalapiv1.Account{}
	opt := client.MatchingLabels{
		utils.GenProjectLabelKey(req.ProjectId): "true",
	}
	vpcs := &v1alpha1.VPCList{}
	if err := s.vpcR.List(ctx, vpcs, opt); err != nil {
		return nil, err
	}
	for _, vpc := range vpcs.Items {
		if vpc.Status.Phase == v1alpha1.VPCPhaseDeleting || vpc.Status.Phase == v1alpha1.VPCPhaseDeleted {
			continue
		}
		resp.AccountId = vpc.Spec.AccountRef.AccountID

		if s.config.Provider == consts.ProviderTypeAKS {
			resp.AzureAppRegistration = s.getAzureAppRegistration(req.ProjectId)
		}

		return resp, nil
	}
	bindingAccount, _, err := s.findBindingAccount(ctx, []models.AccountBindingTarget{
		{
			TargetType: constants.AccountBindingProject,
			TargetKey:  req.ProjectId,
		},
	})
	if err != nil {
		return nil, err
	}
	if len(bindingAccount.GetKey()) == 0 {
		return nil, bizerr.New(bizerr.ResourceNotFound, "account not found")
	}

	resp.AccountId = bindingAccount.GetAccountID()

	if s.config.Provider == consts.ProviderTypeAKS {
		resp.AzureAppRegistration = s.getAzureAppRegistration(req.ProjectId)
	}

	return resp, nil
}

// getAzureAppRegistration returns the Azure App Registration for the specified project.
// Note: This function currently pulls from service configuration
// and does not use the `projectId` parameter.
func (s Service) getAzureAppRegistration(projectId string) *regionalapiv1.AzureAppRegistration {
	return &regionalapiv1.AzureAppRegistration{
		AppRegistrationId:   s.config.CMEKConfig.AppRegistrationId,
		AppRegistrationName: s.config.CMEKConfig.AppRegistrationName,
		AppRegistrationUmi:  s.config.CMEKConfig.AppRegistrationUmi,
	}
}
