package providerconfig

import (
	"context"
	"fmt"
	"os"
	"sync"

	aliv1beta1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	awsUpboundV1 "github.com/upbound/provider-aws/apis/v1beta1"
	azurev1beta1 "github.com/upbound/provider-azure/apis/v1beta1"
	"gopkg.in/yaml.v2"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

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

const (
	aliCloudSecretKey = "credentials"
	//nolint:gosec // This is a constant used to indicate the OIDC token file source for Azure credentials.
	oidcTokenFile = "OIDCTokenFile"
)

var readAccountList = os.ReadFile

type AWSAccountEntry struct {
	AccountID  string `yaml:"accountID"`
	RoleName   string `yaml:"roleName"`
	ExternalID string `yaml:"externalID,omitempty"`
}

type AliCloudAccountEntry struct {
	AccountID       string `yaml:"accountID"`
	SecretName      string `yaml:"secretName"`
	SecretNamespace string `yaml:"secretNamespace"`
}

type AzureAccountEntry struct {
	SubscriptionID      string `yaml:"subscriptionID"`
	TenantID            string `yaml:"tenantID"`
	ClientID            string `yaml:"clientID"`
	BucketResourceGroup string `yaml:"bucketResourceGroup,omitempty"`
}

var azureAccountCache struct {
	mu       sync.RWMutex
	accounts []AzureAccountEntry
}

func SetAzureAccountCache(accounts []AzureAccountEntry) {
	azureAccountCache.mu.Lock()
	defer azureAccountCache.mu.Unlock()
	azureAccountCache.accounts = append([]AzureAccountEntry(nil), accounts...)
}

func GetAzureAccountCache(subscriptionID string) (string, bool) {
	azureAccountCache.mu.RLock()
	defer azureAccountCache.mu.RUnlock()
	for _, account := range azureAccountCache.accounts {
		if account.SubscriptionID == subscriptionID {
			return account.BucketResourceGroup, account.BucketResourceGroup != ""
		}
	}
	return "", false
}

// LoadAzureAccountList loads Azure account entries from the account list config.
func LoadAzureAccountList(path string) ([]AzureAccountEntry, error) {
	data, err := readAccountList(path)
	if err != nil {
		return nil, err
	}

	var list struct {
		Accounts []AzureAccountEntry `yaml:"accounts"`
	}
	if err := yaml.Unmarshal(data, &list); err != nil {
		return nil, err
	}

	return list.Accounts, nil
}

type Initializer interface {
	InitializeProviderConfig(
		ctx context.Context, c client.Client, accountsPath string, rootConfig *config.RootConfig,
	) error
}

type awsInitializer struct{}

func NewAWSInitializer() Initializer {
	return &awsInitializer{}
}

func (m *awsInitializer) InitializeProviderConfig(
	ctx context.Context, c client.Client, accountsPath string, rootConfig *config.RootConfig,
) error {
	log := ctrl.Log.WithName("init-providerconfig")
	dnsConfig := &rootConfig.DNSProviderConfig
	accounts, err := m.loadAccountList(accountsPath)
	if err != nil {
		return fmt.Errorf("load account list: %w", err)
	}

	for _, entry := range accounts {
		if err := m.ensureProviderConfig(ctx, c, &entry); err != nil {
			return fmt.Errorf("upbound ProviderConfig for %s failed: %w", entry.AccountID, err)
		}

		log.Info("Ensured provider config", "accountID", entry.AccountID)
	}

	if err := m.ensureDnsProviderConfig(ctx, c, &dnsConfig.AwsRoute53); err != nil {
		return fmt.Errorf("upbound ProviderConfig for DNS %s failed: %w", dnsConfig.AwsRoute53.AccountID, err)
	}

	log.Info("Ensured DNS provider config", "accountID", dnsConfig.AwsRoute53.AccountID)
	return nil
}

func (m *awsInitializer) loadAccountList(path string) ([]AWSAccountEntry, error) {
	data, err := readAccountList(path)
	if err != nil {
		return nil, err
	}

	var list struct {
		Accounts []AWSAccountEntry `yaml:"accounts"`
	}
	if err := yaml.Unmarshal(data, &list); err != nil {
		return nil, err
	}

	return list.Accounts, nil
}

func (m *awsInitializer) ensureProviderConfig(
	ctx context.Context, c client.Client, account *AWSAccountEntry,
) error {
	pc := &awsUpboundV1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: account.AccountID,
		},
	}

	_, err := controllerutil.CreateOrUpdate(ctx, c, pc, func() error {
		assumeRole := awsUpboundV1.AssumeRoleOptions{
			RoleARN: ptr.To(fmt.Sprintf("arn:aws:iam::%s:role/%s", account.AccountID, account.RoleName)),
		}

		if account.ExternalID != "" {
			assumeRole.ExternalID = ptr.To(account.ExternalID)
		}

		pc.Spec = awsUpboundV1.ProviderConfigSpec{
			AssumeRoleChain: []awsUpboundV1.AssumeRoleOptions{assumeRole},
			Credentials: awsUpboundV1.ProviderCredentials{
				Source: "IRSA",
			},
		}
		return nil
	})

	return err
}

func (m *awsInitializer) ensureDnsProviderConfig(
	ctx context.Context, c client.Client, dnsConf *config.AwsRoute53Config,
) error {
	pc := &awsUpboundV1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: "dns-" + dnsConf.AccountID,
		},
	}

	_, err := controllerutil.CreateOrUpdate(ctx, c, pc, func() error {
		awsRoleArn := os.Getenv("AWS_ROLE_ARN")

		assumeRole := awsUpboundV1.AssumeRoleOptions{
			RoleARN: &dnsConf.RoleArn,
		}
		if dnsConf.ExternalId != "" {
			assumeRole.ExternalID = &dnsConf.ExternalId
		}

		if awsRoleArn == dnsConf.RoleArn {
			pc.Spec = awsUpboundV1.ProviderConfigSpec{
				Credentials: awsUpboundV1.ProviderCredentials{
					Source: "IRSA",
				},
			}
		} else {
			pc.Spec = awsUpboundV1.ProviderConfigSpec{
				Credentials: awsUpboundV1.ProviderCredentials{
					Source: "IRSA",
				},
				AssumeRoleChain: []awsUpboundV1.AssumeRoleOptions{assumeRole},
			}
		}

		return nil
	})

	return err
}

type aliCloudInitializer struct{}

func NewAliCloudInitializer() Initializer {
	return &aliCloudInitializer{}
}

func (m *aliCloudInitializer) InitializeProviderConfig(
	ctx context.Context, c client.Client, accountsPath string, rootConfig *config.RootConfig,
) error {
	log := ctrl.Log.WithName("init-providerconfig")
	dnsConfig := &rootConfig.DNSProviderConfig
	accounts, err := m.loadAccountList(accountsPath)
	if err != nil {
		return fmt.Errorf("load account list: %w", err)
	}

	for _, entry := range accounts {
		if err := m.ensureProviderConfig(ctx, c, &entry); err != nil {
			return fmt.Errorf("upbound ProviderConfig for %s failed: %w", entry.AccountID, err)
		}

		log.Info("Ensured provider config", "accountID", entry.AccountID)
	}

	if err := m.ensureDnsProviderConfig(ctx, c, &dnsConfig.AliCloudDNS); err != nil {
		return fmt.Errorf("upbound ProviderConfig for DNS %s failed: %w", dnsConfig.AliCloudDNS.AccountID, err)
	}

	log.Info("Ensured DNS provider config", "accountID", dnsConfig.AliCloudDNS.AccountID)
	return nil
}

func (m *aliCloudInitializer) loadAccountList(path string) ([]AliCloudAccountEntry, error) {
	data, err := readAccountList(path)
	if err != nil {
		return nil, err
	}

	var list struct {
		Accounts []AliCloudAccountEntry `yaml:"accounts"`
	}
	if err := yaml.Unmarshal(data, &list); err != nil {
		return nil, err
	}

	return list.Accounts, nil
}

func (m *aliCloudInitializer) ensureProviderConfig(
	ctx context.Context, c client.Client, account *AliCloudAccountEntry,
) error {
	pc := &aliv1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: account.AccountID,
		},
	}

	_, err := controllerutil.CreateOrUpdate(ctx, c, pc, func() error {
		pc.Spec.Credentials = aliv1beta1.ProviderCredentials{
			Source: xpv1.CredentialsSourceSecret,
			CommonCredentialSelectors: xpv1.CommonCredentialSelectors{
				SecretRef: &xpv1.SecretKeySelector{
					SecretReference: xpv1.SecretReference{
						Name:      account.SecretName,
						Namespace: account.SecretNamespace,
					},
					Key: aliCloudSecretKey,
				},
			},
		}
		return nil
	})

	return err
}

func (m *aliCloudInitializer) ensureDnsProviderConfig(
	ctx context.Context, c client.Client, dnsConf *config.AliCloudDNSConfig,
) error {
	pc := &aliv1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: "dns-" + dnsConf.AccountID,
		},
	}

	_, err := controllerutil.CreateOrUpdate(ctx, c, pc, func() error {
		pc.Spec.Credentials = aliv1beta1.ProviderCredentials{
			Source: xpv1.CredentialsSourceSecret,
			CommonCredentialSelectors: xpv1.CommonCredentialSelectors{
				SecretRef: &xpv1.SecretKeySelector{
					SecretReference: xpv1.SecretReference{
						Name:      dnsConf.SecretName,
						Namespace: dnsConf.SecretNamespace,
					},
					Key: aliCloudSecretKey,
				},
			},
		}
		return nil
	})

	return err
}

type azureInitializer struct{}

func NewAzureInitializer() Initializer {
	return &azureInitializer{}
}

func (m *azureInitializer) InitializeProviderConfig(
	ctx context.Context, c client.Client, accountsPath string, rootConfig *config.RootConfig,
) error {
	log := ctrl.Log.WithName("init-providerconfig")
	dnsConfig := &rootConfig.DNSProviderConfig
	byocConfig := &rootConfig.BYOC
	accounts, err := m.loadAccountList(accountsPath)
	if err != nil {
		return fmt.Errorf("load account list: %w", err)
	}
	SetAzureAccountCache(accounts)

	for _, entry := range accounts {
		if err := m.ensureProviderConfig(ctx, c, &entry); err != nil {
			return fmt.Errorf("upbound ProviderConfig for %s failed: %w", entry.SubscriptionID, err)
		}

		log.Info("Ensured provider config", "accountID", entry.SubscriptionID)
	}

	if err := m.ensureDnsProviderConfig(ctx, c, &dnsConfig.AzureDNSZone); err != nil {
		return fmt.Errorf("upbound ProviderConfig for DNS %s failed: %w", dnsConfig.AzureDNSZone.SubscriptionID, err)
	}

	log.Info("Ensured DNS provider config", "accountID", dnsConfig.AzureDNSZone.SubscriptionID)

	if byocConfig.AzureGatewayDNSZone.IsConfigured() {
		if err := m.ensureDnsProviderConfig(ctx, c, &byocConfig.AzureGatewayDNSZone); err != nil {
			return fmt.Errorf("upbound ProviderConfig for BYOC gateway DNS %s failed: %w", byocConfig.AzureGatewayDNSZone.SubscriptionID, err)
		}
		log.Info("Ensured BYOC gateway DNS provider config", "accountID", byocConfig.AzureGatewayDNSZone.SubscriptionID)
	}

	if byocConfig.AzureAPIServerDNSZone.IsConfigured() {
		if err := m.ensureDnsProviderConfig(ctx, c, &byocConfig.AzureAPIServerDNSZone); err != nil {
			return fmt.Errorf("upbound ProviderConfig for API server DNS %s failed: %w", byocConfig.AzureAPIServerDNSZone.SubscriptionID, err)
		}
		log.Info("Ensured API server DNS provider config", "accountID", byocConfig.AzureAPIServerDNSZone.SubscriptionID)
	}
	return nil
}

func (m *azureInitializer) loadAccountList(path string) ([]AzureAccountEntry, error) {
	return LoadAzureAccountList(path)
}

func (m *azureInitializer) ensureProviderConfig(
	ctx context.Context, c client.Client, account *AzureAccountEntry,
) error {
	pc := &azurev1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: account.SubscriptionID,
		},
	}

	_, err := controllerutil.CreateOrUpdate(ctx, c, pc, func() error {
		setAzureProviderConfigSpec(pc, account.SubscriptionID, account.TenantID, account.ClientID)
		return nil
	})

	return err
}

func (m *azureInitializer) ensureDnsProviderConfig(
	ctx context.Context, c client.Client, dnsConf *config.AzureDNSZoneConfig,
) error {
	pc := &azurev1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: "dns-" + dnsConf.SubscriptionID,
		},
	}
	_, err := controllerutil.CreateOrUpdate(ctx, c, pc, func() error {
		setAzureProviderConfigSpec(pc, dnsConf.SubscriptionID, dnsConf.TenantID, dnsConf.ClientID)
		return nil
	})
	return err
}

func setAzureProviderConfigSpec(pc *azurev1beta1.ProviderConfig, subscriptionID, tenantID, clientID string) {
	pc.Spec.Credentials = azurev1beta1.ProviderCredentials{
		Source: oidcTokenFile,
	}
	pc.Spec.SubscriptionID = ptr.To(subscriptionID)
	pc.Spec.TenantID = ptr.To(tenantID)
	pc.Spec.ClientID = ptr.To(clientID)
}
