package ack

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/util/retry"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/yaml"

	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
)

type acr struct {
	*task.BaseTask
}

const (
	//nolint:gosec // not a hardcoded credential
	acrImagePullSecretAggregationName = "acr-credential-secret-aggregation"
	//nolint:gosec // not a hardcoded credential
	acrImagePullSecretPrefix = "acr-credential-"
)

func NewACR(ctx *task.Context) deploy.Task {
	return &acr{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

func (t *acr) Name() string {
	return "ACR"
}

// Update kube-system namespace configmap acr-configuration to add the acr registry info
/* default configmap data:
acr-api-version: "2018-12-01"
acr-registry-info: |
  #- instanceId: ""
expiring-threshold: 15m
service-account: default
watch-namespace: default
*/
// We need to inject the acr registry info into the configmap and update watch-namespace and service-account to watch all namespaces and service accounts
/*
service-account: "*"
watch-namespace: "all"
acr-registry-info: |
 - instanceId: cri-ssvokbjk6x7l5uis
   regionId: ap-southeast-1
*/
func (t *acr) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	cm := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "acr-configuration",
			Namespace: "kube-system",
		},
	}
	remote, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(fmt.Errorf("failed to get remote cluster info: %w", err))
	}
	acrInstanceId := t.Config.APPBoxConfig.ACR.InstanceId
	acrRegion := t.Config.APPBoxConfig.ACR.Region
	domains := t.Config.APPBoxConfig.ACR.Domains
	assumeRoleARN := t.Config.APPBoxConfig.ACR.AssumeRoleARN
	err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
		if err := remote.Get(ctx, client.ObjectKeyFromObject(cm), cm); err != nil {
			return fmt.Errorf("failed to get configmap acr-configuration in kube-system namespace: %w", err)
		}
		registryInfos, err := getRegistryInfos(cm)
		if err != nil {
			return fmt.Errorf("failed to get registry infos from configmap: %w", err)
		}
		instanceExists := false
		for idx, registry := range registryInfos {
			if registry.InstanceId == acrInstanceId {
				instanceExists = true
				registryInfos[idx].RegionId = acrRegion
				registryInfos[idx].Domains = domains
				registryInfos[idx].AssumeRoleARN = assumeRoleARN
				break
			}
		}
		if !instanceExists {
			registryInfos = append(registryInfos, acrRegistryInfo{
				InstanceId:    acrInstanceId,
				RegionId:      acrRegion,
				Domains:       domains,
				AssumeRoleARN: assumeRoleARN,
			})
		}
		data, err := yaml.Marshal(registryInfos)
		if err != nil {
			return fmt.Errorf("failed to marshal registry infos: %w", err)
		}
		// Enable webhook mode to ensure the secret is injected into serviceaccount during creation
		// TODO: Maybe we need to set acr-credential-helper replicas to 2 to avoid single point of failure
		webhookconfig, err := yaml.Marshal(webhookConfig{Enable: true, FailurePolicy: "Ignore", TimeoutSeconds: 10})
		if err != nil {
			return fmt.Errorf("failed to marshal webhook-configuration: %w", err)
		}
		if cm.Data == nil {
			cm.Data = make(map[string]string)
		}
		cm.Data["service-account"] = "*"
		cm.Data["watch-namespace"] = "all"
		cm.Data["acr-registry-info"] = string(data)
		cm.Data["webhook-configuration"] = string(webhookconfig)
		if err := remote.Update(ctx, cm); err != nil {
			return fmt.Errorf("failed to update configmap acr-configuration in kube-system namespace: %w", err)
		}
		return nil
	})
	if err != nil {
		return deploy.Panic(fmt.Errorf("failed to update configmap acr-configuration in kube-system namespace: %w", err))
	}
	return deploy.Done()
}

// Ensure there is at least one valid secret.
// There might be multiple imagePullSecrets.
// One is named `acr-credential-secret-aggregation`.
// Others are named `acr-credential-{hash}`.
func (t *acr) PostCreate(ctx context.Context) deploy.Result {
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(fmt.Errorf("failed to get remote cluster info: %w", err))
	}

	if _, err := getACKACRImagePullSecret(ctx, cli); err != nil {
		// recreate acr-credential-helper pod to trigger recreating a valid secret
		labelSelector := client.MatchingLabels{"app": "aliyun-acr-credential-helper"}
		opts := []client.DeleteAllOfOption{
			labelSelector,
			client.InNamespace("kube-system"),
		}
		if err := client.IgnoreNotFound(cli.DeleteAllOf(ctx, &corev1.Pod{}, opts...)); err != nil {
			return deploy.Panic(err)
		}

		return deploy.Panic(fmt.Errorf("ACR imagePullSecret not ready: %w", err))
	}
	return deploy.Done()
}

// Delete is a no-op for acr task
func (t *acr) Delete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

type webhookConfig struct {
	Enable         bool   `json:"enable"`
	FailurePolicy  string `json:"failure-policy"`
	TimeoutSeconds int    `json:"timeout-seconds"`
}

type acrRegistryInfo struct {
	InstanceId    string `json:"instanceId"`
	RegionId      string `json:"regionId"`
	Domains       string `json:"domains"`
	AssumeRoleARN string `json:"assumeRoleARN"`
}

type dockerconfig struct {
	Auths map[string]interface{} `json:"auths"`
}

func getRegistryInfos(cm *corev1.ConfigMap) ([]acrRegistryInfo, error) {
	acrInfoKey := "acr-registry-info"
	acrInfo, ok := cm.Data[acrInfoKey]
	if !ok || strings.TrimSpace(acrInfo) == "" {
		return nil, nil
	}
	var infos []acrRegistryInfo
	if err := yaml.Unmarshal([]byte(acrInfo), &infos); err != nil {
		return nil, fmt.Errorf("failed to unmarshal acr-registry-info: %w", err)
	}
	return infos, nil
}

func getACKACRImagePullSecret(ctx context.Context, cli client.Client) (corev1.LocalObjectReference, error) {
	secrets := &corev1.SecretList{}
	if err := cli.List(ctx, secrets, client.InNamespace("kube-system")); err != nil {
		return corev1.LocalObjectReference{}, fmt.Errorf("failed to list secrets in kube-system namespace: %w", err)
	}

	candidates := make([]string, 0, len(secrets.Items))
	for _, secret := range secrets.Items {
		if secret.Type != corev1.SecretTypeDockerConfigJson || !strings.HasPrefix(secret.Name, acrImagePullSecretPrefix) {
			continue
		}
		data, ok := secret.Data[corev1.DockerConfigJsonKey]
		if !ok {
			continue
		}
		cfg := &dockerconfig{}
		if err := json.Unmarshal(data, cfg); err != nil || len(cfg.Auths) == 0 {
			continue
		}
		candidates = append(candidates, secret.Name)
	}

	if len(candidates) == 0 {
		return corev1.LocalObjectReference{}, fmt.Errorf("no valid ACR imagePullSecret found in kube-system namespace")
	}

	// Make the selection stable to avoid triggering unnecessary rollouts.
	sort.Strings(candidates)
	selected := candidates[0]
	for _, name := range candidates {
		if name != acrImagePullSecretAggregationName {
			selected = name
			break
		}
	}

	return corev1.LocalObjectReference{Name: selected}, nil
}
