package k8s

import (
	"context"
	"fmt"
	"strings"

	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"

	"github.com/ocdp/cluster-service/internal/domain/entity"
	"github.com/ocdp/cluster-service/internal/domain/repository"
)

// EntryClient 使用 Kubernetes API 查询实例相关 Service/Ingress
type EntryClient struct{}

// NewEntryClient 创建 EntryClient
func NewEntryClient() repository.InstanceEntryClient {
	return &EntryClient{}
}

// ListEntries 查询实例的 Service/Ingress 入口
func (c *EntryClient) ListEntries(
	ctx context.Context,
	cluster *entity.Cluster,
	instance *entity.Instance,
) ([]*entity.InstanceEntry, error) {
	clientset, err := c.createClientset(cluster)
	if err != nil {
		return nil, err
	}

	selector := fmt.Sprintf("app.kubernetes.io/instance=%s", instance.Name)

	serviceEntries, err := c.collectServiceEntries(ctx, clientset, instance, selector)
	if err != nil {
		return nil, err
	}
	ingressEntries, err := c.collectIngressEntries(ctx, clientset, instance, selector)
	if err != nil {
		return nil, err
	}

	return append(serviceEntries, ingressEntries...), nil
}

func (c *EntryClient) collectServiceEntries(
	ctx context.Context,
	clientset *kubernetes.Clientset,
	instance *entity.Instance,
	selector string,
) ([]*entity.InstanceEntry, error) {
	services, err := c.listServices(ctx, clientset, instance.Namespace, selector)
	if err != nil {
		return nil, err
	}
	entries := convertServicesToEntries(services, instance, selector == "")
	if len(entries) == 0 && selector != "" {
		// Fallback: widen the search scope and filter manually.
		services, err = c.listServices(ctx, clientset, instance.Namespace, "")
		if err != nil {
			return nil, err
		}
		entries = convertServicesToEntries(services, instance, true)
	}
	return entries, nil
}

func (c *EntryClient) collectIngressEntries(
	ctx context.Context,
	clientset *kubernetes.Clientset,
	instance *entity.Instance,
	selector string,
) ([]*entity.InstanceEntry, error) {
	ingresses, err := c.listIngresses(ctx, clientset, instance.Namespace, selector)
	if err != nil {
		return nil, err
	}
	entries := convertIngressesToEntries(ingresses, instance, selector == "")
	if len(entries) == 0 && selector != "" {
		ingresses, err = c.listIngresses(ctx, clientset, instance.Namespace, "")
		if err != nil {
			return nil, err
		}
		entries = convertIngressesToEntries(ingresses, instance, true)
	}
	return entries, nil
}

func (c *EntryClient) listServices(
	ctx context.Context,
	clientset *kubernetes.Clientset,
	namespace, selector string,
) ([]corev1.Service, error) {
	listOptions := metav1.ListOptions{}
	if selector != "" {
		listOptions.LabelSelector = selector
	}
	services, err := clientset.CoreV1().
		Services(namespace).
		List(ctx, listOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to list services: %w", err)
	}
	return services.Items, nil
}

func (c *EntryClient) listIngresses(
	ctx context.Context,
	clientset *kubernetes.Clientset,
	namespace, selector string,
) ([]networkingv1.Ingress, error) {
	listOptions := metav1.ListOptions{}
	if selector != "" {
		listOptions.LabelSelector = selector
	}
	ingresses, err := clientset.NetworkingV1().
		Ingresses(namespace).
		List(ctx, listOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to list ingresses: %w", err)
	}
	return ingresses.Items, nil
}

func convertServicesToEntries(services []corev1.Service, instance *entity.Instance, enforceMatch bool) []*entity.InstanceEntry {
	entries := make([]*entity.InstanceEntry, 0, len(services))
	for _, svc := range services {
		if enforceMatch && !resourceMatchesInstance(svc.ObjectMeta, instance) {
			continue
		}
		entries = append(entries, convertServiceToEntry(&svc))
	}
	return entries
}

func convertIngressesToEntries(ingresses []networkingv1.Ingress, instance *entity.Instance, enforceMatch bool) []*entity.InstanceEntry {
	entries := make([]*entity.InstanceEntry, 0, len(ingresses))
	for _, ing := range ingresses {
		if enforceMatch && !resourceMatchesInstance(ing.ObjectMeta, instance) {
			continue
		}
		entries = append(entries, convertIngressToEntry(&ing))
	}
	return entries
}

func (c *EntryClient) createClientset(cluster *entity.Cluster) (*kubernetes.Clientset, error) {
	config, err := clientcmd.RESTConfigFromKubeConfig([]byte(cluster.GetKubeConfig()))
	if err != nil {
		config = &rest.Config{
			Host: cluster.Host,
			TLSClientConfig: rest.TLSClientConfig{
				CAData:   []byte(cluster.CAData),
				CertData: []byte(cluster.CertData),
				KeyData:  []byte(cluster.KeyData),
			},
			BearerToken: cluster.Token,
		}
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, fmt.Errorf("failed to create kubernetes client: %w", err)
	}

	return clientset, nil
}

func convertServiceToEntry(svc *corev1.Service) *entity.InstanceEntry {
	clusterIP := svc.Spec.ClusterIP
	if clusterIP == corev1.ClusterIPNone {
		clusterIP = ""
	}

	lbIngress := make([]string, 0, len(svc.Status.LoadBalancer.Ingress))
	for _, ing := range svc.Status.LoadBalancer.Ingress {
		if ing.IP != "" {
			lbIngress = append(lbIngress, ing.IP)
		}
		if ing.Hostname != "" {
			lbIngress = append(lbIngress, ing.Hostname)
		}
	}

	ports := make([]entity.InstanceEntryPort, 0, len(svc.Spec.Ports))
	for _, port := range svc.Spec.Ports {
		ports = append(ports, entity.InstanceEntryPort{
			Name:       port.Name,
			Protocol:   string(port.Protocol),
			Port:       port.Port,
			TargetPort: intOrStringToString(port.TargetPort),
			NodePort:   port.NodePort,
		})
	}

	return &entity.InstanceEntry{
		Kind:                "Service",
		Name:                svc.Name,
		Namespace:           svc.Namespace,
		Type:                string(svc.Spec.Type),
		ClusterIP:           clusterIP,
		ExternalIPs:         append([]string{}, svc.Spec.ExternalIPs...),
		LoadBalancerIngress: lbIngress,
		Ports:               ports,
	}
}

func convertIngressToEntry(ing *networkingv1.Ingress) *entity.InstanceEntry {
	lbIngress := make([]string, 0, len(ing.Status.LoadBalancer.Ingress))
	for _, addr := range ing.Status.LoadBalancer.Ingress {
		if addr.IP != "" {
			lbIngress = append(lbIngress, addr.IP)
		}
		if addr.Hostname != "" {
			lbIngress = append(lbIngress, addr.Hostname)
		}
	}

	hosts := make([]entity.InstanceEntryHost, 0, len(ing.Spec.Rules))
	for _, rule := range ing.Spec.Rules {
		hostEntry := entity.InstanceEntryHost{
			Host: rule.Host,
		}
		if rule.HTTP != nil {
			paths := make([]entity.InstanceEntryPath, 0, len(rule.HTTP.Paths))
			for _, path := range rule.HTTP.Paths {
				name := ""
				port := ""
				if path.Backend.Service != nil {
					name = path.Backend.Service.Name
					port = serviceBackendPortString(path.Backend.Service.Port)
				}
				paths = append(paths, entity.InstanceEntryPath{
					Path:        path.Path,
					ServiceName: name,
					ServicePort: port,
				})
			}
			hostEntry.Paths = paths
		}
		hosts = append(hosts, hostEntry)
	}

	tlsEntries := make([]entity.InstanceEntryTLS, 0, len(ing.Spec.TLS))
	for _, tls := range ing.Spec.TLS {
		tlsEntries = append(tlsEntries, entity.InstanceEntryTLS{
			Hosts:      append([]string{}, tls.Hosts...),
			SecretName: tls.SecretName,
		})
	}

	entryType := "Ingress"
	if ing.Spec.IngressClassName != nil {
		entryType = *ing.Spec.IngressClassName
	}

	return &entity.InstanceEntry{
		Kind:                "Ingress",
		Name:                ing.Name,
		Namespace:           ing.Namespace,
		Type:                entryType,
		LoadBalancerIngress: lbIngress,
		Hosts:               hosts,
		TLS:                 tlsEntries,
	}
}

func intOrStringToString(v intstr.IntOrString) string {
	if v.Type == intstr.String {
		return v.StrVal
	}
	return fmt.Sprintf("%d", v.IntValue())
}

func serviceBackendPortString(port networkingv1.ServiceBackendPort) string {
	if port.Name != "" {
		return port.Name
	}
	if port.Number != 0 {
		return fmt.Sprintf("%d", port.Number)
	}
	return ""
}

func resourceMatchesInstance(meta metav1.ObjectMeta, instance *entity.Instance) bool {
	if instance == nil {
		return false
	}
	labels := meta.GetLabels()
	if labels != nil {
		if labels["app.kubernetes.io/instance"] == instance.Name {
			return true
		}
		labelKeys := []string{"app", "app.kubernetes.io/name", "app.kubernetes.io/component", "release"}
		for _, key := range labelKeys {
			if labels[key] == instance.Name {
				return true
			}
		}
	}
	annotations := meta.GetAnnotations()
	if annotations != nil {
		if annotations["meta.helm.sh/release-name"] == instance.Name {
			if ns := annotations["meta.helm.sh/release-namespace"]; ns == "" || ns == instance.Namespace {
				return true
			}
		}
	}
	name := meta.GetName()
	if name == instance.Name || strings.HasPrefix(name, instance.Name+"-") {
		return true
	}
	return false
}
