package cli

import (
	"context"

	models "github.com/portainer/portainer/api/http/models/kubernetes"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	labels "k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/util/intstr"
)

// GetServices gets all the services for a given namespace in a k8s endpoint.
func (kcl *KubeClient) GetServices(namespace string, lookupApplications bool) ([]models.K8sServiceInfo, error) {
	client := kcl.cli.CoreV1().Services(namespace)

	services, err := client.List(context.Background(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	var result []models.K8sServiceInfo

	for _, service := range services.Items {
		servicePorts := make([]models.K8sServicePort, 0)
		for _, port := range service.Spec.Ports {
			servicePorts = append(servicePorts, models.K8sServicePort{
				Name:       port.Name,
				NodePort:   int(port.NodePort),
				Port:       int(port.Port),
				Protocol:   string(port.Protocol),
				TargetPort: port.TargetPort.String(),
			})
		}

		ingressStatus := make([]models.K8sServiceIngress, 0)
		for _, status := range service.Status.LoadBalancer.Ingress {
			ingressStatus = append(ingressStatus, models.K8sServiceIngress{
				IP:   status.IP,
				Host: status.Hostname,
			})
		}

		var applications []models.K8sApplication
		if lookupApplications {
			applications, _ = kcl.getOwningApplication(namespace, service.Spec.Selector)
		}

		result = append(result, models.K8sServiceInfo{
			Name:                          service.Name,
			UID:                           string(service.GetUID()),
			Type:                          string(service.Spec.Type),
			Namespace:                     service.Namespace,
			CreationTimestamp:             service.GetCreationTimestamp().String(),
			AllocateLoadBalancerNodePorts: service.Spec.AllocateLoadBalancerNodePorts,
			Ports:                         servicePorts,
			IngressStatus:                 ingressStatus,
			Labels:                        service.GetLabels(),
			Annotations:                   service.GetAnnotations(),
			ClusterIPs:                    service.Spec.ClusterIPs,
			ExternalName:                  service.Spec.ExternalName,
			ExternalIPs:                   service.Spec.ExternalIPs,
			Applications:                  applications,
		})
	}

	return result, nil
}

// CreateService creates a new service in a given namespace in a k8s endpoint.
func (kcl *KubeClient) CreateService(namespace string, info models.K8sServiceInfo) error {
	ServiceClient := kcl.cli.CoreV1().Services(namespace)
	var service v1.Service

	service.Name = info.Name
	service.Spec.Type = v1.ServiceType(info.Type)
	service.Namespace = info.Namespace
	service.Annotations = info.Annotations
	service.Labels = info.Labels
	service.Spec.AllocateLoadBalancerNodePorts = info.AllocateLoadBalancerNodePorts
	service.Spec.Selector = info.Selector

	// Set ports.
	for _, p := range info.Ports {
		var port v1.ServicePort
		port.Name = p.Name
		port.NodePort = int32(p.NodePort)
		port.Port = int32(p.Port)
		port.Protocol = v1.Protocol(p.Protocol)
		port.TargetPort = intstr.FromString(p.TargetPort)
		service.Spec.Ports = append(service.Spec.Ports, port)
	}

	// Set ingresses.
	for _, i := range info.IngressStatus {
		var ing v1.LoadBalancerIngress
		ing.IP = i.IP
		ing.Hostname = i.Host
		service.Status.LoadBalancer.Ingress = append(
			service.Status.LoadBalancer.Ingress,
			ing,
		)
	}

	_, err := ServiceClient.Create(context.Background(), &service, metav1.CreateOptions{})
	return err
}

// DeleteServices processes a K8sServiceDeleteRequest by deleting each service
// in its given namespace.
func (kcl *KubeClient) DeleteServices(reqs models.K8sServiceDeleteRequests) error {
	var err error
	for namespace := range reqs {
		for _, service := range reqs[namespace] {
			serviceClient := kcl.cli.CoreV1().Services(namespace)
			err = serviceClient.Delete(
				context.Background(),
				service,
				metav1.DeleteOptions{},
			)
		}
	}
	return err
}

// UpdateService updates service in a given namespace in a k8s endpoint.
func (kcl *KubeClient) UpdateService(namespace string, info models.K8sServiceInfo) error {
	ServiceClient := kcl.cli.CoreV1().Services(namespace)
	var service v1.Service

	service.Name = info.Name
	service.Spec.Type = v1.ServiceType(info.Type)
	service.Namespace = info.Namespace
	service.Annotations = info.Annotations
	service.Labels = info.Labels
	service.Spec.AllocateLoadBalancerNodePorts = info.AllocateLoadBalancerNodePorts
	service.Spec.Selector = info.Selector

	// Set ports.
	for _, p := range info.Ports {
		var port v1.ServicePort
		port.Name = p.Name
		port.NodePort = int32(p.NodePort)
		port.Port = int32(p.Port)
		port.Protocol = v1.Protocol(p.Protocol)
		port.TargetPort = intstr.FromString(p.TargetPort)
		service.Spec.Ports = append(service.Spec.Ports, port)
	}

	// Set ingresses.
	for _, i := range info.IngressStatus {
		var ing v1.LoadBalancerIngress
		ing.IP = i.IP
		ing.Hostname = i.Host
		service.Status.LoadBalancer.Ingress = append(
			service.Status.LoadBalancer.Ingress,
			ing,
		)
	}

	_, err := ServiceClient.Update(context.Background(), &service, metav1.UpdateOptions{})
	return err
}

// getOwningApplication gets the application that owns the given service selector.
func (kcl *KubeClient) getOwningApplication(namespace string, selector map[string]string) ([]models.K8sApplication, error) {
	if len(selector) == 0 {
		return nil, nil
	}

	selectorLabels := labels.SelectorFromSet(selector).String()

	// look for replicasets first, limit 1 (we only support one owner)
	replicasets, err := kcl.cli.AppsV1().ReplicaSets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selectorLabels, Limit: 1})
	if err != nil {
		return nil, err
	}

	var meta metav1.Object
	if replicasets != nil && len(replicasets.Items) > 0 {
		meta = replicasets.Items[0].GetObjectMeta()
	} else {
		// otherwise look for matching pods, limit 1 (we only support one owner)
		pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selectorLabels, Limit: 1})
		if err != nil {
			return nil, err
		}

		if pods == nil || len(pods.Items) == 0 {
			return nil, nil
		}

		meta = pods.Items[0].GetObjectMeta()
	}

	return makeApplication(meta), nil
}

func makeApplication(meta metav1.Object) []models.K8sApplication {
	ownerReferences := meta.GetOwnerReferences()
	if len(ownerReferences) == 0 {
		return nil
	}

	// Currently, we only support one owner reference
	ownerReference := ownerReferences[0]
	return []models.K8sApplication{
		{
			// Only the name is used right now, but we can add more fields in the future
			Name: ownerReference.Name,
		},
	}

}