package image

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"
	"time"

	imageSyncer "github.com/AliyunContainerService/image-syncer/pkg/client"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ecr"
	"github.com/aws/aws-sdk-go-v2/service/ecr/types"
	"github.com/pkg/errors"
	"gopkg.in/yaml.v3"
	"k8s.io/utils/pointer"

	commonErrors "github.com/pingcap/test-infra/common/errors"
	"github.com/pingcap/test-infra/common/model/rms/response"
	configpkg "github.com/pingcap/test-infra/tools/tcctl/pkg/config"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/util"
)

const (
	ecrRegion = "us-west-2"
)

type ECRImageSyncer struct {
	ServerConfig    configpkg.ApiServerConfig
	EcrServer       string
	SyncToECRImages map[string]bool

	ecrClient *ecr.Client
}

func (e *ECRImageSyncer) Run() error {
	if len(e.SyncToECRImages) == 0 {
		return nil
	}
	err := e.initEcrClient()
	if err != nil {
		return fmt.Errorf("init ECR client error: %v", err)
	}
	err = e.createRepository()
	if err != nil {
		return fmt.Errorf("create repository error: %v", err)
	}
	syncer, err := e.buildSyncerClient()
	if err != nil {
		return fmt.Errorf("build syncer client error: %v", err)
	}
	syncer.Run()
	return nil
}

func (e *ECRImageSyncer) initEcrClient() error {
	credential, err := e.getTemporaryECRImageSyncCredential()
	if err != nil {
		return err
	}
	os.Setenv("AWS_ACCESS_KEY_ID", *credential.AccessKeyId)
	os.Setenv("AWS_SECRET_ACCESS_KEY", *credential.SecretAccessKey)
	os.Setenv("AWS_SESSION_TOKEN", *credential.SessionToken)
	os.Setenv("AWS_REGION", ecrRegion)

	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return err
	}
	e.ecrClient = ecr.NewFromConfig(cfg)
	return nil
}

func (e *ECRImageSyncer) createRepository() error {
	getRepoName := func(image string) string {
		return strings.Split(strings.SplitN(image, "/", 2)[1], ":")[0]
	}
	repoMap := make(map[string]bool)
	for image := range e.SyncToECRImages {
		repoMap[getRepoName(image)] = true
	}
	for repo := range repoMap {
		_, err := e.ecrClient.CreateRepository(context.TODO(), &ecr.CreateRepositoryInput{
			RepositoryName: pointer.StringPtr(repo),
			Tags: []types.Tag{{
				Key:   pointer.StringPtr("usedby"),
				Value: pointer.StringPtr("test-infra"),
			}},
		})
		if err != nil {
			var raee *types.RepositoryAlreadyExistsException
			if errors.As(err, &raee) {
				continue
			}
			return err
		}
	}
	return nil
}

func (e *ECRImageSyncer) RewriteToEcrImage(image string) string {
	_, repo, tag := ParseImage(image)
	return fmt.Sprintf("%s/%s:%s", e.EcrServer, repo, tag)
}

func (e *ECRImageSyncer) buildSyncerClient() (*imageSyncer.Client, error) {
	completeImage := func(image string) string {
		registry, repo, tag := ParseImage(image)
		return fmt.Sprintf("%s/%s:%s", registry, repo, tag)
	}
	imageMap := make(map[string]string)
	for image := range e.SyncToECRImages {
		image = completeImage(image)
		imageMap[image] = e.RewriteToEcrImage(image)
	}
	user, password, err := e.getEcrAuthToken()
	if err != nil {
		return nil, err
	}
	config := imageSyncer.Config{
		AuthList: map[string]imageSyncer.Auth{
			e.EcrServer: {
				Username: user,
				Password: password,
			},
		},
		ImageList: imageMap,
	}
	file, err := ioutil.TempFile("/tmp", "image-syncer.*.yaml")
	if err != nil {
		return nil, err
	}
	output, err := yaml.Marshal(config)
	if err != nil {
		return nil, err
	}
	if err = ioutil.WriteFile(file.Name(), output, 0600); err != nil {
		return nil, err
	}
	return imageSyncer.NewSyncClient(file.Name(), "", "", "", 6, 3, "", "", []string{}, []string{})
}

func (e *ECRImageSyncer) getTemporaryECRImageSyncCredential() (*response.GetTemporaryECRImageSyncCredentialResponse, error) {
	r, err := util.Req().
		Host(e.ServerConfig.RmsHost).
		Path("api/v1/credential/aws-ecr").
		Bearer(e.ServerConfig.Token).
		Client(&http.Client{Timeout: 10 * time.Second}).
		Get()
	if err != nil {
		return nil, err
	}
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return nil, err
	}
	switch r.StatusCode {
	case http.StatusOK:
		res := &response.GetTemporaryECRImageSyncCredentialResponse{}
		if err := json.Unmarshal(b, res); err != nil {
			return nil, err
		}
		return res, nil
	}
	statusErr := new(commonErrors.StatusError)
	if err := json.Unmarshal(b, statusErr); err != nil {
		return nil, err
	}
	return nil, statusErr
}

func (e *ECRImageSyncer) getEcrAuthToken() (user, password string, err error) {
	output, err := e.ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{})
	if err != nil {
		return
	}
	rawDecodedText, err := base64.StdEncoding.DecodeString(*output.AuthorizationData[0].AuthorizationToken)
	if err != nil {
		return
	}
	user = strings.Split(string(rawDecodedText), ":")[0]
	password = strings.Split(string(rawDecodedText), ":")[1]
	return
}
