package kubernetes

import (
	"os"
	"path/filepath"
	"sort"

	latest "github.com/docker/compose-on-kubernetes/api/compose/v1alpha3"
	"github.com/docker/compose-on-kubernetes/api/labels"
	apiv1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)

// Stack is the main type used by stack commands so they remain independent from kubernetes compose component version.
type Stack struct {
	Name        string
	Namespace   string
	ComposeFile string
	Spec        *latest.StackSpec
}

type childResource interface {
	setOwner(metav1.OwnerReference) error
	delete() // does not report error, as if a deletion failed, we want to continue deleting other child resources
}

func deleteChildResources(childResources []childResource) {
	for _, cr := range childResources {
		cr.delete()
	}
}

func setChildResourcesOwner(childResources []childResource, owner metav1.OwnerReference) error {
	for _, cr := range childResources {
		if err := cr.setOwner(owner); err != nil {
			return err
		}
	}
	return nil
}

// getServices returns all the stack service names, sorted lexicographically
func (s *Stack) getServices() []string {
	services := make([]string, len(s.Spec.Services))
	for i, service := range s.Spec.Services {
		services[i] = service.Name
	}
	sort.Strings(services)
	return services
}

// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
func (s *Stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) ([]childResource, error) {
	var resources []childResource
	for name, config := range s.Spec.Configs {
		if config.File == "" {
			continue
		}

		fileName := filepath.Base(config.File)
		content, err := os.ReadFile(config.File)
		if err != nil {
			return resources, err
		}

		configMap, err := configMaps.Create(toConfigMap(s.Name, name, fileName, content))
		if err != nil {
			return resources, err
		}
		resources = append(resources, &configMapChildResource{client: configMaps, configMap: configMap})
	}
	return resources, nil
}

type configMapChildResource struct {
	client    corev1.ConfigMapInterface
	configMap *apiv1.ConfigMap
}

func (r *configMapChildResource) setOwner(ref metav1.OwnerReference) error {
	r.configMap.OwnerReferences = append(r.configMap.OwnerReferences, ref)
	_, err := r.client.Update(r.configMap)
	return err
}

func (r *configMapChildResource) delete() {
	r.client.Delete(r.configMap.Name, nil)
}

// toConfigMap converts a Compose Config to a Kube ConfigMap.
func toConfigMap(stackName, name, key string, content []byte) *apiv1.ConfigMap {
	return &apiv1.ConfigMap{
		TypeMeta: metav1.TypeMeta{
			Kind:       "ConfigMap",
			APIVersion: "v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
			Labels: map[string]string{
				labels.ForStackName: stackName,
			},
		},
		Data: map[string]string{
			key: string(content),
		},
	}
}

// createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
func (s *Stack) createFileBasedSecrets(secrets corev1.SecretInterface) ([]childResource, error) {
	var resources []childResource
	for name, secret := range s.Spec.Secrets {
		if secret.File == "" {
			continue
		}

		fileName := filepath.Base(secret.File)
		content, err := os.ReadFile(secret.File)
		if err != nil {
			return resources, err
		}

		secret, err := secrets.Create(toSecret(s.Name, name, fileName, content))
		if err != nil {
			return resources, err
		}
		resources = append(resources, &secretChildResource{client: secrets, secret: secret})
	}
	return resources, nil
}

type secretChildResource struct {
	client corev1.SecretInterface
	secret *apiv1.Secret
}

func (r *secretChildResource) setOwner(ref metav1.OwnerReference) error {
	r.secret.OwnerReferences = append(r.secret.OwnerReferences, ref)
	_, err := r.client.Update(r.secret)
	return err
}

func (r *secretChildResource) delete() {
	r.client.Delete(r.secret.Name, nil)
}

// toSecret converts a Compose Secret to a Kube Secret.
func toSecret(stackName, name, key string, content []byte) *apiv1.Secret {
	return &apiv1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
			Labels: map[string]string{
				labels.ForStackName: stackName,
			},
		},
		Data: map[string][]byte{
			key: content,
		},
	}
}
