package one_shot

import (
	"fmt"
	"io"
	"io/ioutil"
	"path/filepath"
	"strings"

	"github.com/creasty/defaults"
	"github.com/hashicorp/go-multierror"
	"github.com/pkg/errors"
	"gopkg.in/yaml.v3"

	comM "github.com/pingcap/test-infra/common/model"
	reqM "github.com/pingcap/test-infra/common/model/tcms/request"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/config"
	ghpkg "github.com/pingcap/test-infra/tools/tcctl/pkg/github_plan"
	imagepkg "github.com/pingcap/test-infra/tools/tcctl/pkg/image"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/model"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/resource"
	transpkg "github.com/pingcap/test-infra/tools/tcctl/pkg/transform"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/util"
)

type OneShot struct {
	ServerConfig config.ApiServerConfig
	ECRServer    string

	ecrImageSyncer imagepkg.ECRImageSyncer
	imageScanner   transpkg.ImageScanner
}

func (o *OneShot) Init() *OneShot {
	o.ecrImageSyncer = imagepkg.ECRImageSyncer{ServerConfig: o.ServerConfig, EcrServer: o.ECRServer, SyncToECRImages: make(map[string]bool)}
	o.imageScanner = transpkg.ImageScanner{ServerConfig: o.ServerConfig}
	return o
}

func (o *OneShot) Run(reader io.Reader, meta *ghpkg.Config, dryrun bool) error {
	data, err := ioutil.ReadAll(reader)
	if err != nil {
		return errors.Wrap(err, "read plan spec error")
	}
	plans, err := o.renderPlans(string(data), meta)
	if err != nil {
		return errors.Wrap(err, "render plans error")
	}
	err = o.collectAndUpdateImagesToECR(plans)
	if err != nil {
		return errors.Wrap(err, "collect images error")
	}
	err = o.syncECRImages()
	if err != nil {
		return errors.Wrap(err, "sync images to ECR error")
	}
	for _, plan := range plans {
		plan, err := resource.CreatePlan(o.ServerConfig, plan, dryrun)
		if err != nil {
			return err
		}
		planExecURL := plan.PlanExecutionURL
		util.LogInfof("ONE_SHOT plan %s will run later, click %s to open it", plan.Name, planExecURL)
	}
	return nil
}

func (o *OneShot) RunOnMetafiles(reader io.Reader, metafiles map[string]*ghpkg.Config, dryrun bool) error {
	data, err := ioutil.ReadAll(reader)
	if err != nil {
		return fmt.Errorf("read plan spec error: %w", err)
	}
	var batchPlans reqM.TriggerCreateRequest
	for fileName, meta := range metafiles {
		plans, err := o.renderPlans(string(data), meta)
		if err != nil {
			return fmt.Errorf("apply plan with meta: %s error: %w", fileName, err)
		}
		if len(plans) > 1 {
			return fmt.Errorf("only support one plan item now")
		}
		if batchPlans.Name == "" {
			batchPlans.Name = plans[0].Name
		}
		for _, plan := range plans {
			inlinePlan := reqM.TriggerCreateRequestInlinePlan{
				TestPlanBase:                 plan.TestPlanBase,
				PlanCreateRequestOneshotSpec: plan.PlanCreateRequestOneshotSpec,
			}
			inlinePlan.Name += "@" + strings.TrimSuffix(filepath.Base(fileName), filepath.Ext(fileName))
			if err := inlinePlan.Validate(); err != nil {
				return fmt.Errorf("validate plan %s error: %w", inlinePlan.Name, err)
			}
			batchPlans.InlinePlans = append(batchPlans.InlinePlans, &inlinePlan)
		}
	}
	resp, err := resource.CreateBatchPlans(o.ServerConfig, batchPlans, dryrun)
	if err != nil {
		return err
	}
	util.LogInfof("batch plans %s(ID=%d) will run later, view it on tcms website", resp.Name, resp.ID)
	return nil
}

func (o *OneShot) renderPlans(planText string, meta *ghpkg.Config) (plans []*reqM.PlanCreateRequest, err error) {
	var metaParameters map[string]string
	if meta != nil {
		metaParameters = meta.Parameters
	} else {
		metaParameters = make(map[string]string, 0)
	}
	var reader io.Reader
	if reader, err = ghpkg.SubstituteVars(planText, metaParameters); err != nil {
		return nil, err
	}
	d := yaml.NewDecoder(reader)
	var index int
	for {
		doc := model.Resource{}
		err := d.Decode(&doc)
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return nil, err
		}
		switch doc.Kind {
		case "Plan":
			p, err := resource.NewTestPlan(o.ServerConfig, doc)
			if err != nil {
				return nil, err
			}
			plan := p.Req
			if plan.Type == "" {
				plan.Type = comM.TestPlanKindOneShot
			}
			if plan.Type != comM.TestPlanKindOneShot {
				util.LogErrorf("item %d isn't a ONE_SHOT plan, ignore it", index)
				index++
				continue
			}
			if err = defaults.Set(&plan); err != nil {
				return nil, err
			}
			plans = append(plans, &plan)
		default:
			util.LogInfof("item %d isn't a plan, ignore it", index)
		}
		index++
	}
	return plans, nil
}

func (o *OneShot) addImageToECR(image string) {
	o.ecrImageSyncer.SyncToECRImages[image] = true
}

func (o *OneShot) collectAndUpdateImagesToECR(plans []*reqM.PlanCreateRequest) error {
	var errs error
	for _, plan := range plans {
		var (
			update  []func(string) string
			collect = func(image string) {}
		)
		if o.shouldSyncImagesToECR(plan) {
			collect, update = o.addImageToECR, []func(string) string{o.ecrImageSyncer.RewriteToEcrImage}
		}
		err := o.imageScanner.CollectAndUpdateImages(plan, collect, update...)
		if err != nil {
			errs = multierror.Append(errs, fmt.Errorf("scan bestbed images on plan %s error: %v", plan.Name, err))
		}
	}
	return errs
}

func (o *OneShot) shouldSyncImagesToECR(plan *reqM.PlanCreateRequest) bool {
	return strings.HasPrefix(strings.ToLower(plan.TestBed.ResourcePool), "aws-") || strings.HasPrefix(strings.ToLower(plan.TestBed.ResourcePool), "eks-")
}

func (o *OneShot) syncECRImages() error {
	return o.ecrImageSyncer.Run()
}
