package transform

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/goccy/go-yaml"
	"github.com/hashicorp/go-multierror"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	common "github.com/pingcap/test-infra/common/model"
	mResource "github.com/pingcap/test-infra/common/model/resource"
	reqM "github.com/pingcap/test-infra/common/model/tcms/request"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/config"
	imagepkg "github.com/pingcap/test-infra/tools/tcctl/pkg/image"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/util"
)

type ImageScanner struct {
	ServerConfig config.ApiServerConfig
}

func (s *ImageScanner) ScanImagesOnTestbed(testbed *common.TestBed, imageUpdater ...func(image string) string) (map[string]bool, error) {
	var errs error
	result := make(map[string]bool)
	scanComponent := func(component map[string]interface{}, found bool, err error, getter func(component map[string]interface{}) string, updateBy func(image string) error) error {
		if err != nil {
			return err
		}
		if !found {
			return nil
		}
		image := getter(component)
		result[image] = true
		if len(imageUpdater) == 0 {
			return nil
		}
		image = imageUpdater[0](image)
		return updateBy(image)
	}
	for index := range testbed.Items {
		item := &testbed.Items[index]
		spec := item.Spec
		rawSpecObj := map[string]interface{}{}
		err := json.Unmarshal(item.Spec, &rawSpecObj)
		if err != nil {
			return nil, err
		}
		testbedSpec := new(unstructured.Unstructured)
		testbedSpec.Object = rawSpecObj
		switch item.Type {
		case mResource.ResourceTypeTiDBCluster:
			version, found, err := unstructured.NestedString(testbedSpec.Object, "version")
			if err != nil {
				return nil, err
			}
			if !found {
				return nil, fmt.Errorf("version not set on %s testbed: %s", mResource.ResourceTypeTiDBCluster, item.Name)
			}
			paths := [][]string{{"pd"}, {"tidb"}, {"tikv"}, {"tiflash"}, {"ticdc"}, {"pump"}, {"monitor", "initializer"}}
			for _, path := range paths {
				c, f, e := unstructured.NestedMap(testbedSpec.Object, path...)
				err = scanComponent(c, f, e,
					func(component map[string]interface{}) string {
						version := version
						if v, ok := component["version"]; ok {
							version = v.(string)
						}
						image := component["baseImage"].(string)
						if version != "" {
							image = fmt.Sprintf("%s:%s", image, version)
						}
						return image
					},
					func(image string) error {
						registry, repo, tag := imagepkg.ParseImage(image)
						err := unstructured.SetNestedField(testbedSpec.Object, fmt.Sprintf("%s/%s", registry, repo), append(path, "baseImage")...)
						if err != nil {
							return err
						}
						return unstructured.SetNestedField(testbedSpec.Object, tag, append(path, "version")...)
					})
				if err != nil {
					errs = multierror.Append(errs, err)
					continue
				}
			}
		case mResource.ResourceTypeWorkloadNode:
			c, f, e := unstructured.NestedMap(testbedSpec.Object, "container")
			err := scanComponent(c, f, e,
				func(component map[string]interface{}) string {
					return component["image"].(string)
				},
				func(image string) error {
					return unstructured.SetNestedField(testbedSpec.Object, image, "container", "image")
				},
			)
			if err != nil {
				errs = multierror.Append(errs, err)
				continue
			}
		case mResource.ResourceTypeTiDBLightning, mResource.ResourceTypeMySQLNode, mResource.ResourceTypeBRNode:
			c, f, e := unstructured.NestedMap(testbedSpec.Object)
			err := scanComponent(c, f, e,
				func(component map[string]interface{}) string {
					return component["image"].(string)
				},
				func(image string) error {
					return unstructured.SetNestedField(testbedSpec.Object, image, "image")
				},
			)
			if err != nil {
				errs = multierror.Append(errs, err)
				continue
			}
		case mResource.ResourceTypeDrainer:
			c, f, e := unstructured.NestedMap(testbedSpec.Object)
			err := scanComponent(c, f, e,
				func(component map[string]interface{}) string {
					return fmt.Sprintf("%s:%s", component["baseImage"].(string), component["clusterVersion"].(string))
				},
				func(image string) error {
					registry, repo, tag := imagepkg.ParseImage(image)
					err := unstructured.SetNestedField(testbedSpec.Object, fmt.Sprintf("%s/%s", registry, repo), "baseImage")
					if err != nil {
						return err
					}
					return unstructured.SetNestedField(testbedSpec.Object, tag, "clusterVersion")
				},
			)
			if err != nil {
				errs = multierror.Append(errs, err)
				continue
			}
		case mResource.ResourceTypeDMCluster:
			version, found, err := unstructured.NestedString(testbedSpec.Object, "version")
			if err != nil {
				return nil, err
			}
			if !found {
				return nil, fmt.Errorf("version not set on %s testbed: %s", mResource.ResourceTypeDMCluster, item.Name)
			}
			paths := [][]string{{"worker"}, {"master"}}
			for _, path := range paths {
				c, f, e := unstructured.NestedMap(testbedSpec.Object, path...)
				err = scanComponent(c, f, e,
					func(component map[string]interface{}) string {
						image := component["baseImage"].(string)
						if version != "" {
							image = fmt.Sprintf("%s:%s", image, version)
						}
						return image
					},
					func(image string) error {
						registry, repo, _ := imagepkg.ParseImage(image)
						return unstructured.SetNestedField(testbedSpec.Object, fmt.Sprintf("%s/%s", registry, repo), append(path, "baseImage")...)
					})
				if err != nil {
					errs = multierror.Append(errs, err)
					continue
				}
			}
		case mResource.ResourceTypeKafkaCluster:
			paths := [][]string{{"zkspec"}, {"kafkaspec"}}
			for _, path := range paths {
				c, f, e := unstructured.NestedMap(testbedSpec.Object, path...)
				err = scanComponent(c, f, e,
					func(component map[string]interface{}) string {
						return component["image"].(string)
					},
					func(image string) error {
						return unstructured.SetNestedField(testbedSpec.Object, image, append(path, "image")...)
					})
				if err != nil {
					errs = multierror.Append(errs, err)
					continue
				}
			}
		}
		if len(imageUpdater) != 0 {
			spec, err = json.Marshal(testbedSpec.Object)
			if err != nil {
				return nil, err
			}
			item.Spec = spec
		}
	}
	return result, errs
}

func (s *ImageScanner) ScanImagesOnCases(plan *reqM.PlanCreateRequest, imageUpdater ...func(image string) string) (map[string]bool, error) {
	var errs error
	result := make(map[string]bool)
	apiPath := "api/v1/cases"
	caseMap := make(map[string]*common.TestCaseBase)

	var cases []string
	for _, step := range plan.Pipeline {
		cases = append(cases, step.CaseName)
	}
	for _, step := range plan.Steps {
		cases = append(cases, step.CaseName)
	}
	for idx := range plan.PlanCreateRequestOneshotSpec.InlineCases {
		item := plan.PlanCreateRequestOneshotSpec.InlineCases[idx]
		caseMap[item.Name] = item
	}
	// 100 here will be paramised
	for i, j := 0, 100; i < len(cases); i, j = j, j+100 {
		if j > len(cases) {
			j = len(cases)
		}
		resp, err := util.Req().Host(s.ServerConfig.TcmsHost).Bearer(s.ServerConfig.Token).Path(apiPath).Param(map[string]string{"names": strings.Join(cases[i:j], ","), "count": fmt.Sprintf("%d", j-i), "tcctl": "1"}).Get()
		if err != nil {
			return nil, err
		}
		if resp.StatusCode != 200 {
			content, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, err
			}
			return nil, util.HttpError{Code: resp.StatusCode, Url: resp.Request.URL.String(), Content: string(content)}
		}
		d := yaml.NewDecoder(resp.Body)
		for {
			testcase := common.TestCaseBase{}
			err = d.Decode(&testcase)
			if errors.Is(err, io.EOF) {
				break
			}
			if err != nil {
				return nil, err
			}
			caseMap[testcase.Name] = &testcase
		}
	}
	for _, caseName := range cases {
		if _, exist := caseMap[caseName]; !exist {
			errs = multierror.Append(errs, fmt.Errorf("case `%s` isn't exist", caseName))
		}
	}
	if errs != nil {
		return nil, errs
	}
	for _, kase := range caseMap {
		result[kase.Framework.Generic.Image] = true
	}
	if len(imageUpdater) == 0 {
		return result, nil
	}
	for index := range plan.Pipeline {
		step := &plan.Pipeline[index]
		step.CaseImage = imageUpdater[0](caseMap[step.CaseName].Framework.Generic.Image)
	}
	for index := range plan.Steps {
		step := &plan.Steps[index]
		step.CaseImage = imageUpdater[0](caseMap[step.CaseName].Framework.Generic.Image)
	}
	for index := range plan.PlanCreateRequestOneshotSpec.InlineCases {
		item := plan.PlanCreateRequestOneshotSpec.InlineCases[index]
		item.Framework.Generic.Image = imageUpdater[0](item.Framework.Generic.Image)
	}
	return result, nil
}

func (s *ImageScanner) CollectAndUpdateImages(plan *reqM.PlanCreateRequest, collectFunc func(image string), updateFunc ...func(image string) string) error {
	images, err := s.ScanImagesOnTestbed(&plan.TestBed, updateFunc...)
	if err != nil {
		return err
	}
	images, err = s.ScanImagesOnCases(plan, updateFunc...)
	if err != nil {
		return err
	}
	for image := range images {
		collectFunc(image)
	}
	return nil
}
