package main

import (
	"fmt"
	"iter"
	"k8s.io/api/core/v1"
	"os"
	"slices"
	"strconv"
)

type ValidationLevel int

const (
	Info ValidationLevel = iota
	Warning
	Error
)

type Resolver interface {
	ServiceAccounts(application *Application) []string
	PortNumbers(application *Application) []Port
}

func LogValidationMsg(level ValidationLevel, msg string, v ...any) {
	fmt.Fprintf(os.Stderr, "NOTICE: "+msg+"\n", v...)
}

func IterToSlice[K any](i iter.Seq[K]) []K {
	res := make([]K, 0)
	for v := range i {
		res = append(res, v)
	}
	return res
}

func validate(files []string, options *Options) error {
	clientset, _ := GetKubernetesConnection()
	config, err := readConfig(files)
	if err != nil {
		return err
	}

	cluster, err := NewCluster(clientset)
	if err != nil {
		return err
	}

	config.Infer(cluster)

	fmt.Fprintln(os.Stderr, "")

	// map  applname1 -> appname2 where appname1 is in an open namespace and app2 is in a closed namespace.
	// Exclusing when 'from' side is a CIDR.
	openToClosedAccess := make(map[string]string)

	applicationPods := make(map[string][]v1.Pod)
	for _, ns := range config.Namespaces {
		namespace := ns.Name

		if cluster.Namespace(namespace).Name != namespace {
			LogValidationMsg(Error, "ERROR: namespace not found: %s", namespace)
			continue
		}
		if !ns.Open {
			podsNotPartOfAnyApplication(cluster, namespace, ns)
		}

		// checking for service accounts shared by applications
		// map of namespace/sa -> []applicationname
		serviceAccountMap := make(map[string][]string)

		for _, application := range ns.Applications {
			pods := cluster.Pods(application)
			applicationPods[application.Name] = pods
			//log.Printf(namespace + "/" + application.Name)
			if len(pods) == 0 {
				LogValidationMsg(Error, "application %s: no running pods found", application.Name)
			}
			ownerReferences := cluster.OwnerReferences(application)
			if len(ownerReferences) > 1 {
				LogValidationMsg(Error, "Application %s: multiple owners found:  %v. The application definition can possibly be made more fine-grain", application.Name, ownerReferences)
			}
			// check ports
			for _, port := range application.Ports {
				for _, pod := range pods {
					if !HasPort(pod, port) {
						LogValidationMsg(Error, "application %s: port %v not found in pod %s/%s",
							application.Name, port, pod.Namespace, pod.Name)
					}
				}
			}
			// Check service accounts
			applicationServiceAccounts := make(map[string]bool)
			for _, sa := range application.ServiceAccounts {
				applicationServiceAccounts[sa] = true
			}
			for _, pod := range pods {
				delete(applicationServiceAccounts, pod.Spec.ServiceAccountName)
			}
			if len(applicationServiceAccounts) > 0 {
				LogValidationMsg(Error, "application %s: service accounts %v configured but not used by running workloads",
					application.Name, MapKeys(applicationServiceAccounts))
			}
			for _, pod := range pods {
				sa := pod.Namespace + "/" + pod.Spec.ServiceAccountName
				if !slices.Contains(serviceAccountMap[sa], application.Name) {
					serviceAccountMap[sa] = append(serviceAccountMap[sa],
						application.Name)
				}
				if pod.Spec.ServiceAccountName == "default" {
					LogValidationMsg(Warning, "Pod %s/%s: running with default service account",
						pod.Namespace, pod.Name)
					if application.ServiceAccounts != nil && !slices.Contains(application.ServiceAccounts, "default") {
						LogValidationMsg(Warning, "application %s: Pod %s/%s: running with 'default' service account but configured with %v",
							application.Name, pod.Namespace, pod.Name, application.ServiceAccounts)
					}
				} else {
					if !slices.Contains(application.ServiceAccounts, pod.Spec.ServiceAccountName) {
						LogValidationMsg(Warning, "application %s: Pod %s/%s: running with service account '%s' but configured with %v",
							application.Name, pod.Namespace, pod.Name, pod.Spec.ServiceAccountName, application.ServiceAccounts)
					}
				}
			}
		}

		// service accounts shared by multiple applications.
		for sa, applist := range serviceAccountMap {
			if len(applist) == 1 {
				continue
			}
			LogValidationMsg(Error, "service account %s: shared by multiple applications %v, the application definition can be made more fine-grain.", sa, applist)
		}
	}

	for _, communication := range config.Communications {
		if len(communication.Ports) == 0 {
			continue
		}
		for _, applicationName := range communication.To {
			application, _, _ := config.GetApplication(applicationName)
			if application == nil {
				continue
			}
			for _, port := range communication.Ports {
				pods := cluster.Pods(application)
				for _, pod := range pods {
					if !HasPort(pod, port) {
						LogValidationMsg(Error, "communication %v -> %v: port %v is not configured in pod %s/%s",
							communication.From, communication.To, port, pod.Namespace, pod.Name)
					}
				}

			}
		}
		for _, communication := range config.Communications {
			for _, applicationName := range communication.To {
				application, _, _ := config.GetApplication(applicationName)
				if application == nil {
					continue
				}
				// capability linkerd must exist on target namespace
				if !slices.Contains(application.Namespace.Capabilities, "linkerd") {
					continue
				}
				// annotation linkerd.io/inject must not be disabled
				pods := applicationPods[applicationName]
				for _, pod := range pods {
					if pod.Annotations["linkerd.io/inject"] == "disabled" {
						continue
					}
				}

				if !application.Namespace.Open {
					for _, applicationNameFrom := range communication.From {
						applicationFrom, networkFrom, _ := config.GetApplication(applicationNameFrom)
						if applicationFrom != nil && !applicationFrom.Namespace.Open {
							continue
						}
						if networkFrom == nil && cluster.IsLinkerdEnabled(application) {
							openToClosedAccess[applicationNameFrom] = applicationName
						}
					}
				}
			}
		}
	}

	for appFrom, appTo := range openToClosedAccess {
		LogValidationMsg(Error, "Access from 'open' application '%s' to 'closed' application '%s'. This will lead to generation of a network authentication for this workload.",
			appFrom, appTo)
	}

	return nil
}

func podsNotPartOfAnyApplication(cluster *Cluster, namespace string, ns *Namespace) {
	// Pods in the nemsapce that are not covered by any application

	namespacePods := cluster.PodList(namespace)
	namespacePods = slices.DeleteFunc(namespacePods, func(pod v1.Pod) bool {
		return pod.Spec.HostNetwork == true
	})
	podNames := make(map[string]bool)
	for _, pod := range namespacePods {
		podNames[pod.Name] = true
	}
	for _, application := range ns.Applications {
		for _, pod := range cluster.Pods(application) {
			delete(podNames, pod.Name)
		}
	}
	for podName, _ := range podNames {
		LogValidationMsg(Error, "ERROR: pod %s/%s not part of any applications",
			namespace, podName)
	}
}

func HasPort(pod v1.Pod, port Port) bool {
	if port.Protocol == "" {
		port.Protocol = "TCP"
	}
	for _, container := range pod.Spec.Containers {
		for _, kport := range container.Ports {
			if kport.Protocol == "" {
				kport.Protocol = "TCP"
			}
			if port.Protocol == string(kport.Protocol) && (port.Port == kport.Name || port.Port == strconv.Itoa(int(kport.ContainerPort))) {
				return true
			}
		}
	}
	return false
}