package agent

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/tidbcloud/devops/api"
	jmpent "github.com/tidbcloud/devops/entity/jump"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"

	"github.com/fsnotify/fsnotify"
	"github.com/go-logr/logr"
	"k8s.io/client-go/tools/clientcmd"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)

type KubernetesCredStore struct {
	inputDir  string
	outputDir string
	logger    logr.Logger
	fsWatcher *fsnotify.Watcher
	mu        sync.Mutex
}

func NewKubernetesCredStore(inputDir, outputDir string, logger logr.Logger) *KubernetesCredStore {
	return &KubernetesCredStore{
		inputDir:  inputDir,
		outputDir: outputDir,
		logger:    logger.WithName("KubernetesCredStore"),
		mu:        sync.Mutex{},
	}
}

func (s *KubernetesCredStore) Init() error {
	var err error

	// create dir if not exist
	dirs := []string{
		s.inputDir,
		s.outputDir,
		s.InputCredPath(),
	}
	for _, dir := range dirs {
		err = os.Mkdir(dir, 0777)
		if err != nil && !os.IsExist(err) {
			return err
		}
	}

	// first time build
	err = s.Rebuild()
	if err != nil {
		return fmt.Errorf("first time build failed: %w", err)
	}

	// build fs watcher
	s.fsWatcher, err = fsnotify.NewWatcher()
	if err != nil {
		return fmt.Errorf("create fs watcher failed: %w", err)
	}
	go func() {
		for {
			select {
			case event := <-s.fsWatcher.Events:

				switch event.Op {
				case fsnotify.Chmod:
					continue
				}

				s.logger.Info("rebuild kube config cause receiving a fs event", "event_name", event.Name, "event_op", event.Op)

				err := s.Rebuild()
				if err != nil {
					s.logger.Error(err, "rebuild kube config failed")
				}

			case err := <-s.fsWatcher.Errors:
				s.logger.Error(err, "fs watcher error")
				return
			}
		}
	}()
	err = s.fsWatcher.Add(s.InputCredPath())
	if err != nil {
		return fmt.Errorf("add watch failed: %w", err)
	}

	return nil
}

// Rebuild walk all files in cred root path, and merge all configs into one file.
func (s *KubernetesCredStore) Rebuild() error {
	final, err := s.buildCredential(nil)
	if err != nil {
		return err
	}

	cfgPath := s.OutputConfigPath()

	s.mu.Lock()
	defer s.mu.Unlock()

	// reuse current context
	cur, err := clientcmd.LoadFromFile(cfgPath)
	if err != nil && !os.IsNotExist(err) {
		s.logger.Info("load config failed when rebuilding", "file", cfgPath, "error", err.Error())
	}
	if cur != nil && final.Contexts[cur.CurrentContext] != nil {
		final.CurrentContext = cur.CurrentContext
	}

	// write to file
	err = clientcmd.WriteToFile(*final, cfgPath)
	if err != nil {
		return fmt.Errorf("write config failed: %w", err)
	}

	return nil
}

func (s *KubernetesCredStore) GetCredential(credIDs []jmpent.CredentialID) (*clientcmdapi.Config, error) {
	return s.buildCredential(credIDs)
}

func (s *KubernetesCredStore) OutputConfigPath() string {
	return filepath.Join(s.outputDir, "config")
}

func (s *KubernetesCredStore) InputCredPath() string {
	return filepath.Join(s.inputDir, "creds")
}

func (s *KubernetesCredStore) buildCredential(credIDs []jmpent.CredentialID) (*clientcmdapi.Config, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	targets := container.NewSet[string]()
	foundCreds := container.NewSet[string]()
	result := clientcmdapi.NewConfig()

	for _, id := range credIDs {
		targets.Insert(string(id))
	}

	err := filepath.Walk(s.InputCredPath(), func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			s.logger.Error(err, "visit a file failed when building credential")
			return nil
		}

		if info.IsDir() {
			return nil
		}
		if strings.HasPrefix(info.Name(), ".") {
			return nil
		}

		config, err := clientcmd.LoadFromFile(path)
		if err != nil {
			s.logger.Error(err, "load a config failed when building credential", "file", path)
			return nil
		}

		for contextName, context := range config.Contexts {
			if targets.Len() > 0 && !targets.Has(contextName) {
				continue
			}

			cluster, ok := config.Clusters[context.Cluster]
			if !ok {
				return errs.APIErrorf(api.ErrCodeNotFound, "cluster %q of credential %q not found", context.Cluster, contextName)
			}
			authInfo, ok := config.AuthInfos[context.AuthInfo]
			if !ok {
				return errs.APIErrorf(api.ErrCodeNotFound, "auth info %q of credential %q not found", context.AuthInfo, contextName)
			}

			result.Contexts[contextName] = context
			result.Clusters[context.Cluster] = cluster
			result.AuthInfos[context.AuthInfo] = authInfo

			foundCreds.Insert(contextName)
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("merge config failed: %w", err)
	}

	if targets.Len() > 0 {
		if miss := targets.Difference(foundCreds); miss.Len() > 0 {
			return nil, errs.APIErrorf(api.ErrCodeNotFound, "credentials %v not found", miss.Slice())
		}

		result.CurrentContext = string(credIDs[0])
	}

	return result, nil
}
