// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// 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 installation

import (
	"context"

	v3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
	appsv1 "k8s.io/api/apps/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"

	operatorv1 "github.com/projectcalico/calico/operator/api/v1"
	"github.com/projectcalico/calico/operator/pkg/common"
	"github.com/projectcalico/calico/operator/pkg/controller/managedfields"
	"github.com/projectcalico/calico/operator/pkg/render"
)

// A field manager owns its declared fields as a set. A write site that declares a field on a
// different schedule needs its own manager, because dropping a field deletes it.
const (
	// installationFieldManager owns the shared config fields defaulted from the Installation.
	installationFieldManager = "installation"

	// bpfFieldManager owns spec.bpfEnabled, which both installation write sites declare.
	bpfFieldManager = "installation-bpf"

	// openShiftFelixHealthPort avoids the port OpenShift's SDN already holds.
	openShiftFelixHealthPort = 9199
)

// declareFelixConfiguration declares the fields defaulted from the Installation spec. It declares
// every one every time, so a field the spec stops asking for is declared without a value, which
// clears whatever the operator wrote there.
func (r *ReconcileInstallation) declareFelixConfiguration(ctx context.Context, install *operatorv1.Installation, needNsMigration bool) managedfields.DeclareFn[*v3.FelixConfiguration] {
	return func(current *v3.FelixConfiguration) (*managedfields.Declaration[*v3.FelixConfiguration], error) {
		felixConfig := &v3.FelixConfiguration{}
		d := &managedfields.Declaration[*v3.FelixConfiguration]{
			Manager: installationFieldManager,
			Owned:   felixConfig,

			// Defer leaves a user's value alone, Override takes the field back. Fields the
			// operator only defaults get Defer; modes it has to keep consistent with what it
			// renders get Override.
			Policies: map[string]managedfields.ConflictPolicy{
				"spec.routeTableRange":         managedfields.ConflictDefer,
				"spec.healthPort":              managedfields.ConflictDefer,
				"spec.vxlanVNI":                managedfields.ConflictDefer,
				"spec.vxlanPort":               managedfields.ConflictDefer,
				"spec.bpfHostConntrackBypass":  managedfields.ConflictDefer,
				"spec.bpfKubeProxyHealthzPort": managedfields.ConflictDefer,
				"spec.nftablesMode":            managedfields.ConflictOverride,
				"spec.programClusterRoutes":    managedfields.ConflictOverride,
			},
		}
		owned := &felixConfig.Spec

		// Keep calico-node's route tables clear of the ones the CNI plugin uses.
		switch install.Spec.CNI.Type {
		case operatorv1.PluginAmazonVPC:
			// AWS uses the ENI device number + 1, and the VLAN table ID + 100.
			owned.RouteTableRange = &v3.RouteTableRange{Min: 65, Max: 99}
		case operatorv1.PluginGKE:
			owned.RouteTableRange = &v3.RouteTableRange{Min: 10, Max: 250}
		}

		owned.HealthPort = ptr.To(defaultFelixHealthPort(install))

		vxlanVNI, vxlanPort := 4096, 4789
		if install.Spec.KubernetesProvider == operatorv1.ProviderDockerEE {
			// MKE's docker swarm VXLAN uses 4096/4789, and the clash deletes vxlan.calico.
			// MKE's docs recommend 10000.
			vxlanVNI = 10000
			if install.Spec.BPFEnabled() {
				// The eBPF dataplane's flow-based VXLAN device clashes with the host's VXLAN interface.
				vxlanPort = 8472

				// The eBPF dataplane only works with MKE when conntrack bypass is off.
				owned.BPFHostConntrackBypass = ptr.To(false)
			}
		}
		owned.VXLANVNI = &vxlanVNI
		owned.VXLANPort = &vxlanPort

		if install.Spec.BPFEnabled() && !install.Spec.KubeProxyManagementEnabled() {
			// The platform's kube-proxy holds 10256, so Felix's healthz server would fail to
			// bind. Port 0 only validates on newer nodes, so hold the first write until all of
			// them accept it.
			nodeDSExists, err := r.nodeDaemonSetExists(ctx)
			if err != nil {
				return nil, err
			}
			alreadySet := current.Spec.BPFKubeProxyHealthzPort != nil && *current.Spec.BPFKubeProxyHealthzPort == 0
			if alreadySet || allNodesRunTargetVersion(install, needNsMigration, nodeDSExists, r.ext.ProductVersion(&install.Spec)) {
				owned.BPFKubeProxyHealthzPort = ptr.To(0)
			}
		}

		if install.Spec.CalicoNetwork != nil && install.Spec.CalicoNetwork.LinuxDataplane != nil {
			owned.NFTablesMode = ptr.To(nftablesMode(install))
		}

		// Gated on the field being set, so leaving it unset keeps meaning "whatever Calico
		// defaults to" rather than pinning today's default into the datastore.
		if install.Spec.CalicoNetwork != nil && install.Spec.CalicoNetwork.ClusterRoutingMode != nil {
			mode := *install.Spec.CalicoNetwork.ClusterRoutingMode
			owned.ProgramClusterRoutes = ptr.To(felixProgramClusterRoutesValue(mode))
		}

		extPaths, err := r.ext.DeclareFelixConfiguration(&install.Spec, current, felixConfig)
		if err != nil {
			return nil, err
		}
		for _, path := range extPaths {
			d.Policies[path] = managedfields.ConflictOverride
		}

		return d, nil
	}
}

// nodeDaemonSetExists reports whether calico-node has been rendered yet.
func (r *ReconcileInstallation) nodeDaemonSetExists(ctx context.Context) (bool, error) {
	ds := &appsv1.DaemonSet{}
	err := r.client.Get(ctx, types.NamespacedName{Namespace: common.CalicoNamespace, Name: common.NodeDaemonSetName}, ds)
	if apierrors.IsNotFound(err) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

// defaultFelixHealthPort is the port the operator defaults Felix's health server to. It has to
// match what render falls back to, or calico-node's probe targets a port Felix is not listening on.
func defaultFelixHealthPort(install *operatorv1.Installation) int {
	if install.Spec.KubernetesProvider.IsOpenShift() {
		return openShiftFelixHealthPort
	}
	return render.DefaultFelixHealthPort
}

// nftablesMode is the dataplane mode Felix should run in. The operator has always owned it,
// so nothing older needs preserving.
func nftablesMode(install *operatorv1.Installation) v3.NFTablesMode {
	if !install.Spec.IsNftables() {
		return v3.NFTablesModeDisabled
	}
	if install.Spec.BPFEnabled() {
		// BPF mode replaces kube-proxy, so nftables needs no compatibility with its mode.
		return v3.NFTablesModeEnabled
	}

	// kube-proxy is running, so let Felix pick per node and keep upgrades smooth.
	return v3.NFTablesModeAuto
}

// declareBPFEnabled declares spec.bpfEnabled. Both installation write sites use it so the field
// stays under one manager with the same value.
func (r *ReconcileInstallation) declareBPFEnabled(ctx context.Context, install *operatorv1.Installation, needNsMigration bool) managedfields.DeclareFn[*v3.FelixConfiguration] {
	return func(current *v3.FelixConfiguration) (*managedfields.Declaration[*v3.FelixConfiguration], error) {
		enabled, err := r.bpfEnabledValue(ctx, install, current, needNsMigration)
		if err != nil || enabled == nil {
			return nil, err
		}
		return &managedfields.Declaration[*v3.FelixConfiguration]{
			Manager: bpfFieldManager,
			Owned: &v3.FelixConfiguration{
				Spec: v3.FelixConfigurationSpec{BPFEnabled: enabled},
			},
			Policies: map[string]managedfields.ConflictPolicy{
				// A user who changed this by hand gets a degraded status, not an override.
				"spec.bpfEnabled": managedfields.ConflictError,
			},
		}, nil
	}
}

// bpfEnabledValue resolves the dataplane Felix should run, or nil to leave the field alone.
// Turning eBPF on waits for the calico-node rollout to mount the BPF volumes.
func (r *ReconcileInstallation) bpfEnabledValue(ctx context.Context, install *operatorv1.Installation, current *v3.FelixConfiguration, needNsMigration bool) (*bool, error) {
	ds := &appsv1.DaemonSet{}
	err := r.client.Get(ctx, types.NamespacedName{Namespace: common.CalicoNamespace, Name: common.NodeDaemonSetName}, ds)
	if apierrors.IsNotFound(err) {
		if needNsMigration {
			// calico-node still serves nodes out of kube-system. Claiming the field here either
			// fights a value the user set or restarts the dataplane mid-migration.
			return nil, nil
		}

		// A fresh install has no calico-node rollout to wait for.
		return ptr.To(install.Spec.BPFEnabled()), nil
	}
	if err != nil {
		return nil, err
	}
	if !install.Spec.BPFEnabled() {
		return ptr.To(false), nil
	}

	// Operators before the FelixConfiguration field enabled eBPF through a calico-node env var.
	envVarEnabled, err := bpfEnabledOnDaemonsetWithEnvVar(ds)
	if err != nil {
		return nil, err
	}
	if envVarEnabled || isRolloutCompleteWithBPFVolumes(ds) {
		return ptr.To(true), nil
	}
	return ptr.To(bpfEnabledOnFelixConfig(current)), nil
}
