package main

import (
	"fmt"
	"regexp"
	"strings"

	v1 "github.com/tidbcloud/infra-api/api/v1"

	"github.com/Masterminds/semver"
	"github.com/hashicorp/go-multierror"
	"github.com/pingcap/errors"
)

var (
	tenantDirRegex  = regexp.MustCompile(`tenant-(\d+)`)
	clusterDirRegex = regexp.MustCompile(`cluster-(\d+)`)
)

// validateTree validates the layered file tree
func validateTree(t *Tree) error {

	var errs *multierror.Error
	validVersions := map[string]bool{}

	if t.VersionedConfigs != nil {
		for _, vConfig := range t.VersionedConfigs {
			err := validateVersionedConfig(vConfig)
			if err != nil {
				errs = multierror.Append(errs, err)
			} else {
				validVersions[vConfig.Meta.Version] = true
			}
		}
	} else {
		errs = multierror.Append(errs, errors.New("at least one versioned config is required"))
	}

	if t.GlobalMeta != nil {
		if _, ok := validVersions[t.GlobalMeta.Version]; !ok {
			errs = multierror.Append(errs, errors.Errorf("global default version is not defined, version %s", t.GlobalMeta.Version))
		}
		errs = multierror.Append(errs, validateGlobalImages(t.GlobalMeta.Images))
		if len(t.GlobalMeta.UpdatePolicy) < 1 {
			errs = multierror.Append(errs, errors.Errorf("update policy must be specified"))
		}
	} else {
		errs = multierror.Append(errs, errors.New("global default versions.yaml is required"))
	}

	for _, tOverlay := range t.TenantOverlays {
		errs = multierror.Append(errs, validateTenantOverlay(tOverlay, validVersions))
	}

	return errs.ErrorOrNil()
}

func validateVersionedConfig(vConfig VersionedConfig) error {
	var errs *multierror.Error

	if vConfig.Meta == nil {
		errs = multierror.Append(errs, errors.New("versioned config must have a version"))
	}

	errs = multierror.Append(errs, validateVersion(vConfig.Meta.Version))

	if len(vConfig.Configs) < len(componentToFile) {
		errs = multierror.Append(errs, errors.Errorf("versioned base config must include all components"))
	}

	err := validateConfigs(vConfig.Configs)
	if err != nil {
		errs = multierror.Append(errs, err)
	}
	// TODO: validate instance overlays?
	return errs.ErrorOrNil()
}

func validateTenantOverlay(tenantOverlay TenantOverlay, validVersions map[string]bool) error {
	var errs *multierror.Error

	errs = multierror.Append(errs, validateTenantID(tenantOverlay.TenantID))

	err := validateConfigs(tenantOverlay.Configs)
	if err != nil {
		errs = multierror.Append(errs, err)
	}

	if tenantOverlay.Meta != nil {
		meta := tenantOverlay.Meta
		if meta.Version != "" {
			tVersion := tenantOverlay.Meta.Version
			if _, ok := validVersions[tVersion]; !ok {
				errs = multierror.Append(errs, errors.Errorf("tenant's version is not defined, tenant: %s, version: %s", tenantOverlay.TenantID, tVersion))
			}
		}
		if meta.UpdatePolicy == v1.UpdatePolicyImmediate && meta.Version == "" {
			errs = multierror.Append(errs, errors.Errorf("tenant's update policy is immediate, but version is not defined, tenant: %s", tenantOverlay.TenantID))
		}
	}

	for _, cOverlay := range tenantOverlay.ClusterOverlays {
		errs = multierror.Append(errs, validateClusterOverlay(cOverlay, validVersions))
	}

	return errs
}

func validateClusterOverlay(cOverlay ClusterOverlay, validVersions map[string]bool) error {
	var errs *multierror.Error

	errs = multierror.Append(errs, validateClusterID(cOverlay.ClusterID))

	err := validateConfigs(cOverlay.Configs)
	if err != nil {
		errs = multierror.Append(errs, err)
	}

	if cOverlay.Meta != nil {
		meta := cOverlay.Meta
		if meta.Version != "" {
			cVersion := cOverlay.Meta.Version
			if _, ok := validVersions[cVersion]; !ok {
				errs = multierror.Append(errs, errors.Errorf("version %s of cluster %s is not defined", cVersion, cOverlay.ClusterID))
			}
		}
		if meta.UpdatePolicy == v1.UpdatePolicyImmediate && meta.Version == "" {
			errs = multierror.Append(errs, errors.Errorf("cluster %s's update policy is immediate, but version is not defined", cOverlay.ClusterID))
		}
	}

	return errs
}

func validateVersion(ver string) error {
	// semver works for both with/without 'v' prefix, but we want to enforce the prefix in repo
	if !strings.HasPrefix(ver, "v") {
		return errors.Errorf("version directory %s must be started with 'v'", ver)
	}

	var (
		constrain *semver.Constraints
		err       error
	)
	parsed, err := semver.NewVersion(ver)
	if err != nil {
		return err
	}
	// we use vx.y.x-rc.x for prerelease format
	// prerelease tag w/o rc.x is considered invalid
	if strings.Contains(parsed.Prerelease(), "rc") {
		constrain, err = semver.NewConstraint(">= v7.1.0-rc.0")
		if err != nil {
			return err
		}
	} else {
		constrain, err = semver.NewConstraint(">= v4.0.9")
		if err != nil {
			return err
		}
	}
	_, errs := constrain.Validate(parsed)
	return multierror.Append(nil, errs...).ErrorOrNil()
}

func validateGlobalImages(images *Images) error {
	if images == nil {
		return errors.New("global default image setting is required")
	}
	if images.TiDB == nil || images.TiDB.BaseImage == nil {
		return errors.New("global default image of TiDB is required")
	}
	if images.TiKV == nil || images.TiKV.BaseImage == nil {
		return errors.New("global default image of TiKV is required")
	}
	if images.PD == nil || images.PD.BaseImage == nil {
		return errors.New("global default image of PD is required")
	}
	if images.TiFlash == nil || images.TiFlash.BaseImage == nil {
		return errors.New("global default image of TiFlash is required")
	}
	if images.Backup == nil || images.Backup.BaseImage == nil {
		return errors.New("global default image of Backup is required")
	}
	if images.Restore == nil || images.Restore.BaseImage == nil {
		return errors.New("global default image of Restore is required")
	}
	if images.Lightning == nil || images.Lightning.BaseImage == nil {
		return errors.New("global default image of Lightning is required")
	}
	if images.TiCDC == nil || images.TiCDC.BaseImage == nil {
		return errors.New("global default image of TiCDC is required")
	}
	return nil
}

func validateTenantID(id string) error {
	if id == "" {
		return errors.New("tenant ID is empty")
	}
	return nil
}

func validateClusterID(id string) error {
	if id == "" {
		return errors.New("cluster ID is empty")
	}
	return nil
}

func parseTenantDirName(dir string) (string, error) {
	if !tenantDirRegex.MatchString(dir) {
		return "", errors.Errorf("illegal tenant dir name %s", dir)
	}
	return tenantDirRegex.FindStringSubmatch(dir)[1], nil
}

func parseClusterDirName(dir string) (string, error) {
	if !clusterDirRegex.MatchString(dir) {
		return "", errors.Errorf("illegal cluster dir name %s", dir)
	}
	return clusterDirRegex.FindStringSubmatch(dir)[1], nil
}

func validateConfigs(configs ComponentConfigs) error {
	for _, cfg := range configs {
		err := validateConfig(cfg.Config.MP)
		if err != nil {
			return err
		}
	}
	return nil
}

func validateConfig(base map[string]interface{}, keyPath ...string) error {
	child, err := GetChild(base, keyPath...)
	if err != nil {
		return errors.Wrap(err, "get child")
	}

	for k, v := range child {
		switch v.(type) {
		case map[string]interface{}:
			if err := validateConfig(child, k); err != nil {
				return errors.Wrap(err, fmt.Sprintf("error processing key %s", k))
			}
		default:
			if value, ok := v.(string); ok {
				err = CheckLegalExpression(value)
				if err != nil {
					return err
				}
			}
		}
	}
	return nil
}
