package cli

import (
	"context"
	"fmt"
	"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"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) {
	classeses, err := kcl.cli.NetworkingV1().IngressClasses().List(context.Background(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	ingresses, err := kcl.GetIngresses("")
	if err != nil {
		return nil, err
	}

	usedClasses := make(map[string]struct{})
	for _, ingress := range ingresses {
		usedClasses[ingress.ClassName] = struct{}{}
	}

	results := []models.K8sIngressController{}
	for _, class := range classeses.Items {
		ingressClass := parseIngressClass(class)
		if _, ok := usedClasses[class.Name]; ok {
			ingressClass.Used = true
		}

		results = append(results, ingressClass)
	}

	return results, nil
}

// fetchIngressClasses fetches all the ingress classes in a k8s endpoint.
func (kcl *KubeClient) fetchIngressClasses() ([]models.K8sIngressController, error) {
	ingressClasses, err := kcl.cli.NetworkingV1().IngressClasses().List(context.Background(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	var controllers []models.K8sIngressController
	for _, ingressClass := range ingressClasses.Items {
		controllers = append(controllers, parseIngressClass(ingressClass))
	}
	return controllers, nil
}

// parseIngressClass converts a k8s native ingress class object to a Portainer K8sIngressController object.
func parseIngressClass(ingressClasses netv1.IngressClass) models.K8sIngressController {
	ingressContoller := models.K8sIngressController{
		Name:      ingressClasses.Spec.Controller,
		ClassName: ingressClasses.Name,
	}

	switch {
	case strings.Contains(ingressContoller.Name, "nginx"):
		ingressContoller.Type = "nginx"
	case strings.Contains(ingressContoller.Name, "traefik"):
		ingressContoller.Type = "traefik"
	default:
		ingressContoller.Type = "other"
	}

	return ingressContoller
}

// GetIngress gets an ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) GetIngress(namespace, ingressName string) (models.K8sIngressInfo, error) {
	ingress, err := kcl.cli.NetworkingV1().Ingresses(namespace).Get(context.Background(), ingressName, metav1.GetOptions{})
	if err != nil {
		return models.K8sIngressInfo{}, err
	}

	return parseIngress(*ingress), nil
}

// GetIngresses gets all the ingresses for a given namespace in a k8s endpoint.
func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, error) {
	if kcl.GetIsKubeAdmin() {
		return kcl.fetchIngresses(namespace)
	}

	return kcl.fetchIngressesForNonAdmin(namespace)
}

// fetchIngressesForNonAdmin gets all the ingresses for non-admin users in a k8s endpoint.
func (kcl *KubeClient) fetchIngressesForNonAdmin(namespace string) ([]models.K8sIngressInfo, error) {
	nonAdminNamespaces := kcl.GetClientNonAdminNamespaces()

	log.Debug().
		Strs("non_admin_namespaces", nonAdminNamespaces).
		Msg("fetching ingresses for non-admin user")

	if len(nonAdminNamespaces) == 0 {
		return nil, nil
	}

	ingresses, err := kcl.fetchIngresses(namespace)
	if err != nil {
		return nil, err
	}

	nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
	results := make([]models.K8sIngressInfo, 0)
	for _, ingress := range ingresses {
		if _, ok := nonAdminNamespaceSet[ingress.Namespace]; ok {
			results = append(results, ingress)
		}
	}

	return results, nil
}

// fetchIngresses fetches all the ingresses for a given namespace in a k8s endpoint.
func (kcl *KubeClient) fetchIngresses(namespace string) ([]models.K8sIngressInfo, error) {
	ingresses, err := kcl.cli.NetworkingV1().Ingresses(namespace).List(context.Background(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	ingressClasses, err := kcl.fetchIngressClasses()
	if err != nil {
		return nil, err
	}

	results := []models.K8sIngressInfo{}
	if len(ingresses.Items) == 0 {
		return results, nil
	}

	for _, ingress := range ingresses.Items {
		result := parseIngress(ingress)
		if ingress.Spec.IngressClassName != nil {
			result.Type = findUsedIngressFromIngressClasses(ingressClasses, *ingress.Spec.IngressClassName).Name
		}
		results = append(results, result)
	}

	return results, nil
}

// parseIngress converts a k8s native ingress object to a Portainer K8sIngressInfo object.
func parseIngress(ingress netv1.Ingress) models.K8sIngressInfo {
	ingressClassName := ""
	if ingress.Spec.IngressClassName != nil {
		ingressClassName = *ingress.Spec.IngressClassName
	}

	result := models.K8sIngressInfo{
		Name:         ingress.Name,
		Namespace:    ingress.Namespace,
		UID:          string(ingress.UID),
		Annotations:  ingress.Annotations,
		Labels:       ingress.Labels,
		CreationDate: ingress.CreationTimestamp.Time,
		ClassName:    ingressClassName,
	}

	for _, tls := range ingress.Spec.TLS {
		result.TLS = append(result.TLS, models.K8sIngressTLS{
			Hosts:      tls.Hosts,
			SecretName: tls.SecretName,
		})
	}

	hosts := make(map[string]struct{})
	for _, r := range ingress.Spec.Rules {
		hosts[r.Host] = struct{}{}

		if r.HTTP == nil {
			continue
		}
		for _, p := range r.HTTP.Paths {
			var path models.K8sIngressPath
			path.IngressName = result.Name
			path.Host = r.Host

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

	for host := range hosts {
		result.Hosts = append(result.Hosts, host)
	}

	return result
}

// findUsedIngressFromIngressClasses searches for an ingress in a slice of ingress classes and returns the ingress if found.
func findUsedIngressFromIngressClasses(ingressClasses []models.K8sIngressController, className string) models.K8sIngressController {
	for _, ingressClass := range ingressClasses {
		if ingressClass.ClassName == className {
			return ingressClass
		}
	}

	return models.K8sIngressController{}
}

// 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 {
	ingress := kcl.convertToK8sIngress(info, owner)
	_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Create(context.Background(), &ingress, metav1.CreateOptions{})
	if err != nil {
		return err
	}

	return nil
}

// convertToK8sIngress converts a Portainer K8sIngressInfo object to a k8s native Ingress object.
// this is required for create and update operations.
func (kcl *KubeClient) convertToK8sIngress(info models.K8sIngressInfo, owner string) netv1.Ingress {
	ingressSpec := netv1.IngressSpec{}
	if info.ClassName != "" {
		ingressSpec.IngressClassName = &info.ClassName
	}

	result := netv1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:        info.Name,
			Namespace:   info.Namespace,
			Annotations: info.Annotations,
		},

		Spec: ingressSpec,
	}

	labels := make(map[string]string)
	labels["io.portainer.kubernetes.ingress.owner"] = stackutils.SanitizeLabel(owner)
	result.Labels = labels

	tls := []netv1.IngressTLS{}
	for _, t := range info.TLS {
		tls = append(tls, netv1.IngressTLS{
			Hosts:      t.Hosts,
			SecretName: t.SecretName,
		})
	}
	result.Spec.TLS = tls

	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 {
		result.Spec.Rules = append(result.Spec.Rules, netv1.IngressRule{
			Host: rule,
			IngressRuleValue: netv1.IngressRuleValue{
				HTTP: &netv1.HTTPIngressRuleValue{
					Paths: paths,
				},
			},
		})
	}

	for _, host := range info.Hosts {
		if _, ok := rules[host]; !ok {
			result.Spec.Rules = append(result.Spec.Rules, netv1.IngressRule{
				Host: host,
			})
		}
	}

	return result
}

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

			if err != nil {
				return err
			}
		}
	}

	return nil
}

// UpdateIngress updates an existing ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInfo) error {
	ingress := kcl.convertToK8sIngress(info, "")
	_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Update(context.Background(), &ingress, metav1.UpdateOptions{})
	if err != nil {
		return err
	}

	return nil
}

// CombineIngressWithService combines an ingress with a service that is being used by the ingress.
// this is required to display the service that is being used by the ingress in the UI edit view.
func (kcl *KubeClient) CombineIngressWithService(ingress models.K8sIngressInfo) (models.K8sIngressInfo, error) {
	services, err := kcl.GetServices(ingress.Namespace)
	if err != nil {
		return models.K8sIngressInfo{}, fmt.Errorf("an error occurred during the CombineIngressWithService operation, unable to retrieve services from the Kubernetes for a namespace level user. Error: %w", err)
	}

	serviceMap := kcl.buildServicesMap(services)
	for pathIndex, path := range ingress.Paths {
		if _, ok := serviceMap[path.ServiceName]; ok {
			ingress.Paths[pathIndex].HasService = true
		}
	}

	return ingress, nil
}

// CombineIngressesWithServices combines a list of ingresses with a list of services that are being used by the ingresses.
// this is required to display the services that are being used by the ingresses in the UI list view.
func (kcl *KubeClient) CombineIngressesWithServices(ingresses []models.K8sIngressInfo) ([]models.K8sIngressInfo, error) {
	services, err := kcl.GetServices("")
	if err != nil {
		if k8serrors.IsUnauthorized(err) {
			return nil, fmt.Errorf("an error occurred during the CombineIngressesWithServices operation, unauthorized access to the Kubernetes API. Error: %w", err)
		}

		return nil, fmt.Errorf("an error occurred during the CombineIngressesWithServices operation, unable to retrieve services from the Kubernetes for a cluster level user. Error: %w", err)
	}

	serviceMap := kcl.buildServicesMap(services)
	for ingressIndex, ingress := range ingresses {
		for pathIndex, path := range ingress.Paths {
			if _, ok := serviceMap[path.ServiceName]; ok {
				(ingresses)[ingressIndex].Paths[pathIndex].HasService = true
			}
		}
	}

	return ingresses, nil
}
