package cli

import (
	"context"
	"strings"

	models "github.com/portainer/portainer/api/http/models/kubernetes"
	"github.com/portainer/portainer/api/stacks/stackutils"
	"github.com/rs/zerolog/log"
	netv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) {
	var controllers []models.K8sIngressController

	// We know that each existing class points to a controller so we can start
	// by collecting these easy ones.
	classClient := kcl.cli.NetworkingV1().IngressClasses()
	classList, err := classClient.List(context.Background(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	// We want to know which of these controllers is in use.
	var ingresses []models.K8sIngressInfo
	namespaces, err := kcl.GetNamespaces()
	if err != nil {
		return nil, err
	}
	for namespace := range namespaces {
		t, err := kcl.GetIngresses(namespace)
		if err != nil {
			// User might not be able to list ingresses in system/not allowed
			// namespaces.
			log.Debug().Err(err).Msg("failed to list ingresses for the current user, skipped sending ingress")
			continue
		}
		ingresses = append(ingresses, t...)
	}
	usedClasses := make(map[string]struct{})
	for _, ingress := range ingresses {
		usedClasses[ingress.ClassName] = struct{}{}
	}

	for _, class := range classList.Items {
		var controller models.K8sIngressController
		controller.Name = class.Spec.Controller
		controller.ClassName = class.Name

		// If the class is used mark it as such.
		if _, ok := usedClasses[class.Name]; ok {
			controller.Used = true
		}

		switch {
		case strings.Contains(controller.Name, "nginx"):
			controller.Type = "nginx"
		case strings.Contains(controller.Name, "traefik"):
			controller.Type = "traefik"
		default:
			controller.Type = "other"
		}
		controllers = append(controllers, controller)
	}
	return controllers, nil
}

// GetIngresses gets all the ingresses for a given namespace in a k8s endpoint.
func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, error) {
	// Fetch ingress classes to build a map. We will later use the map to lookup
	// each ingresses "type".
	classes := make(map[string]string)
	classClient := kcl.cli.NetworkingV1().IngressClasses()
	classList, err := classClient.List(context.Background(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	for _, class := range classList.Items {
		// Write the ingress classes "type" to our map.
		classes[class.Name] = class.Spec.Controller
	}

	// Fetch each ingress.
	ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
	ingressList, err := ingressClient.List(context.Background(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	var infos []models.K8sIngressInfo
	for _, ingress := range ingressList.Items {
		var info models.K8sIngressInfo
		info.Name = ingress.Name
		info.UID = string(ingress.UID)
		info.Namespace = namespace
		ingressClass := ingress.Spec.IngressClassName
		info.ClassName = ""
		if ingressClass != nil {
			info.ClassName = *ingressClass
		}
		info.Type = classes[info.ClassName]
		info.Annotations = ingress.Annotations

		// Gather TLS information.
		for _, v := range ingress.Spec.TLS {
			var tls models.K8sIngressTLS
			tls.Hosts = v.Hosts
			tls.SecretName = v.SecretName
			info.TLS = append(info.TLS, tls)
		}

		// Gather list of paths and hosts.
		hosts := make(map[string]struct{})
		for _, r := range ingress.Spec.Rules {
			// We collect all exiting hosts in a map to avoid duplicates.
			// Then, later convert it to a slice for the frontend.
			hosts[r.Host] = struct{}{}

			if r.HTTP == nil {
				continue
			}

			// There are multiple paths per rule. We want to flatten the list
			// for our frontend.
			for _, p := range r.HTTP.Paths {
				var path models.K8sIngressPath
				path.IngressName = info.Name
				path.Host = r.Host

				path.Path = p.Path
				if p.PathType != nil {
					path.PathType = string(*p.PathType)
				}
				path.ServiceName = p.Backend.Service.Name
				path.Port = int(p.Backend.Service.Port.Number)
				info.Paths = append(info.Paths, path)
			}
		}

		// Store list of hosts.
		for host := range hosts {
			info.Hosts = append(info.Hosts, host)
		}

		infos = append(infos, info)
	}

	return infos, nil
}

// CreateIngress creates a new ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error {
	ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
	var ingress netv1.Ingress

	ingress.Name = info.Name
	ingress.Namespace = info.Namespace
	if info.ClassName != "" {
		ingress.Spec.IngressClassName = &info.ClassName
	}
	ingress.Annotations = info.Annotations
	if ingress.Labels == nil {
		ingress.Labels = make(map[string]string)
	}
	ingress.Labels["io.portainer.kubernetes.ingress.owner"] = stackutils.SanitizeLabel(owner)

	// Store TLS information.
	var tls []netv1.IngressTLS
	for _, i := range info.TLS {
		tls = append(tls, netv1.IngressTLS{
			Hosts:      i.Hosts,
			SecretName: i.SecretName,
		})
	}
	ingress.Spec.TLS = tls

	// Parse "paths" into rules with paths.
	rules := make(map[string][]netv1.HTTPIngressPath)
	for _, path := range info.Paths {
		pathType := netv1.PathType(path.PathType)
		rules[path.Host] = append(rules[path.Host], netv1.HTTPIngressPath{
			Path:     path.Path,
			PathType: &pathType,
			Backend: netv1.IngressBackend{
				Service: &netv1.IngressServiceBackend{
					Name: path.ServiceName,
					Port: netv1.ServiceBackendPort{
						Number: int32(path.Port),
					},
				},
			},
		})
	}

	for rule, paths := range rules {
		ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
			Host: rule,
			IngressRuleValue: netv1.IngressRuleValue{
				HTTP: &netv1.HTTPIngressRuleValue{
					Paths: paths,
				},
			},
		})
	}

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

// DeleteIngresses processes a K8sIngressDeleteRequest by deleting each ingress
// in its given namespace.
func (kcl *KubeClient) DeleteIngresses(reqs models.K8sIngressDeleteRequests) error {
	var err error
	for namespace := range reqs {
		for _, ingress := range reqs[namespace] {
			ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
			err = ingressClient.Delete(
				context.Background(),
				ingress,
				metav1.DeleteOptions{},
			)
		}
	}
	return err
}

// UpdateIngress updates an existing ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInfo) error {
	ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
	var ingress netv1.Ingress

	ingress.Name = info.Name
	ingress.Namespace = info.Namespace
	if info.ClassName != "" {
		ingress.Spec.IngressClassName = &info.ClassName
	}
	ingress.Annotations = info.Annotations

	// Store TLS information.
	var tls []netv1.IngressTLS
	for _, i := range info.TLS {
		tls = append(tls, netv1.IngressTLS{
			Hosts:      i.Hosts,
			SecretName: i.SecretName,
		})
	}
	ingress.Spec.TLS = tls

	// Parse "paths" into rules with paths.
	rules := make(map[string][]netv1.HTTPIngressPath)
	for _, path := range info.Paths {
		pathType := netv1.PathType(path.PathType)
		rules[path.Host] = append(rules[path.Host], netv1.HTTPIngressPath{
			Path:     path.Path,
			PathType: &pathType,
			Backend: netv1.IngressBackend{
				Service: &netv1.IngressServiceBackend{
					Name: path.ServiceName,
					Port: netv1.ServiceBackendPort{
						Number: int32(path.Port),
					},
				},
			},
		})
	}

	for rule, paths := range rules {
		ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
			Host: rule,
			IngressRuleValue: netv1.IngressRuleValue{
				HTTP: &netv1.HTTPIngressRuleValue{
					Paths: paths,
				},
			},
		})
	}

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