package ghpkg

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/creasty/defaults"
	"github.com/google/go-jsonnet"
	"github.com/hashicorp/go-multierror"
	"gopkg.in/yaml.v3"

	"github.com/pkg/errors"
	"github.com/valyala/fasttemplate"

	common "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"
	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"
)

var vm = jsonnet.MakeVM()

type Syncer struct {
	PlansDir         string
	Git              *GitTool
	ServerConfig     config.ApiServerConfig
	ECRServer        string
	ARMMode          bool
	OnlySyncEcrImage bool
	MetafilePath     string

	config *Config
	// plans and templatePlans
	plans map[string]*reqM.PlanCreateRequest
	// templatePlans means plans which generated by the template files
	templatePlans map[string][]*reqM.PlanCreateRequest

	arm64ImageChecker imagepkg.ARM64ImageChecker
	ecrImageSyncer    imagepkg.ECRImageSyncer
	imageScanner      transpkg.ImageScanner
}

func (s *Syncer) Init() *Syncer {
	s.plans = make(map[string]*reqM.PlanCreateRequest)
	s.templatePlans = make(map[string][]*reqM.PlanCreateRequest)
	s.arm64ImageChecker = imagepkg.ARM64ImageChecker{ImageExistCache: map[string]error{}}
	s.ecrImageSyncer = imagepkg.ECRImageSyncer{ServerConfig: s.ServerConfig, EcrServer: s.ECRServer, SyncToECRImages: make(map[string]bool)}
	s.imageScanner = transpkg.ImageScanner{ServerConfig: s.ServerConfig}
	return s
}

func (s *Syncer) indexPlans() error {
	if err := s.readMetaFile(); err != nil {
		return errors.Wrapf(err, "read the meta file error, please check the %s", s.MetafilePath)
	}
	if err := s.scanPlans(SubstituteVars); err != nil {
		return errors.Wrap(err, "scan plans error")
	}
	if err := s.attachGithubInfo(); err != nil {
		return errors.Wrap(err, "attach github info error")
	}
	if err := s.checkARM64ArchSupport(); err != nil {
		return errors.Wrapf(err, "check the ARM64 arch support error")
	}
	if err := s.patchPlans(); err != nil {
		return errors.Wrap(err, "modify plans error")
	}
	if err := s.renamePlans(); err != nil {
		return errors.Wrap(err, "rename plans error")
	}
	if err := s.validate(); err != nil {
		return errors.Wrap(err, "validate plans error")
	}
	return nil
}

func (s *Syncer) Sync(dryrun bool, removeOldPlans bool) error {
	err := s.indexPlans()
	if err != nil {
		return errors.Wrap(err, "index plans error")
	}
	err = s.collectAndUpdateImagesToECR()
	if err != nil {
		return errors.Wrapf(err, "collect images error")
	}
	err = s.syncECRImages()
	if err != nil {
		return errors.Wrap(err, "sync images to ECR error")
	}
	if s.OnlySyncEcrImage {
		return nil
	}
	err = s.submitPlans(dryrun)
	if err != nil {
		return errors.Wrap(err, "submit plans error")
	}
	if dryrun || !removeOldPlans {
		return nil
	}
	err = s.removeOldPlans()
	if err != nil {
		return errors.Wrap(err, "remove old plans error")
	}
	return nil
}

func (s *Syncer) Check() error {
	err := s.indexPlans()
	if err != nil {
		return errors.Wrap(err, "index plans error")
	}
	err = s.collectAndUpdateImagesToECR()
	if err != nil {
		return errors.Wrapf(err, "collect images error")
	}
	return nil
}

func (s *Syncer) readMetaFile() error {
	c, err := ReadConfigFile(path.Join(s.PlansDir, s.MetafilePath))
	if err != nil {
		return err
	}
	var errs error
	if err = checkPlanPathOrNameListExistence(c.ReleaseList); err != nil {
		errs = multierror.Append(errs, fmt.Errorf("check the existence of release list error: %v", err))
	}
	if err = checkPlanPathOrNameListExistence(c.Whitelist); err != nil {
		errs = multierror.Append(errs, fmt.Errorf("check the existence of whitelist error: %v", err))
	}
	if err = checkPlanPathOrNameListExistence(c.Blacklist); err != nil {
		errs = multierror.Append(errs, fmt.Errorf("check the existence of blacklist error: %v", err))
	}
	s.config = c
	for _, image := range s.config.SyncToECRImages {
		s.addImageToECR(image)
	}
	return errs
}

// scanPlans travels plans and records the (path, plan) pairs
func (s *Syncer) scanPlans(varsReplacer func(template string, parameters map[string]string) (io.Reader, error)) (err error) {
	var files []string
	if err := filepath.Walk(s.PlansDir, func(path string, f os.FileInfo, err error) error {
		if f.IsDir() {
			// skip the dir that contains a versions file, ignore the multi version plan dir since
			// it's only supported on_shot type now, TODO: boost it scale on future
			if _, err := os.Stat(filepath.Join(path, "versions")); err == nil {
				return filepath.SkipDir
			}
			return nil
		}
		if filepath.Ext(path) != ".yaml" && !strings.HasSuffix(path, tplJsonnetExt) {
			return nil
		}
		files = append(files, path)
		return nil
	}); err != nil {
		return err
	}

	addPlans := func(f string, doc model.Resource) error {
		switch doc.Kind {
		case "Plan":
			p, err := resource.NewTestPlan(s.ServerConfig, doc)
			if err != nil {
				return err
			}
			plan := p.Req
			if plan.Type != "" && plan.Type != common.TestPlanKindGithub {
				util.LogErrorf("%s isn't a GITHUB plan, ignore it", f)
				return nil
			}
			plan.Type = common.TestPlanKindGithub
			relPath, err := filepath.Rel(s.PlansDir, f)
			if err != nil {
				return err
			}
			err = defaults.Set(&plan)
			if err != nil {
				return err
			}
			if plan.Priority != 0 {
				return fmt.Errorf("plan %s: priority should be empty", f)
			}
			if s.onRecycleBin(relPath, &plan) {
				return nil
			}
			switch {
			case filepath.Ext(f) == ".yaml":
				s.plans[relPath] = &plan
			case strings.HasSuffix(f, tplJsonnetExt):
				s.templatePlans[relPath] = append(s.templatePlans[relPath], &plan)
			}
		default:
			util.LogInfof("%s isn't a plan, ignore it", f)
		}
		return nil
	}

	for _, f := range files {
		reader, err := util.ReadFromFile(f)
		if err != nil {
			return errors.Wrapf(err, "read file %s error", f)
		}
		b, err := ioutil.ReadAll(reader)
		if err != nil {
			return errors.Wrapf(err, "read file %s error", f)
		}
		if reader, err = varsReplacer(string(b), s.config.Parameters); err != nil {
			return errors.Wrapf(err, "parse file %s error", f)
		}

		switch {
		case filepath.Ext(f) == ".yaml":
			d := yaml.NewDecoder(reader)
			for {
				doc := model.Resource{}
				err := d.Decode(&doc)
				if errors.Is(err, io.EOF) {
					break
				}
				if err != nil {
					return errors.Wrapf(err, "read file %s error", f)
				}
				if err := addPlans(f, doc); err != nil {
					return err
				}
			}
		case strings.HasSuffix(f, tplJsonnetExt):
			code, err := ioutil.ReadAll(reader)
			if err != nil {
				return err
			}
			jsonStr, err := vm.EvaluateAnonymousSnippet(f, string(code))
			if err != nil {
				return errors.Wrapf(err, "eval jsonnet file `%s` error", f)
			}
			var values interface{}
			if err := json.Unmarshal([]byte(jsonStr), &values); err != nil {
				return errors.Wrapf(err, "unmarshal plan template `%s` error", f)
			}
			switch v := values.(type) {
			case []interface{}:
				for _, value := range v {
					str, err := yaml.Marshal(value)
					if err != nil {
						return err
					}
					doc := model.Resource{}
					if err := yaml.Unmarshal(str, &doc); err != nil {
						return err
					}
					if err := addPlans(f, doc); err != nil {
						return err
					}
				}
			default:
				str, err := yaml.Marshal(v)
				if err != nil {
					return err
				}
				doc := model.Resource{}
				if err := yaml.Unmarshal(str, &doc); err != nil {
					return err
				}
				if err := addPlans(f, doc); err != nil {
					return err
				}
			}
		}
	}
	return
}

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

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

func (s *Syncer) syncECRImages() error {
	return s.ecrImageSyncer.Run()
}

func (s *Syncer) checkARM64ArchSupport() error {
	if !s.ARMMode {
		return nil
	}
	var errs error
	for path, plan := range s.plans {
		if err := s.checkARM64ImageExist(plan); err != nil {
			errs = multierror.Append(errs, errors.Wrapf(err, "check arm64 image existence for plan %s(%s) failed", path, plan.Name))
		}
	}
	for path, plans := range s.templatePlans {
		for _, plan := range plans {
			if err := s.checkARM64ImageExist(plan); err != nil {
				errs = multierror.Append(errs, errors.Wrapf(err, "check arm64 image existence for plan %s(%s) failed", path, plan.Name))
			}
		}
	}
	return errs
}

func (s *Syncer) checkARM64ImageExist(plan *reqM.PlanCreateRequest) error {
	images, err := s.imageScanner.ScanImagesOnTestbed(&plan.TestBed)
	if err != nil {
		return err
	}
	var errs error
	for image := range images {
		if err := s.arm64ImageChecker.IsSupport(image); err != nil {
			errs = multierror.Append(errs, err)
		}
	}
	return errs
}

func (s *Syncer) patchPlans() error {
	branch, err := s.Git.GetBranch()
	if err != nil {
		return err
	}
	branchType := GetBranchType(branch)
	if err := s.patchPlanSchedule(branch, branchType); err != nil {
		return fmt.Errorf("patch plan schedule error: %w", err)
	}
	if err := s.patchPlanPriorities(); err != nil {
		return fmt.Errorf("patch plan priority error: %w", err)
	}
	if err := s.patchPlanReleaseTag(); err != nil {
		return fmt.Errorf("patch plan release tag error: %w", err)
	}
	if err := s.patchResourcePool(); err != nil {
		return fmt.Errorf("patch the resource pool error: %w", err)
	}
	return nil
}

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

func (s *Syncer) patchPlanSchedule(branch string, branchType BranchType) error {
	switch branchType {
	case BranchTypeMain:
		if s.config.DisableSchedule {
			s.disableScheduleAll()
			return nil
		}
		return s.eraseBlacklistSchedule()
	case BranchTypeRelease, BranchTypeFeature:
		if s.config.DisableSchedule {
			s.disableScheduleAll()
			return nil
		}
		return s.eraseNoOnWhitelistSchedule()
	case BranchTypeReleaseTag:
		if s.config.Whitelist != nil {
			util.LogWarningf("for release tag branch, whitelist is no need to set")
		}
		// release tag branch no need be crontab scheduling
		for _, plan := range s.plans {
			plan.Schedule = ""
		}
		for _, plans := range s.templatePlans {
			for _, plan := range plans {
				plan.Schedule = ""
			}
		}
	case BranchTypeUnknown:
		return fmt.Errorf("unknown type branch: %s", branch)
	}
	return nil
}

func (s *Syncer) disableScheduleAll() {
	for path, plan := range s.plans {
		util.LogInfof("plan %s's schedule is disabled since the switch `disable_schedule` is on", path)
		plan.Schedule = ""
	}
	for path, plans := range s.templatePlans {
		util.LogInfof("plan %s's schedule is disabled since the switch `disable_schedule` is on", path)
		for _, plan := range plans {
			plan.Schedule = ""
		}
	}
}

func (s *Syncer) eraseNoOnWhitelistSchedule() error {
	for path, plan := range s.plans {
		if !s.onWhitelist(path, plan) {
			util.LogInfof("disable the cron schedule of plan %s(%s) because it doesn't appear on whitelist", path, plan.Name)
			plan.Schedule = ""
		}
	}

	for path, plans := range s.templatePlans {
		for _, plan := range plans {
			if !s.onWhitelist(path, plan) {
				util.LogInfof("disable the cron schedule of plan %s(%s) because it doesn't appear on whitelist", path, plan.Name)
				plan.Schedule = ""
			}
		}
	}
	return nil
}

func (s *Syncer) eraseBlacklistSchedule() error {
	for path, plan := range s.plans {
		if s.onBlacklist(path, plan) {
			util.LogInfof("disable the cron schedule of plan %s(%s) because it appears on blacklist", path, plan.Name)
			plan.Schedule = ""
		}
	}

	for path, plans := range s.templatePlans {
		for _, plan := range plans {
			if s.onBlacklist(path, plan) {
				util.LogInfof("disable the cron schedule of plan %s(%s) because it appears on blacklist", path, plan.Name)
				plan.Schedule = ""
			}
		}
	}
	return nil
}

func (s *Syncer) patchPlanPriorities() error {
	planPriorityMap := make(map[string]int)
	for priority, paths := range s.config.Priorities {
		for _, path := range paths {
			plan, exist := s.plans[path]
			// ignore not exist here because the `s.Config.RecycleBin` discards some plans
			if !exist {
				continue
			}
			if _, exist := planPriorityMap[path]; exist {
				return fmt.Errorf("plan %s already set to priority %d", path, planPriorityMap[path])
			}
			planPriorityMap[path] = priority
			plan.Priority = common.ExecPriority(priority)
		}
	}
	return nil
}

func (s *Syncer) patchPlanReleaseTag() error {
	alreadyReleaseTagCheck := func(path string, plan *reqM.PlanCreateRequest) error {
		for _, tag := range plan.Tags {
			if tag == "release" {
				return fmt.Errorf("the `release` tag should only be added on the `release` field on meta.yaml now, please remove it from tags: %s", path)
			}
		}
		return nil
	}
	var errs = checkPlanPathOrNameListExistence(s.config.ReleaseList)
	for path, plan := range s.plans {
		if err := alreadyReleaseTagCheck(path, plan); err != nil {
			errs = multierror.Append(errs, err)
			continue
		}
		if s.onReleaseList(path, plan) && !s.onBlacklist(path, plan) {
			util.LogDebugf("plan %s is on the release list", path)
			plan.Tags = append(plan.Tags, "release")
		}
	}
loopTemplatePlans:
	for path, plans := range s.templatePlans {
		for _, plan := range plans {
			if err := alreadyReleaseTagCheck(path, plan); err != nil {
				errs = multierror.Append(errs, err)
				continue loopTemplatePlans
			}
			if s.onReleaseList(path, plan) && !s.onBlacklist(path, plan) {
				util.LogDebugf("plan %s is on the release list", path)
				plan.Tags = append(plan.Tags, "release")
			}
		}
	}
	return errs
}

func (s *Syncer) patchResourcePool() error {
	if s.config.ResourcePool != nil {
		for _, plan := range s.plans {
			resourcePool, err := GetResourcePool(s.config.ResourcePool, plan.Path, plan.TestPlanBase.TestBed.ResourcePool)
			if err != nil {
				return err
			}
			plan.TestPlanBase.TestBed.ResourcePool = resourcePool
		}
		for path, plans := range s.templatePlans {
			for _, plan := range plans {
				resourcePool, err := GetResourcePool(s.config.ResourcePool, path, plan.TestPlanBase.TestBed.ResourcePool)
				if err != nil {
					return err
				}
				plan.TestPlanBase.TestBed.ResourcePool = resourcePool
			}
		}
		return nil
	}
	// compatible with legacy meta.yaml
	if !s.ARMMode {
		return nil
	}
	for _, plan := range s.plans {
		// FIXME(@mahjonp) uses the api to decide whether the resource pool is arm64 kind
		if !strings.HasSuffix(plan.TestPlanBase.TestBed.ResourcePool, "arm64") {
			util.LogInfof("patch plan %s resource pool as arm64", plan.Name)
			plan.TestPlanBase.TestBed.ResourcePool = "arm64"
		}
	}
	for _, plans := range s.templatePlans {
		for _, plan := range plans {
			if !strings.HasSuffix(plan.TestPlanBase.TestBed.ResourcePool, "arm64") {
				util.LogInfof("patch plan %s resource pool as arm64", plan.Name)
				plan.TestPlanBase.TestBed.ResourcePool = "arm64"
			}
		}
	}
	return nil
}

func (s *Syncer) onWhitelist(path string, plan *reqM.PlanCreateRequest) bool {
	return onPlanPathOrNameList(path, plan.Name, s.config.Whitelist)
}

func (s *Syncer) onBlacklist(path string, plan *reqM.PlanCreateRequest) bool {
	return onPlanPathOrNameList(path, plan.Name, s.config.Blacklist)
}

func (s *Syncer) onReleaseList(path string, plan *reqM.PlanCreateRequest) bool {
	return onPlanPathOrNameList(path, plan.Name, s.config.ReleaseList)
}

func (s *Syncer) onRecycleBin(path string, plan *reqM.PlanCreateRequest) bool {
	return onPlanPathOrNameList(path, plan.Name, s.config.RecycleBin)
}

func (s *Syncer) attachGithubInfo() error {
	branch, err := s.Git.GetBranch()
	if err != nil {
		return err
	}
	commit, err := s.Git.GetCommit()
	if err != nil {
		return err
	}
	commitTs, err := s.Git.GetCommitTs()
	if err != nil {
		return err
	}
	for path, plan := range s.plans {
		if plan.GitHubPlanInfo == nil {
			plan.GitHubPlanInfo = &common.GitHubPlanInfo{}
		}
		plan.GitHubPlanInfo.Path = path
		plan.GitHubPlanInfo.Branch = branch
		plan.GitHubPlanInfo.CommitHash = commit
		plan.GitHubPlanInfo.CommitTs = commitTs
		plan.GitHubPlanInfo.Repo = s.Git.GetRepoURL()
	}
	for path, plans := range s.templatePlans {
		for _, plan := range plans {
			if plan.GitHubPlanInfo == nil {
				plan.GitHubPlanInfo = &common.GitHubPlanInfo{}
			}
			plan.GitHubPlanInfo.Path = path
			plan.GitHubPlanInfo.Branch = branch
			plan.GitHubPlanInfo.CommitHash = commit
			plan.GitHubPlanInfo.CommitTs = commitTs
			plan.GitHubPlanInfo.Repo = s.Git.GetRepoURL()
		}
	}
	return nil
}

func (s *Syncer) renamePlans() error {
	fileName := strings.TrimSuffix(filepath.Base(filepath.Base(s.MetafilePath)), filepath.Ext(filepath.Base(s.MetafilePath)))
	planArchSuffix := strings.TrimPrefix(fileName, "meta")

	for _, plan := range s.plans {
		plan.Name = fmt.Sprintf("%s@%s", plan.Name+planArchSuffix, plan.Branch)
	}
	for _, plans := range s.templatePlans {
		for _, plan := range plans {
			plan.Name = fmt.Sprintf("%s@%s", plan.Name+planArchSuffix, plan.Branch)
		}
	}
	return nil
}

func (s *Syncer) validate() error {
	var errs error
	planNameMaps := make(map[string]string)
	for _, plan := range s.plans {
		if path, e := planNameMaps[plan.Name]; e {
			errs = multierror.Append(errs, fmt.Errorf("the plan name %s in %s is duplicated with %s", plan.Name, plan.Path, path))
			continue
		}
		planNameMaps[plan.Name] = plan.Path
		if err := plan.Validate(); err != nil {
			util.LogErrorf("%s check error: %v", plan.Path, err)
			errs = multierror.Append(errs, errors.Wrapf(err, "plan %s check error", plan.Path))
			delete(s.plans, plan.Path)
		}
	}
loopTemplatePlans:
	for _, plans := range s.templatePlans {
		for _, plan := range plans {
			if path, e := planNameMaps[plan.Name]; e {
				errs = multierror.Append(errs, fmt.Errorf("the plan name %s in %s is duplicated with %s", plan.Name, plan.Path, path))
				continue loopTemplatePlans
			}
			planNameMaps[plan.Name] = plan.Path
			if err := plan.Validate(); err != nil {
				util.LogErrorf("%s check error: %v", plan.Path, err)
				errs = multierror.Append(errs, errors.Wrapf(err, "plan %s check error", plan.Path))
				delete(s.templatePlans, plan.Path)
				continue loopTemplatePlans
			}
		}
	}

	return errs
}

func (s *Syncer) submitPlans(dryrun bool) error {
	apiPath := "api/v1/gh-plans"
	var errs error
	var planCol []*reqM.PlanCreateRequest
	for path := range s.plans {
		planCol = append(planCol, s.plans[path])
	}
	for _, plans := range s.templatePlans {
		planCol = append(planCol, plans...)
	}

	for _, plan := range planCol {
		resp, err := util.Req().Host(s.ServerConfig.TcmsHost).Bearer(s.ServerConfig.Token).Path(apiPath).DryRun(dryrun).Json(plan).Post()
		if err != nil {
			errs = multierror.Append(errs, errors.Wrapf(err, "submit plan %s failed", plan.Path))
			continue
		}
		if resp.StatusCode != http.StatusOK {
			b, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				errs = multierror.Append(errs, errors.Wrap(err, "read http response error"))
				continue
			}
			errs = multierror.Append(errs, fmt.Errorf("submit plan %s failed, response=%s", plan.Path, string(b)))
		} else {
			util.LogInfof("submit plan %s success", plan.Path)
		}
	}
	return errs
}

func (s *Syncer) validateGithubPlan(plan reqM.PlanCreateRequest) error {
	if plan.Type != common.TestPlanKindGithub {
		return fmt.Errorf("type must be `%s`", common.TestPlanKindGithub)
	}
	return nil
}

func (s *Syncer) removeOldPlans() error {
	branch, err := s.Git.GetBranch()
	if err != nil {
		return err
	}
	resp, err := util.Req().Host(s.ServerConfig.TcmsHost).
		Bearer(s.ServerConfig.Token).
		Path("api/v1/gh-plans/old").
		Param(map[string]string{"branch": branch}).
		Delete()
	if err != nil {
		util.LogErrorf("remove old plans error: %s", err)
		return err
	}
	if resp.StatusCode != http.StatusOK {
		b, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return errors.Wrap(err, "read http response error")
		}
		util.LogErrorf("remove old plans failed, response=%s", string(b))
		return fmt.Errorf("remove old plans failed")
	} else {
		util.LogInfof("remove old plans succeed")
	}
	return nil
}

func onPlanPathOrNameList(path, planName string, list []PlanPathOrName) bool {
	for _, item := range list {
		switch item.Type() {
		case PlanPathOrNameNameType:
			if string(item) == planName {
				return true
			}
		case PlanPathOrNameDirType:
			if string(item) == "./" {
				return true
			}
			if strings.HasSuffix(string(item), "/") && strings.HasPrefix(path, string(item)) {
				return true
			}
			if !strings.HasSuffix(string(item), "/") && strings.HasPrefix(path, string(item)+"/") {
				return true
			}
		case PlanPathOrNameFileType:
			if string(item) == path {
				return true
			}
		}
	}
	return false
}

func checkPlanPathOrNameListExistence(list []PlanPathOrName) error {
	var errs error
	for _, item := range list {
		switch item.Type() {
		case PlanPathOrNameDirType:
			stat, err := os.Stat(string(item))
			if err != nil {
				errs = multierror.Append(errs, err)
				continue
			}
			if !stat.IsDir() {
				errs = multierror.Append(errs, fmt.Errorf("%s isn't a dir", item))
				continue
			}
		case PlanPathOrNameFileType:
			stat, err := os.Stat(string(item))
			if err != nil {
				errs = multierror.Append(errs, err)
				continue
			}
			if stat.IsDir() {
				errs = multierror.Append(errs, fmt.Errorf("%s isn't a file", item))
				continue
			}
		}
	}
	return errs
}

func SubstituteVars(template string, parameters map[string]string) (io.Reader, error) {
	variables := make(map[string]string)
	if len(parameters) != 0 {
		for name, value := range parameters {
			variables[fmt.Sprintf("meta.parameters.%s", name)] = value
		}
	}

	buf := &bytes.Buffer{}
	tmpl := fasttemplate.New(template, "{{", "}}")
	_, err := tmpl.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
		// only substitute meta.parameters prefix variables
		if strings.HasPrefix(tag, "meta.parameters.") {
			val, e := variables[tag]
			if !e {
				return 0, fmt.Errorf("unknown variable %q", tag)
			}
			return w.Write([]byte(val))
		} else {
			return w.Write([]byte(fmt.Sprintf("{{%s}}", tag)))
		}
	})
	if err != nil {
		return nil, errors.Wrapf(err, fmt.Sprintf("substitute variables error in %q", template))
	}
	return buf, nil
}

func ReadConfigFile(path string) (*Config, error) {
	r, err := util.ReadFromFile(path)
	if err != nil {
		return nil, err
	}
	b, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, err
	}
	var config Config
	if err = yaml.Unmarshal(b, &config); err != nil {
		return nil, err
	}
	if err = config.Init(); err != nil {
		return nil, err
	}
	return &config, nil
}
