package env

import (
	"context"
	"errors"
	"fmt"

	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/shared-provider/pkg/scheme"
	"github.com/tidbcloud/shared-provider/tests/framework/env/config"
	"github.com/tidbcloud/shared-provider/tests/framework/env/crd"
	"github.com/tidbcloud/shared-provider/tests/framework/env/yaml"
)

const (
	ClusterInfraAPI = "infraAPI"
	ClusterDefault  = "default"
)

type ResourceType string

const (
	ResourceTypeCRD     ResourceType = "CRD"
	ResourceTypeRawYAML ResourceType = "Yaml"
	// ResourceTypeHelm      ResourceType = "Helm"
	// ResourceTypeKustomize ResourceType = "Kustomize".
)

var (
	errUnknownCluster      = errors.New("unknown cluster")
	errUnknownResourceType = errors.New("unknown resource type")
)

// Environment defines a test env.
type Environment interface {
	Start(ctx context.Context) error
	Stop(ctx context.Context) error
	// Config returns cfg of specified cluster
	Config(cluster string) (*rest.Config, bool)

	Register(cluster, ns string, typ ResourceType, paths ...string) error
}

type environment struct {
	clusters map[string]*cluster
}

type cluster struct {
	config *rest.Config

	resources []resource

	crdManager  crd.Interface
	yamlManager yaml.Interface
}

type resource struct {
	ns    string
	typ   ResourceType
	paths []string
}

func New(c *config.Config) (Environment, error) {
	usrCfg, err := c.LoadUserConfig()
	if err != nil {
		return nil, err
	}
	defaultCluster, err := newCluster(usrCfg)
	if err != nil {
		return nil, err
	}

	infraCfg, err := c.LoadInfraAPIConfig()
	if err != nil {
		return nil, err
	}
	infraCluster, err := newCluster(infraCfg)
	if err != nil {
		return nil, err
	}

	return &environment{
		clusters: map[string]*cluster{
			ClusterDefault:  defaultCluster,
			ClusterInfraAPI: infraCluster,
		},
	}, nil
}

func newCluster(cfg *rest.Config) (*cluster, error) {
	c, err := client.New(cfg, client.Options{
		Scheme: scheme.Scheme,
	})
	if err != nil {
		return nil, fmt.Errorf("new cluster for environment setup error: %w", err)
	}

	return &cluster{
		config:      cfg,
		crdManager:  crd.New(c),
		yamlManager: yaml.New(c),
	}, nil
}

func (e *environment) Config(clusterName string) (*rest.Config, bool) {
	c, ok := e.clusters[clusterName]
	if !ok {
		return nil, false
	}

	return c.config, true
}

func (e *environment) Start(ctx context.Context) error {
	for _, c := range e.clusters {
		for _, r := range c.resources {
			switch r.typ {
			case ResourceTypeCRD:
				if err := c.crdManager.Install(ctx, r.paths...); err != nil {
					return fmt.Errorf("install crds err: %w", err)
				}
			case ResourceTypeRawYAML:
				if err := c.yamlManager.CreateOrUpdate(ctx, r.ns, r.paths...); err != nil {
					return fmt.Errorf("install raw yaml err: %w", err)
				}
			default:
				return fmt.Errorf("%s: %w", r.typ, errUnknownResourceType)
			}
		}
	}

	return nil
}

func (e *environment) Stop(ctx context.Context) error {
	for _, c := range e.clusters {
		if err := c.crdManager.Cleanup(ctx); err != nil {
			return fmt.Errorf("cleanup crds err: %w", err)
		}
		if err := c.yamlManager.Cleanup(ctx); err != nil {
			return fmt.Errorf("cleanup raw yaml err: %w", err)
		}
	}

	return nil
}

func (e *environment) Register(clusterName, ns string, typ ResourceType, paths ...string) error {
	c, ok := e.clusters[clusterName]
	if !ok {
		return fmt.Errorf("can't register resource to %s/%s: %w", clusterName, ns, errUnknownCluster)
	}

	c.resources = append(c.resources, resource{
		ns:    ns,
		typ:   typ,
		paths: paths,
	})

	return nil
}
