/*
Copyright 2026.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package common

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"strings"

	yamlv3 "gopkg.in/yaml.v3"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	apiequality "k8s.io/apimachinery/pkg/api/equality"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/client-go/util/retry"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	infrav1alpha1 "github.com/sandbox0-ai/sandbox0/infra-operator/api/v1alpha1"
	"github.com/sandbox0-ai/sandbox0/pkg/internalauth"
	"github.com/sandbox0-ai/sandbox0/pkg/naming"
)

type ResourceManager struct {
	Client client.Client
	Scheme *runtime.Scheme
	// ImagePullPolicy overrides container pull policy when set.
	ImagePullPolicy *corev1.PullPolicy
	// LocalDev config for running operator outside the cluster.
	LocalDev LocalDevConfig
}

type LocalDevConfig struct {
	LocalDevMode   bool
	KubeconfigPath string
}

const PodTemplateConfigHashAnnotation = "infra.sandbox0.ai/config-hash"

const (
	ServiceConfigBaseNameAnnotation = "infra.sandbox0.ai/service-config-base-name"
	ServiceConfigHashAnnotation     = "infra.sandbox0.ai/service-config-hash"
	serviceConfigNameHashLength     = 16
)

func NewResourceManager(client client.Client, scheme *runtime.Scheme, imagePullPolicy *corev1.PullPolicy, localDev LocalDevConfig) *ResourceManager {
	return &ResourceManager{
		Client:          client,
		Scheme:          scheme,
		ImagePullPolicy: imagePullPolicy,
		LocalDev:        localDev,
	}
}

// UpdateObjectIfChanged updates obj only when mutate changes the live object.
func (r *ResourceManager) UpdateObjectIfChanged(ctx context.Context, obj client.Object, mutate func()) error {
	before := obj.DeepCopyObject()
	mutate()
	if apiequality.Semantic.DeepEqual(before, obj) {
		return nil
	}
	return r.Client.Update(ctx, obj)
}

// ServiceDefinition defines deployment/daemonset configuration for a service.
type ServiceDefinition struct {
	Name               string
	Port               int32
	TargetPort         int32
	Ports              []corev1.ContainerPort
	Image              string
	ImagePullPolicy    *corev1.PullPolicy
	Command            []string
	Args               []string
	EnvVars            []corev1.EnvVar
	VolumeMounts       []corev1.VolumeMount
	Volumes            []corev1.Volume
	PodAnnotations     map[string]string
	LivenessProbe      *corev1.Probe
	ReadinessProbe     *corev1.Probe
	ServiceAccountName string
	Resources          *corev1.ResourceRequirements
}

type ServiceConfigRef struct {
	ConfigMapName string
	Hash          string
}

// PodAnnotations returns the pod-template annotations that roll workloads when
// the referenced service config changes.
func (r ServiceConfigRef) PodAnnotations() map[string]string {
	return ConfigHashAnnotationFromHash(r.Hash)
}

// ReconcileDeployment creates or updates a deployment.
func (r *ResourceManager) ReconcileDeployment(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, name string, labels map[string]string, replicas int32, def ServiceDefinition) error {
	return r.ReconcileDeploymentWithScope(ctx, NewObjectScope(infra), name, labels, replicas, def)
}

func (r *ResourceManager) ReconcileDeploymentWithScope(ctx context.Context, scope ObjectScope, name string, labels map[string]string, replicas int32, def ServiceDefinition) error {
	deploy := &appsv1.Deployment{}
	err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: scope.Namespace}, deploy)
	if err != nil && !errors.IsNotFound(err) {
		return err
	}

	defaultResources := corev1.ResourceRequirements{
		Requests: corev1.ResourceList{
			corev1.ResourceCPU:    resource.MustParse("100m"),
			corev1.ResourceMemory: resource.MustParse("256Mi"),
		},
		Limits: corev1.ResourceList{
			corev1.ResourceCPU:    resource.MustParse("500m"),
			corev1.ResourceMemory: resource.MustParse("512Mi"),
		},
	}
	if def.Resources != nil {
		defaultResources = *def.Resources
	}

	desiredLabels := EnsureManagedLabels(labels, name)
	desiredDeploy := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: scope.Namespace,
			Labels:    desiredLabels,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: labels,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels:      desiredLabels,
					Annotations: EnsurePodTemplateAnnotations(def.PodAnnotations),
				},
				Spec: corev1.PodSpec{
					ServiceAccountName: def.ServiceAccountName,
					Containers: []corev1.Container{
						{
							Name:            def.Name,
							Image:           def.Image,
							ImagePullPolicy: ResolveImagePullPolicy(def, r.ImagePullPolicy),
							Command:         def.Command,
							Args:            def.Args,
							Env:             def.EnvVars,
							VolumeMounts:    def.VolumeMounts,
							Ports:           ResolveContainerPorts(def),
							Resources:       defaultResources,
							LivenessProbe:   def.LivenessProbe,
							ReadinessProbe:  def.ReadinessProbe,
						},
					},
					Volumes: def.Volumes,
				},
			},
		},
	}

	if err := scope.SetControllerReference(desiredDeploy, r.Scheme); err != nil {
		return err
	}

	if errors.IsNotFound(err) {
		return r.Client.Create(ctx, desiredDeploy)
	}

	return r.UpdateObjectIfChanged(ctx, deploy, func() {
		deploy.Labels = desiredLabels
		deploy.OwnerReferences = desiredDeploy.OwnerReferences
		deploy.Spec.Replicas = desiredDeploy.Spec.Replicas
		deploy.Spec.Selector = desiredDeploy.Spec.Selector
		deploy.Spec.Template = desiredDeploy.Spec.Template
	})
}

// EnsureDeploymentReady validates deployment readiness before reporting success.
func (r *ResourceManager) EnsureDeploymentReady(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, name string, replicas int32) error {
	return r.EnsureDeploymentReadyWithScope(ctx, NewObjectScope(infra), name, replicas)
}

func (r *ResourceManager) EnsureDeploymentReadyWithScope(ctx context.Context, scope ObjectScope, name string, replicas int32) error {
	if replicas == 0 {
		return nil
	}

	deploy := &appsv1.Deployment{}
	if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: scope.Namespace}, deploy); err != nil {
		return err
	}

	desired := replicas
	if deploy.Spec.Replicas != nil {
		desired = *deploy.Spec.Replicas
	}
	if desired == 0 {
		return nil
	}
	if deploy.Status.ReadyReplicas < desired {
		return fmt.Errorf("deployment %q not ready: %d/%d ready", name, deploy.Status.ReadyReplicas, desired)
	}
	return nil
}

// ReconcileDaemonSet creates or updates a daemonset.
func (r *ResourceManager) ReconcileDaemonSet(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, name string, labels map[string]string, def ServiceDefinition) error {
	return r.ReconcileDaemonSetWithScope(ctx, NewObjectScope(infra), name, labels, def)
}

func (r *ResourceManager) ReconcileDaemonSetWithScope(ctx context.Context, scope ObjectScope, name string, labels map[string]string, def ServiceDefinition) error {
	defaultResources := corev1.ResourceRequirements{
		Requests: corev1.ResourceList{
			corev1.ResourceCPU:    resource.MustParse("100m"),
			corev1.ResourceMemory: resource.MustParse("128Mi"),
		},
		Limits: corev1.ResourceList{
			corev1.ResourceCPU:    resource.MustParse("500m"),
			corev1.ResourceMemory: resource.MustParse("256Mi"),
		},
	}
	if def.Resources != nil {
		defaultResources = *def.Resources
	}

	desiredLabels := EnsureManagedLabels(labels, name)
	desiredDs := &appsv1.DaemonSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: scope.Namespace,
			Labels:    desiredLabels,
		},
		Spec: appsv1.DaemonSetSpec{
			Selector: &metav1.LabelSelector{
				MatchLabels: labels,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels:      desiredLabels,
					Annotations: EnsurePodTemplateAnnotations(def.PodAnnotations),
				},
				Spec: corev1.PodSpec{
					ServiceAccountName: def.ServiceAccountName,
					HostNetwork:        true,
					HostPID:            true,
					DNSPolicy:          corev1.DNSClusterFirstWithHostNet,
					Containers: []corev1.Container{
						{
							Name:            def.Name,
							Image:           def.Image,
							ImagePullPolicy: ResolveImagePullPolicy(def, r.ImagePullPolicy),
							Env:             def.EnvVars,
							VolumeMounts:    def.VolumeMounts,
							Ports:           ResolveContainerPorts(def),
							SecurityContext: &corev1.SecurityContext{
								Privileged: BoolPtr(true),
								Capabilities: &corev1.Capabilities{
									Add: []corev1.Capability{"NET_ADMIN", "SYS_ADMIN"},
								},
							},
							Resources:      defaultResources,
							LivenessProbe:  def.LivenessProbe,
							ReadinessProbe: def.ReadinessProbe,
						},
					},
					Volumes: def.Volumes,
				},
			},
		},
	}

	if err := scope.SetControllerReference(desiredDs, r.Scheme); err != nil {
		return err
	}

	return r.ApplyDaemonSetWithScope(ctx, scope, desiredDs)
}

// ApplyStatefulSet creates or updates a statefulset using fresh reads on each retry.
func (r *ResourceManager) ApplyStatefulSet(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, desired *appsv1.StatefulSet) error {
	return r.ApplyStatefulSetWithScope(ctx, NewObjectScope(infra), desired)
}

func (r *ResourceManager) ApplyStatefulSetWithScope(ctx context.Context, scope ObjectScope, desired *appsv1.StatefulSet) error {
	if err := scope.SetControllerReference(desired, r.Scheme); err != nil {
		return err
	}

	key := types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		current := &appsv1.StatefulSet{}
		err := r.Client.Get(ctx, key, current)
		if errors.IsNotFound(err) {
			return r.Client.Create(ctx, desired.DeepCopy())
		}
		if err != nil {
			return err
		}

		return r.UpdateObjectIfChanged(ctx, current, func() {
			current.Labels = desired.Labels
			current.Annotations = desired.Annotations
			current.OwnerReferences = desired.OwnerReferences
			current.Spec.ServiceName = desired.Spec.ServiceName
			current.Spec.Replicas = desired.Spec.Replicas
			current.Spec.Selector = desired.Spec.Selector
			current.Spec.Template = desired.Spec.Template
			current.Spec.VolumeClaimTemplates = desired.Spec.VolumeClaimTemplates
			current.Spec.PersistentVolumeClaimRetentionPolicy = desired.Spec.PersistentVolumeClaimRetentionPolicy
			current.Spec.Ordinals = desired.Spec.Ordinals
		})
	})
}

// ApplyDaemonSet creates or updates a daemonset using fresh reads on each retry
// so controller-driven status/resourceVersion updates do not cause reconcile
// loops to fail on optimistic concurrency conflicts.
func (r *ResourceManager) ApplyDaemonSet(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, desired *appsv1.DaemonSet) error {
	return r.ApplyDaemonSetWithScope(ctx, NewObjectScope(infra), desired)
}

func (r *ResourceManager) ApplyDaemonSetWithScope(ctx context.Context, scope ObjectScope, desired *appsv1.DaemonSet) error {
	if err := scope.SetControllerReference(desired, r.Scheme); err != nil {
		return err
	}

	key := types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		current := &appsv1.DaemonSet{}
		err := r.Client.Get(ctx, key, current)
		if errors.IsNotFound(err) {
			return r.Client.Create(ctx, desired.DeepCopy())
		}
		if err != nil {
			return err
		}

		return r.UpdateObjectIfChanged(ctx, current, func() {
			current.Labels = desired.Labels
			current.Annotations = desired.Annotations
			current.OwnerReferences = desired.OwnerReferences
			current.Spec.Selector = desired.Spec.Selector
			current.Spec.Template = desired.Spec.Template
		})
	})
}

func ResolveContainerPorts(def ServiceDefinition) []corev1.ContainerPort {
	if len(def.Ports) > 0 {
		return def.Ports
	}
	if def.TargetPort == 0 {
		return nil
	}
	return []corev1.ContainerPort{
		{
			Name:          "http",
			ContainerPort: def.TargetPort,
		},
	}
}

func ResolveImagePullPolicy(def ServiceDefinition, fallback *corev1.PullPolicy) corev1.PullPolicy {
	if def.ImagePullPolicy != nil {
		return *def.ImagePullPolicy
	}
	if fallback != nil {
		return *fallback
	}
	return ""
}

func ResolveServiceType(config *infrav1alpha1.ServiceNetworkConfig) corev1.ServiceType {
	if config != nil && config.Type != "" {
		return config.Type
	}
	return corev1.ServiceTypeClusterIP
}

func ResolveServicePort(config *infrav1alpha1.ServiceNetworkConfig, fallback int32) int32 {
	if config != nil && config.Port != 0 {
		return config.Port
	}
	return fallback
}

// ResolveRegionID returns the canonical region ID used by data-plane services.
func ResolveRegionID(infra *infrav1alpha1.Sandbox0Infra) string {
	if infra == nil {
		return ""
	}
	if regionID := strings.TrimSpace(infra.Spec.Region); regionID != "" {
		return regionID
	}
	if infra.Spec.PublicExposure == nil {
		return ""
	}
	return strings.TrimSpace(infra.Spec.PublicExposure.RegionID)
}

// ResolveClusterID returns the data-plane cluster ID, defaulting single-cluster installs.
func ResolveClusterID(infra *infrav1alpha1.Sandbox0Infra) string {
	if infra != nil && infra.Spec.Cluster != nil {
		return naming.ClusterIDOrDefault(&infra.Spec.Cluster.ID)
	}
	return naming.DefaultClusterID
}

// ResolveSSHEndpoint returns the advertised SSH endpoint for a region when the
// SSH gateway is enabled and public routing is configured.
func ResolveSSHEndpoint(infra *infrav1alpha1.Sandbox0Infra, fallbackPort int32) (string, int32, bool) {
	if infra == nil || infra.Spec.Services == nil || infra.Spec.Services.SSHGateway == nil || !infra.Spec.Services.SSHGateway.Enabled {
		return "", 0, false
	}
	if infra.Spec.PublicExposure == nil {
		return "", 0, false
	}

	rootDomain := strings.TrimSpace(infra.Spec.PublicExposure.RootDomain)
	regionLabel := strings.TrimSpace(infra.Spec.PublicExposure.RegionID)
	if rootDomain == "" || regionLabel == "" {
		return "", 0, false
	}

	servicePort := ResolveServicePort(infra.Spec.Services.SSHGateway.Service, fallbackPort)
	if servicePort <= 0 {
		return "", 0, false
	}
	port := servicePort
	if infra.Spec.Services.SSHGateway.EndpointPort > 0 {
		port = infra.Spec.Services.SSHGateway.EndpointPort
	}

	return fmt.Sprintf("%s.ssh.%s", regionLabel, rootDomain), port, true
}

func ResolveServiceAnnotations(config *infrav1alpha1.ServiceNetworkConfig) map[string]string {
	if config == nil || len(config.Annotations) == 0 {
		return nil
	}
	return CloneStringMap(config.Annotations)
}

// ReconcileService creates or updates a service.
// For NodePort type, the port is also used as NodePort.
func (r *ResourceManager) ReconcileService(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, name string, labels map[string]string, serviceType corev1.ServiceType, annotations map[string]string, port, targetPort int32) error {
	return r.ReconcileServiceWithScope(ctx, NewObjectScope(infra), name, labels, serviceType, annotations, port, targetPort)
}

func (r *ResourceManager) ReconcileServiceWithScope(ctx context.Context, scope ObjectScope, name string, labels map[string]string, serviceType corev1.ServiceType, annotations map[string]string, port, targetPort int32) error {
	return r.ReconcileServicePortsWithScope(ctx, scope, name, labels, serviceType, annotations, []corev1.ServicePort{
		BuildServicePort("http", port, targetPort, serviceType),
	})
}

// ReconcileServicePorts creates or updates a service with multiple ports.
func (r *ResourceManager) ReconcileServicePorts(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, name string, labels map[string]string, serviceType corev1.ServiceType, annotations map[string]string, ports []corev1.ServicePort) error {
	return r.ReconcileServicePortsWithScope(ctx, NewObjectScope(infra), name, labels, serviceType, annotations, ports)
}

func (r *ResourceManager) ReconcileServicePortsWithScope(ctx context.Context, scope ObjectScope, name string, labels map[string]string, serviceType corev1.ServiceType, annotations map[string]string, ports []corev1.ServicePort) error {
	return r.reconcileServicePorts(ctx, scope, name, labels, serviceType, annotations, ports, nil)
}

func (r *ResourceManager) ReconcileServicePortsWithSpecMutator(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, name string, labels map[string]string, serviceType corev1.ServiceType, annotations map[string]string, ports []corev1.ServicePort, mutate func(*corev1.ServiceSpec)) error {
	return r.ReconcileServicePortsWithScopeAndSpecMutator(ctx, NewObjectScope(infra), name, labels, serviceType, annotations, ports, mutate)
}

func (r *ResourceManager) ReconcileServicePortsWithScopeAndSpecMutator(ctx context.Context, scope ObjectScope, name string, labels map[string]string, serviceType corev1.ServiceType, annotations map[string]string, ports []corev1.ServicePort, mutate func(*corev1.ServiceSpec)) error {
	return r.reconcileServicePorts(ctx, scope, name, labels, serviceType, annotations, ports, mutate)
}

func (r *ResourceManager) reconcileServicePorts(ctx context.Context, scope ObjectScope, name string, labels map[string]string, serviceType corev1.ServiceType, annotations map[string]string, ports []corev1.ServicePort, mutate func(*corev1.ServiceSpec)) error {
	if len(ports) == 0 {
		return fmt.Errorf("service %q requires at least one port", name)
	}

	desiredLabels := EnsureManagedLabels(labels, name)
	desiredSpec := corev1.ServiceSpec{
		Type:     serviceType,
		Selector: labels,
		Ports:    ports,
	}
	if mutate != nil {
		mutate(&desiredSpec)
	}
	desiredSvc := corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:        name,
			Namespace:   scope.Namespace,
			Labels:      desiredLabels,
			Annotations: CloneStringMap(annotations),
		},
		Spec: desiredSpec,
	}

	if err := scope.SetControllerReference(&desiredSvc, r.Scheme); err != nil {
		return err
	}

	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		svc := &corev1.Service{}
		err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: scope.Namespace}, svc)
		if errors.IsNotFound(err) {
			return r.Client.Create(ctx, desiredSvc.DeepCopy())
		}
		if err != nil {
			return err
		}

		return r.UpdateObjectIfChanged(ctx, svc, func() {
			svc.Labels = desiredLabels
			svc.Annotations = CloneStringMap(annotations)
			svc.OwnerReferences = desiredSvc.OwnerReferences
			svc.Spec.Type = desiredSvc.Spec.Type
			svc.Spec.Selector = desiredSvc.Spec.Selector
			svc.Spec.Ports = desiredSvc.Spec.Ports
			svc.Spec.ExternalTrafficPolicy = desiredSvc.Spec.ExternalTrafficPolicy
		})
	})
}

// BuildServicePort returns a ServicePort with a target port.
// For NodePort type, the port is also used as NodePort.
func BuildServicePort(name string, port, targetPort int32, serviceType corev1.ServiceType) corev1.ServicePort {
	sp := corev1.ServicePort{
		Name:       name,
		Port:       port,
		TargetPort: intstr.FromInt(int(targetPort)),
	}
	if serviceType == corev1.ServiceTypeNodePort {
		sp.NodePort = port
	}
	return sp
}

// EnsureManagedLabels merges standard managed labels without overriding existing values.
func EnsureManagedLabels(labels map[string]string, fallbackName string) map[string]string {
	out := MergeLabels(labels, map[string]string{
		"app.kubernetes.io/managed-by": "sandbox0infra-operator",
	})
	if out["app.kubernetes.io/name"] == "" {
		out["app.kubernetes.io/name"] = fallbackName
	}
	return out
}

// MergeLabels returns a copy of base with overrides applied.
func MergeLabels(base map[string]string, overrides map[string]string) map[string]string {
	out := make(map[string]string, len(base)+len(overrides))
	for key, value := range base {
		out[key] = value
	}
	for key, value := range overrides {
		out[key] = value
	}
	return out
}

func CloneStringMap(src map[string]string) map[string]string {
	if len(src) == 0 {
		return nil
	}
	out := make(map[string]string, len(src))
	for key, value := range src {
		out[key] = value
	}
	return out
}

// ReconcileIngress creates or updates an ingress.
func (r *ResourceManager) ReconcileIngress(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, serviceName string, servicePort int32, config *infrav1alpha1.IngressConfig) error {
	return r.ReconcileIngressWithScope(ctx, NewObjectScope(infra), serviceName, servicePort, config)
}

func (r *ResourceManager) ReconcileIngressWithScope(ctx context.Context, scope ObjectScope, serviceName string, servicePort int32, config *infrav1alpha1.IngressConfig) error {
	ingressName := serviceName

	ingress := &networkingv1.Ingress{}
	err := r.Client.Get(ctx, types.NamespacedName{Name: ingressName, Namespace: scope.Namespace}, ingress)
	if err != nil && !errors.IsNotFound(err) {
		return err
	}

	pathType := networkingv1.PathTypePrefix
	desiredLabels := EnsureManagedLabels(ingress.Labels, ingressName)
	desiredAnnotations := CloneStringMap(config.Annotations)
	hosts := ingressHosts(config)
	rules := make([]networkingv1.IngressRule, 0, len(hosts))
	for _, host := range hosts {
		rules = append(rules, networkingv1.IngressRule{
			Host: host,
			IngressRuleValue: networkingv1.IngressRuleValue{
				HTTP: &networkingv1.HTTPIngressRuleValue{
					Paths: []networkingv1.HTTPIngressPath{
						{
							Path:     "/",
							PathType: &pathType,
							Backend: networkingv1.IngressBackend{
								Service: &networkingv1.IngressServiceBackend{
									Name: serviceName,
									Port: networkingv1.ServiceBackendPort{
										Number: servicePort,
									},
								},
							},
						},
					},
				},
			},
		})
	}
	desiredIngress := &networkingv1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:        ingressName,
			Namespace:   scope.Namespace,
			Labels:      desiredLabels,
			Annotations: desiredAnnotations,
		},
		Spec: networkingv1.IngressSpec{
			Rules: rules,
		},
	}
	if config.ClassName != "" {
		desiredIngress.Spec.IngressClassName = &config.ClassName
	}
	desiredIngress.Spec.TLS = ingressTLS(config, hosts)

	if err := scope.SetControllerReference(desiredIngress, r.Scheme); err != nil {
		return err
	}

	if errors.IsNotFound(err) {
		return r.Client.Create(ctx, desiredIngress)
	}

	return r.UpdateObjectIfChanged(ctx, ingress, func() {
		ingress.Labels = desiredLabels
		ingress.Annotations = desiredAnnotations
		ingress.OwnerReferences = desiredIngress.OwnerReferences
		ingress.Spec = desiredIngress.Spec
	})
}

func ingressHosts(config *infrav1alpha1.IngressConfig) []string {
	seen := map[string]struct{}{}
	hosts := []string{}
	add := func(host string) {
		host = strings.ToLower(strings.TrimSpace(host))
		if host == "" {
			return
		}
		if _, ok := seen[host]; ok {
			return
		}
		seen[host] = struct{}{}
		hosts = append(hosts, host)
	}
	add(config.Host)
	for _, host := range config.ExtraHosts {
		add(host)
	}
	if len(hosts) == 0 {
		return []string{""}
	}
	return hosts
}

func ingressTLS(config *infrav1alpha1.IngressConfig, defaultHosts []string) []networkingv1.IngressTLS {
	if len(config.TLS) == 0 {
		if config.TLSSecret == "" {
			return nil
		}
		return []networkingv1.IngressTLS{
			{
				Hosts:      defaultHosts,
				SecretName: config.TLSSecret,
			},
		}
	}

	tls := make([]networkingv1.IngressTLS, 0, len(config.TLS))
	for _, entry := range config.TLS {
		secretName := strings.TrimSpace(entry.SecretName)
		if secretName == "" {
			continue
		}
		hosts := normalizeHostList(entry.Hosts)
		if len(hosts) == 0 {
			hosts = defaultHosts
		}
		tls = append(tls, networkingv1.IngressTLS{
			Hosts:      hosts,
			SecretName: secretName,
		})
	}
	return tls
}

func normalizeHostList(values []string) []string {
	seen := map[string]struct{}{}
	hosts := []string{}
	for _, value := range values {
		host := strings.ToLower(strings.TrimSpace(value))
		if host == "" {
			continue
		}
		if _, ok := seen[host]; ok {
			continue
		}
		seen[host] = struct{}{}
		hosts = append(hosts, host)
	}
	return hosts
}

// ReconcileServiceConfigMap creates or updates a configmap for a service.
func (r *ResourceManager) ReconcileServiceConfigMap(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, name string, labels map[string]string, config any) error {
	return r.ReconcileServiceConfigMapWithScope(ctx, NewObjectScope(infra), name, labels, config)
}

func (r *ResourceManager) ReconcileServiceConfigMapWithScope(ctx context.Context, scope ObjectScope, name string, labels map[string]string, config any) error {
	if config == nil {
		config = map[string]any{}
	}

	payload, err := yamlv3.Marshal(config)
	if err != nil {
		return fmt.Errorf("marshal config for %s: %w", name, err)
	}

	desiredLabels := EnsureManagedLabels(labels, name)
	desired := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: scope.Namespace,
			Labels:    desiredLabels,
		},
		Data: map[string]string{
			"config.yaml": string(payload),
		},
	}

	if err := scope.SetControllerReference(desired, r.Scheme); err != nil {
		return err
	}

	existing := &corev1.ConfigMap{}
	err = r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: scope.Namespace}, existing)
	if err != nil && !errors.IsNotFound(err) {
		return err
	}

	if errors.IsNotFound(err) {
		return r.Client.Create(ctx, desired)
	}

	return r.UpdateObjectIfChanged(ctx, existing, func() {
		existing.Data = desired.Data
		existing.Labels = desired.Labels
		existing.OwnerReferences = desired.OwnerReferences
	})
}

func (r *ResourceManager) ReconcileHashedServiceConfigMap(ctx context.Context, infra *infrav1alpha1.Sandbox0Infra, name string, labels map[string]string, config any) (ServiceConfigRef, error) {
	return r.ReconcileHashedServiceConfigMapWithScope(ctx, NewObjectScope(infra), name, labels, config)
}

// ReconcileHashedServiceConfigMapWithScope creates an immutable ConfigMap whose
// name includes the config hash. Service pods mount config.yaml with subPath, so
// the pod spec must reference the exact ConfigMap object for the desired config.
func (r *ResourceManager) ReconcileHashedServiceConfigMapWithScope(ctx context.Context, scope ObjectScope, name string, labels map[string]string, config any) (ServiceConfigRef, error) {
	if config == nil {
		config = map[string]any{}
	}

	payload, err := yamlv3.Marshal(config)
	if err != nil {
		return ServiceConfigRef{}, fmt.Errorf("marshal config for %s: %w", name, err)
	}
	hash := ConfigHashFromPayload(payload)
	configMapName := HashedServiceConfigMapName(name, hash)

	desiredLabels := EnsureManagedLabels(labels, name)
	desired := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      configMapName,
			Namespace: scope.Namespace,
			Labels:    desiredLabels,
			Annotations: map[string]string{
				ServiceConfigBaseNameAnnotation: name,
				ServiceConfigHashAnnotation:     hash,
			},
		},
		Immutable: BoolPtr(true),
		Data: map[string]string{
			"config.yaml": string(payload),
		},
	}

	if err := scope.SetControllerReference(desired, r.Scheme); err != nil {
		return ServiceConfigRef{}, err
	}

	existing := &corev1.ConfigMap{}
	err = r.Client.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: scope.Namespace}, existing)
	if err != nil && !errors.IsNotFound(err) {
		return ServiceConfigRef{}, err
	}

	if errors.IsNotFound(err) {
		if err := r.Client.Create(ctx, desired); err != nil {
			return ServiceConfigRef{}, err
		}
	} else {
		if existing.Data["config.yaml"] != desired.Data["config.yaml"] {
			return ServiceConfigRef{}, fmt.Errorf("service configmap %q already exists with mismatched config hash", configMapName)
		}
		if err := r.UpdateObjectIfChanged(ctx, existing, func() {
			existing.Labels = desired.Labels
			existing.Annotations = desired.Annotations
			existing.OwnerReferences = desired.OwnerReferences
			if existing.Immutable == nil || !*existing.Immutable {
				existing.Immutable = BoolPtr(true)
			}
		}); err != nil {
			return ServiceConfigRef{}, err
		}
	}

	ref := ServiceConfigRef{ConfigMapName: configMapName, Hash: hash}
	if err := r.cleanupUnusedServiceConfigMaps(ctx, scope, name, ref.ConfigMapName); err != nil {
		return ServiceConfigRef{}, err
	}
	return ref, nil
}

func (r *ResourceManager) cleanupUnusedServiceConfigMaps(ctx context.Context, scope ObjectScope, baseName, currentName string) error {
	var pods corev1.PodList
	if err := r.Client.List(ctx, &pods, client.InNamespace(scope.Namespace)); err != nil {
		return err
	}
	inUse := make(map[string]struct{})
	for _, pod := range pods.Items {
		for _, volume := range pod.Spec.Volumes {
			if volume.ConfigMap == nil {
				continue
			}
			inUse[volume.ConfigMap.Name] = struct{}{}
		}
	}

	var configMaps corev1.ConfigMapList
	if err := r.Client.List(ctx, &configMaps, client.InNamespace(scope.Namespace)); err != nil {
		return err
	}
	for _, configMap := range configMaps.Items {
		if configMap.Name == currentName {
			continue
		}
		if configMap.Annotations[ServiceConfigBaseNameAnnotation] != baseName {
			continue
		}
		if _, ok := inUse[configMap.Name]; ok {
			continue
		}
		cm := configMap.DeepCopy()
		if err := r.Client.Delete(ctx, cm); err != nil && !errors.IsNotFound(err) {
			return err
		}
	}
	return nil
}

// ReconcileNamespace creates a namespace if it does not exist.
// It ignores errors if the operator does not have permission to create the namespace.
func (r *ResourceManager) ReconcileNamespace(ctx context.Context, name string) error {
	ns := &corev1.Namespace{}
	err := r.Client.Get(ctx, types.NamespacedName{Name: name}, ns)
	if err == nil {
		return nil
	}

	if !errors.IsNotFound(err) {
		return err
	}

	desired := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}

	err = r.Client.Create(ctx, desired)
	if err != nil {
		if errors.IsForbidden(err) {
			log.FromContext(ctx).Info("Forbidden to create namespace, skipping", "namespace", name)
			return nil
		}
		return err
	}

	return nil
}

func GetOrInitMap(config map[string]any, key string) map[string]any {
	if val, ok := config[key]; ok {
		if typed, ok := val.(map[string]any); ok {
			return typed
		}
	}

	child := map[string]any{}
	config[key] = child
	return child
}

// GenerateRandomString generates a random string of specified length.
func GenerateRandomString(length int) string {
	bytes := make([]byte, length)
	if _, err := rand.Read(bytes); err != nil {
		return "defaultsecret123456789012"
	}

	encoded := base64.URLEncoding.EncodeToString(bytes)
	if len(encoded) > length {
		return encoded[:length]
	}
	return encoded
}

// EnsureSecretValue ensures a named secret contains a key, generating if needed.
func EnsureSecretValue(ctx context.Context, client client.Client, scheme *runtime.Scheme, infra *infrav1alpha1.Sandbox0Infra, name, key string, length int) (string, error) {
	return EnsureSecretValueWithScope(ctx, client, scheme, NewObjectScope(infra), name, key, length)
}

func EnsureSecretValueWithScope(ctx context.Context, client client.Client, scheme *runtime.Scheme, scope ObjectScope, name, key string, length int) (string, error) {
	secret := &corev1.Secret{}
	err := client.Get(ctx, types.NamespacedName{Name: name, Namespace: scope.Namespace}, secret)
	if err != nil && !errors.IsNotFound(err) {
		return "", err
	}

	if errors.IsNotFound(err) {
		value := GenerateRandomString(length)
		secret = &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name,
				Namespace: scope.Namespace,
			},
			Type: corev1.SecretTypeOpaque,
			StringData: map[string]string{
				key: value,
			},
		}
		if err := scope.SetControllerReference(secret, scheme); err != nil {
			return "", err
		}
		if err := client.Create(ctx, secret); err != nil {
			return "", err
		}
		return value, nil
	}

	if value, ok := secret.Data[key]; ok && len(value) > 0 {
		return string(value), nil
	}

	value := GenerateRandomString(length)
	if secret.Data == nil {
		secret.Data = map[string][]byte{}
	}
	secret.Data[key] = []byte(value)
	if err := client.Update(ctx, secret); err != nil {
		return "", err
	}
	return value, nil
}

// EnsureEd25519KeyPair ensures a named secret contains an Ed25519 keypair.
func EnsureEd25519KeyPair(ctx context.Context, client client.Client, scheme *runtime.Scheme, infra *infrav1alpha1.Sandbox0Infra, name, privateKeyKey, publicKeyKey string) (string, string, error) {
	return EnsureEd25519KeyPairWithScope(ctx, client, scheme, NewObjectScope(infra), name, privateKeyKey, publicKeyKey)
}

func EnsureEd25519KeyPairWithScope(ctx context.Context, client client.Client, scheme *runtime.Scheme, scope ObjectScope, name, privateKeyKey, publicKeyKey string) (string, string, error) {
	secret := &corev1.Secret{}
	err := client.Get(ctx, types.NamespacedName{Name: name, Namespace: scope.Namespace}, secret)
	if err != nil && !errors.IsNotFound(err) {
		return "", "", err
	}

	generatePair := func() (string, string, error) {
		privateKeyPEM, publicKeyPEM, err := internalauth.GenerateEd25519KeyPair()
		if err != nil {
			return "", "", err
		}
		return string(privateKeyPEM), string(publicKeyPEM), nil
	}

	if errors.IsNotFound(err) {
		privateKeyPEM, publicKeyPEM, err := generatePair()
		if err != nil {
			return "", "", err
		}
		secret = &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name,
				Namespace: scope.Namespace,
			},
			Type: corev1.SecretTypeOpaque,
			StringData: map[string]string{
				privateKeyKey: privateKeyPEM,
				publicKeyKey:  publicKeyPEM,
			},
		}
		if err := scope.SetControllerReference(secret, scheme); err != nil {
			return "", "", err
		}
		if err := client.Create(ctx, secret); err != nil {
			return "", "", err
		}
		return privateKeyPEM, publicKeyPEM, nil
	}

	privateKeyPEM, privateKeyOK := secret.Data[privateKeyKey]
	publicKeyPEM, publicKeyOK := secret.Data[publicKeyKey]
	if privateKeyOK && len(privateKeyPEM) > 0 && publicKeyOK && len(publicKeyPEM) > 0 {
		return string(privateKeyPEM), string(publicKeyPEM), nil
	}

	regeneratedPrivate, regeneratedPublic, err := generatePair()
	if err != nil {
		return "", "", err
	}
	if secret.Data == nil {
		secret.Data = map[string][]byte{}
	}
	secret.Data[privateKeyKey] = []byte(regeneratedPrivate)
	secret.Data[publicKeyKey] = []byte(regeneratedPublic)
	if err := client.Update(ctx, secret); err != nil {
		return "", "", err
	}
	return regeneratedPrivate, regeneratedPublic, nil
}

// GetSecretValue returns the value from a secret key reference.
func GetSecretValue(ctx context.Context, client client.Client, namespace string, ref infrav1alpha1.SecretKeyRef) (string, error) {
	if ref.Name == "" {
		return "", fmt.Errorf("secret name is required")
	}

	secret := &corev1.Secret{}
	if err := client.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: namespace}, secret); err != nil {
		return "", err
	}

	key := ref.Key
	if key == "" {
		key = "password"
	}

	value, ok := secret.Data[key]
	if !ok {
		return "", fmt.Errorf("key %s not found in secret %s", key, ref.Name)
	}

	return string(value), nil
}

// ResolveSecretKeyRef returns a ref with defaults applied when fields are empty.
func ResolveSecretKeyRef(ref infrav1alpha1.SecretKeyRef, defaultName, defaultKey string) infrav1alpha1.SecretKeyRef {
	if ref.Name == "" {
		ref.Name = defaultName
	}
	if ref.Key == "" {
		ref.Key = defaultKey
	}
	return ref
}

// GetServiceLabels returns standard labels for a service.
func GetServiceLabels(instanceName, componentName string) map[string]string {
	return map[string]string{
		"app.kubernetes.io/name":       componentName,
		"app.kubernetes.io/instance":   instanceName,
		"app.kubernetes.io/component":  componentName,
		"app.kubernetes.io/managed-by": "sandbox0infra-operator",
	}
}

// BoolPtr returns a pointer to a bool.
func BoolPtr(b bool) *bool {
	return &b
}

// ResolveSandboxNodePlacement derives the shared scheduling constraints for
// sandbox workloads and node-local sandbox services. The shared spec-level
// placement takes precedence, with netd placement kept as a compatibility
// fallback for older manifests.
func ResolveSandboxNodePlacement(infra *infrav1alpha1.Sandbox0Infra) (map[string]string, []corev1.Toleration) {
	if infra == nil {
		return nil, nil
	}

	var nodeSelector map[string]string
	if infra.Spec.SandboxNodePlacement != nil && len(infra.Spec.SandboxNodePlacement.NodeSelector) > 0 {
		nodeSelector = cloneNodeSelector(infra.Spec.SandboxNodePlacement.NodeSelector)
	} else if infra.Spec.Services != nil && infra.Spec.Services.Netd != nil {
		nodeSelector = cloneNodeSelector(infra.Spec.Services.Netd.NodeSelector)
	}

	var tolerations []corev1.Toleration
	if infra.Spec.SandboxNodePlacement != nil && len(infra.Spec.SandboxNodePlacement.Tolerations) > 0 {
		tolerations = cloneTolerations(infra.Spec.SandboxNodePlacement.Tolerations)
	} else if infra.Spec.Services != nil && infra.Spec.Services.Netd != nil {
		tolerations = cloneTolerations(infra.Spec.Services.Netd.Tolerations)
	}

	return nodeSelector, tolerations
}

func cloneNodeSelector(src map[string]string) map[string]string {
	if len(src) == 0 {
		return nil
	}

	cloned := make(map[string]string, len(src))
	for key, value := range src {
		cloned[key] = value
	}
	return cloned
}

func cloneTolerations(src []corev1.Toleration) []corev1.Toleration {
	if len(src) == 0 {
		return nil
	}

	cloned := make([]corev1.Toleration, len(src))
	copy(cloned, src)
	return cloned
}

func ConfigHash(config any) (string, error) {
	if config == nil {
		config = map[string]any{}
	}

	payload, err := yamlv3.Marshal(config)
	if err != nil {
		return "", err
	}
	return ConfigHashFromPayload(payload), nil
}

func ConfigHashFromPayload(payload []byte) string {
	sum := sha256.Sum256(payload)
	return hex.EncodeToString(sum[:])
}

func ConfigHashAnnotation(config any) (map[string]string, error) {
	hash, err := ConfigHash(config)
	if err != nil {
		return nil, err
	}
	return ConfigHashAnnotationFromHash(hash), nil
}

func ConfigHashAnnotationFromHash(hash string) map[string]string {
	return map[string]string{
		PodTemplateConfigHashAnnotation: hash,
	}
}

func HashedServiceConfigMapName(baseName, hash string) string {
	if len(hash) > serviceConfigNameHashLength {
		hash = hash[:serviceConfigNameHashLength]
	}
	suffix := "-config-" + hash
	if len(baseName)+len(suffix) <= 253 {
		return baseName + suffix
	}
	maxBaseLen := 253 - len(suffix)
	if maxBaseLen < 1 {
		maxBaseLen = 1
	}
	return strings.TrimRight(baseName[:maxBaseLen], "-.") + suffix
}

// EnsurePodTemplateAnnotations returns a cloned annotations map for pod templates.
func EnsurePodTemplateAnnotations(annotations map[string]string) map[string]string {
	out := map[string]string{}
	for key, value := range annotations {
		out[key] = value
	}
	return out
}
