//go:build !darwin
// +build !darwin

package env

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"syscall"

	corev1 "k8s.io/api/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/tests/e2e/dag"
	"github.com/tidbcloud/eks-provider/tests/e2e/data"
	"github.com/tidbcloud/eks-provider/tests/e2e/label"
	"github.com/tidbcloud/eks-provider/tests/e2e/suite"
	"github.com/tidbcloud/eks-provider/tests/e2e/utils/helper"
	"github.com/tidbcloud/eks-provider/tests/e2e/utils/wait"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

func init() {
	suite.AddEnv(
		label.New(
			NewInfraProviderEnv(
				suite.GetOptions(),
				CloudAWS,
				InfraProviderVersionOld,
			),
			"setup infra provider with old version for aws",
			label.CloudAWS,
			label.FeatUpgrade,
		),
		label.New(
			NewInfraProviderEnv(
				suite.GetOptions(),
				CloudAWS,
				InfraProviderVersionNew,
			),
			"setup infra provider with new version for aws",
			label.CloudAWS,
		),
		label.New(
			NewInfraProviderEnv(
				suite.GetOptions(),
				CloudGCP,
				InfraProviderVersionOld,
			),
			"setup infra provider with old version for gcp",
			label.CloudGCP,
			label.FeatUpgrade,
		),
		label.New(
			NewInfraProviderEnv(
				suite.GetOptions(),
				CloudGCP,
				InfraProviderVersionNew,
			),
			"setup infra provider with new version for gcp",
			label.CloudGCP,
		),
	)
}

type InfraProviderEnv struct {
	Config     *Config
	Reconciler string

	Namespace *corev1.Namespace

	opt   *suite.Options
	cloud string

	v InfraProviderVersion

	logFile        *os.File
	containers     []string
	cancel         context.CancelFunc
	childrenCancel []context.CancelFunc
}

func NewInfraProviderEnv(opt *suite.Options, cloud string, v InfraProviderVersion) dag.Env {
	return &InfraProviderEnv{
		opt:   opt,
		cloud: cloud,
		v:     v,
	}
}

func (e *InfraProviderEnv) Name() string {
	return WithCloudAndVersion("InfraProvider", e.cloud, e.v)
}

func (e *InfraProviderEnv) Envs() []string {
	return []string{
		WithCloud("Config", e.cloud),
	}
}

func (e *InfraProviderEnv) Setup(ctx context.Context) error {
	// TODO: refactor it
	switch e.v {
	case InfraProviderVersionNew:
		e.Reconciler = "e2e-new"
	case InfraProviderVersionOld:
		e.Reconciler = "e2e-old"
	default:
		panic("unreachable")
	}

	switch e.v {
	case InfraProviderVersionOld:
		e.Namespace = data.NewOldNamespace()
	case InfraProviderVersionNew:
		e.Namespace = data.NewNewNamespace()
	default:
		panic("unreachable")
	}
	if err := e.Config.InfraAPIClient.Create(ctx, e.Namespace); err != nil {
		return err
	}

	// Provider should be stopped in cleanup function
	// So don't interrupt this task by env context
	if err := e.runProviderCmd(context.Background()); err != nil {
		return err
	}
	return nil
}

func (e *InfraProviderEnv) WithContext(ctx context.Context) context.Context {
	ctx, cancel := context.WithCancel(ctx)
	e.childrenCancel = append(e.childrenCancel, cancel)
	return ctx
}

func (e *InfraProviderEnv) Cleanup(ctx context.Context) error {
	if err := e.cleanupNamespace(ctx); err != nil {
		fmt.Printf("cannot cleanup namespace %s because of %v, please manually clean up it", e.Namespace.Name, err)
	}

	return e.cleanupCmd(ctx)
}

func (e *InfraProviderEnv) cleanupNamespace(ctx context.Context) error {
	if e.Namespace == nil {
		return nil
	}
	fmt.Println("try to clean up namespace")

	errList := []error{}
	c := &cloudv1.Cluster{}
	np := &cloudv1.NetworkPeer{}
	if err := e.Config.InfraAPIClient.DeleteAllOf(ctx, c, client.InNamespace(e.Namespace.Name)); err != nil {
		errList = append(errList, err)
	}
	if err := e.Config.InfraAPIClient.DeleteAllOf(ctx, np, client.InNamespace(e.Namespace.Name)); err != nil {
		errList = append(errList, err)
	}

	if len(errList) != 0 {
		return fmt.Errorf("cannot delete clusters and networkpeers: %v", errList)
	}

	cl := &cloudv1.ClusterList{}
	if err := wait.WaitForAllObjectInNamespaceDeleted(
		ctx,
		e.Config.InfraAPIClient,
		e.Namespace.Name,
		cl,
		wait.LongTaskTimeout,
	); err != nil {
		return fmt.Errorf("failed to wait for clusters deleted: %v", errList)
	}

	// Network should be deleted after all of other resources are deleted.
	// Or it will be in PendingDelete status for too long time
	n := &cloudv1.Network{}
	if err := e.Config.InfraAPIClient.DeleteAllOf(ctx, n, client.InNamespace(e.Namespace.Name)); err != nil {
		return fmt.Errorf("cannot delete network: %w", err)
	}

	if err := helper.DeleteAndWait(ctx, e.Config.InfraAPIClient, e.Namespace, wait.LongTaskTimeout); err != nil {
		return fmt.Errorf("cannot delete namespace: %w", err)
	}

	return nil
}

func (e *InfraProviderEnv) cleanupCmd(ctx context.Context) error {
	e.cancel()

	if e.logFile != nil {
		e.logFile.Close()
	}
	if len(e.containers) == 0 {
		return nil
	}
	args := append([]string{"stop"}, e.containers...)
	cmd := exec.CommandContext(ctx, "docker", args...)
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Pdeathsig: syscall.SIGKILL,
	}
	return cmd.Run()
}

func (e *InfraProviderEnv) interrupt() {
	for _, cancel := range e.childrenCancel {
		cancel()
	}
}

func (e *InfraProviderEnv) runProviderCmd(ctx context.Context) error {
	ctx, e.cancel = context.WithCancel(ctx)

	cfg, err := e.validateInfraProviderConfig()
	if err != nil {
		return err
	}
	f, err := os.OpenFile(cfg.logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_TRUNC, 0755)
	if err != nil {
		return err
	}
	e.logFile = f

	cmd := exec.CommandContext(ctx, cfg.name, cfg.args...)
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Pdeathsig: syscall.SIGKILL,
	}

	cmd.Stdout = f
	cmd.Stderr = f
	cmd.Env = cfg.env

	if err := cmd.Start(); err != nil {
		return err
	}

	if cfg.containerName != "" {
		e.containers = append(e.containers, cfg.containerName)
	}

	go func() {
		if err := cmd.Wait(); err != nil {
			e.interrupt()
			fmt.Printf("infra provider(%s %v) unexpectedly exit: %v, please check the log file at %s", cfg.name, cfg.args, err, cfg.logFile)
		}

	}()

	return nil
}

type infraProviderConfig struct {
	name string
	args []string
	env  []string

	logFile string

	containerName string
}

// this function change config file to meet args of test
// func (e *InfraProviderEnv) normalizeConfig(file string) (string, error) {
// 	bs, err := os.ReadFile(file)
// 	if err != nil {
// 		return "", fmt.Errorf("cannot open config file: %w", err)
// 	}
// 	cfg := pkgconfig.RootConfig{}
// 	if err := yaml.Unmarshal(bs, &cfg); err != nil {
// 		return "", fmt.Errorf("invalid config: %w", err)
// 	}
//
// 	delete(cfg.Predicate.MatchLabels, "cloud.pingcap.com/reconciler")
// 	exps := []metav1.LabelSelectorRequirement{}
// 	for _, exp := range cfg.Predicate.MatchExpressions {
// 		if exp.Key != "cloud.pingcap.com/reconciler" {
// 			exps = append(exps, exp)
// 		}
// 	}
// 	cfg.Predicate.MatchExpressions = exps
// 	cfg.Predicate.MatchLabels["cloud.pingcap.com/reconciler"] = e.Reconciler
//
// 	nbs, err := yaml.Marshal(&cfg)
// 	if err != nil {
// 		return "", fmt.Errorf("cannot marshal config file: %w", err)
// 	}
// 	cfgFile := filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s-infra-provider-root-config.yaml", e.cloud, e.v))
// 	if err := os.WriteFile(cfgFile, nbs, 0777); err != nil {
// 		return "", fmt.Errorf("cannot write config file: %w", err)
// 	}
//
// 	return cfgFile, nil
// }

func (e *InfraProviderEnv) validateInfraProviderConfig() (*infraProviderConfig, error) {
	cfg := infraProviderConfig{}
	var popt *suite.InfraProviderOptions
	var runtime *suite.Runtime
	var envs []string
	switch e.cloud {
	case CloudAWS:
		envs = []string{
			"AWS_PROFILE=" + e.opt.AWS.Profile,
			"AWS_REGION=" + e.opt.AWS.Region,
		}

		switch e.v {
		case InfraProviderVersionOld:
			popt = &e.opt.AWS.OldProvider
			runtime = &e.opt.AWS.OldProvider.Runtime
		case InfraProviderVersionNew:
			popt = &e.opt.AWS.NewProvider
			runtime = &e.opt.AWS.NewProvider.Runtime
		}

	case CloudGCP:
		switch e.v {
		case InfraProviderVersionOld:
			popt = &e.opt.GCP.OldProvider
			runtime = &e.opt.GCP.OldProvider.Runtime
		case InfraProviderVersionNew:
			popt = &e.opt.GCP.NewProvider
			runtime = &e.opt.GCP.NewProvider.Runtime
		}
	}

	cfg.logFile = popt.LogFile
	if cfg.logFile == "" {
		cfg.logFile = filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s-infra-provider.log", e.cloud, e.v))
	}

	switch runtime.Type {
	case suite.RuntimeTypeLocal:
		cfg.name = popt.Cmd
		cfg.args = []string{
			"--base-kubeconfig", e.opt.BaseConfig,
			"--root-config", popt.Config,
			"--infra-kubeconfig", e.opt.InfraAPIConfig,
		}
		cfg.env = envs
	case suite.RuntimeTypeDocker:
		u, err := user.Current()
		if err != nil {
			return nil, err
		}
		cfg.name = "docker"
		cfg.containerName = fmt.Sprintf("%s-%s-infra-provider", e.cloud, e.v)
		cfg.args = []string{
			"run", "--rm",
			"--name", cfg.containerName,
			"-u", u.Uid + ":" + u.Gid,
			"--entrypoint", popt.Cmd,
			"-v", popt.Config + ":/config.yaml",
			"-v", e.opt.InfraAPIConfig + ":/.kube/config",
		}
		for _, env := range envs {
			cfg.args = append(cfg.args, "-e", env)
		}
		cfg.args = append(cfg.args, runtime.ExtraArgs...)
		cfg.args = append(cfg.args,
			runtime.Image,
			"--base-kubeconfig=/.kube/config",
			"--root-config=/config.yaml",
			"--infra-kubeconfig=/.kube/config",
		)
	}

	return &cfg, nil
}
