// Copyright Istio Authors
//
// 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 controller

import (
	"context"
	"strings"
	"sync"

	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"istio.io/api/annotation"
	"istio.io/istio/pilot/pkg/config/kube/clustertrustbundle"
	"istio.io/istio/pilot/pkg/config/kube/crdclient"
	"istio.io/istio/pilot/pkg/features"
	"istio.io/istio/pilot/pkg/keycertbundle"
	"istio.io/istio/pilot/pkg/leaderelection"
	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pilot/pkg/server"
	"istio.io/istio/pilot/pkg/serviceregistry/aggregate"
	"istio.io/istio/pilot/pkg/serviceregistry/provider"
	"istio.io/istio/pilot/pkg/serviceregistry/serviceentry"
	"istio.io/istio/pkg/backoff"
	"istio.io/istio/pkg/config/mesh/kubemesh"
	"istio.io/istio/pkg/config/mesh/meshwatcher"
	"istio.io/istio/pkg/config/schema/collection"
	"istio.io/istio/pkg/config/schema/collections"
	kubelib "istio.io/istio/pkg/kube"
	"istio.io/istio/pkg/kube/krt"
	"istio.io/istio/pkg/kube/multicluster"
	"istio.io/istio/pkg/util/sets"
	"istio.io/istio/pkg/webhooks"
)

const (
	// Name of the webhook config in the config - no need to change it.
	webhookName = "sidecar-injector.istio.io"
)

type kubeController struct {
	MeshServiceController *aggregate.Controller
	*Controller
	workloadEntryController *serviceentry.Controller
	stop                    chan struct{}

	// onSynced, if set, runs exactly once the first time HasSynced observes the embedded
	// registry has synced - before HasSynced reports true to the caller. This is used during a
	// credential-rotation swap to atomically replace the aggregate controller's registry for this
	// cluster before Component's pendingSwap sees this kubeController as synced and closes the
	// old one; otherwise the old registry could still be "current" when Close() runs and its EDS
	// shard would be wiped out from under the new registry.
	onSynced     func()
	onSyncedOnce sync.Once
}

// HasSynced reports whether the embedded registry has completed its initial sync. If onSynced is
// set, it runs synchronously on the first true observation, before this method returns - see the
// onSynced field comment for why the ordering matters.
func (k *kubeController) HasSynced() bool {
	synced := k.Controller.HasSynced()
	if synced && k.onSynced != nil {
		k.onSyncedOnce.Do(k.onSynced)
	}
	return synced
}

func (k *kubeController) Close() {
	close(k.stop)
	clusterID := k.Controller.clusterID
	k.MeshServiceController.UnRegisterHandlersForCluster(clusterID)
	// DeleteRegistryIfCurrent avoids deleting a registry that HasSynced's UpdateRegistry call
	// above already swapped in to replace this one. If it was already superseded, our EDS shard
	// key now belongs to that new registry, so Cleanup must not remove it.
	current := k.MeshServiceController.DeleteRegistryIfCurrent(k.Controller)
	if k.workloadEntryController != nil {
		k.MeshServiceController.DeleteRegistry(clusterID, provider.External)
	}
	if err := k.Controller.Cleanup(current); err != nil {
		log.Warnf("failed cleaning up services in %s: %v", clusterID, err)
	}
	if k.opts.XDSUpdater != nil {
		k.opts.XDSUpdater.ConfigUpdate(&model.PushRequest{Full: true, Reason: model.NewReasonStats(model.ClusterUpdate), Forced: true})
	}
}

// liveServiceHosts returns the hostname/namespace pairs registry currently knows about, for use
// as the "keep" set passed to XDSUpdater.PruneShard once registry's initial sync completes.
func liveServiceHosts(registry *Controller) map[string]sets.String {
	svcs := registry.Services()
	keep := make(map[string]sets.String, len(svcs))
	for _, svc := range svcs {
		hostname := string(svc.Hostname)
		if keep[hostname] == nil {
			keep[hostname] = sets.New[string]()
		}
		keep[hostname].Insert(svc.Attributes.Namespace)
	}
	return keep
}

// Multicluster structure holds the remote kube Controllers and multicluster specific attributes.
type Multicluster struct {
	// serverID of this pilot instance used for leader election
	serverID string

	// options to use when creating kube controllers
	opts Options

	s server.Instance

	serviceEntryController *serviceentry.Controller

	clusterLocal model.ClusterLocalProvider

	distributeCACert bool
	caBundleWatcher  *keycertbundle.Watcher
	revision         string

	component *multicluster.Component[*kubeController]
}

// NewMulticluster initializes data structure to store multicluster information
func NewMulticluster(
	serverID string,
	opts Options,
	serviceEntryController *serviceentry.Controller,
	caBundleWatcher *keycertbundle.Watcher,
	revision string,
	distributeCACert bool,
	clusterLocal model.ClusterLocalProvider,
	s server.Instance,
	controller *multicluster.Controller,
) *Multicluster {
	mc := &Multicluster{
		serverID:               serverID,
		opts:                   opts,
		serviceEntryController: serviceEntryController,
		distributeCACert:       distributeCACert,
		caBundleWatcher:        caBundleWatcher,
		revision:               revision,
		clusterLocal:           clusterLocal,
		s:                      s,
	}
	mc.component = multicluster.BuildMultiClusterComponent(controller, func(cluster *multicluster.Cluster) *kubeController {
		stop := make(chan struct{})
		client := cluster.Client
		configCluster := opts.ClusterID == cluster.ID

		options := opts
		options.ClusterID = cluster.ID
		options.MultiClusterController = controller
		if !configCluster {
			options.SyncTimeout = features.RemoteClusterTimeout
		}
		log.Infof("Initializing Kubernetes service registry %q", options.ClusterID)
		options.ConfigCluster = configCluster

		// Create per-cluster mesh watcher for remote clusters
		// This allows controllers to detect cluster-specific settings that may be relevant for the local proxies, e.g. trust domain.
		if !configCluster {
			// Save the config cluster's mesh watcher as fallback for when the remote meshconfig is unreadable
			// (e.g. during upgrades before RBAC rules are applied to the remote cluster).
			options.ConfigClusterMeshWatcher = opts.MeshWatcher
			meshConfigMapName := mc.getMeshConfigMapName()
			meshSource := kubemesh.NewConfigMapSource(
				client,
				options.SystemNamespace,
				meshConfigMapName,
				kubemesh.MeshConfigKey,
				krt.NewOptionsBuilder(stop, "", nil),
			)
			remoteMeshCollection := meshwatcher.NewCollection(
				krt.NewOptionsBuilder(stop, "", nil),
				meshSource,
			)
			options.MeshWatcher = meshwatcher.ConfigAdapter(remoteMeshCollection)
			log.Infof("Created mesh watcher for remote cluster %q", cluster.ID)
		}

		kubeRegistry := NewController(client, options)

		// When the remote meshconfig changes (e.g. trust domain becomes readable after RBAC is applied),
		// trigger a full push so services and endpoints are rebuilt with the correct trust domain.
		if !configCluster && options.XDSUpdater != nil {
			options.MeshWatcher.AddMeshHandler(func() {
				kubeRegistry.opts.XDSUpdater.ConfigUpdate(&model.PushRequest{
					Full:   true,
					Reason: model.NewReasonStats(model.GlobalUpdate),
					Forced: true,
				})
			})
		}
		kubeController := &kubeController{
			MeshServiceController: opts.MeshServiceController,
			Controller:            kubeRegistry,
			stop:                  stop,
		}
		mc.initializeCluster(cluster, kubeController, kubeRegistry, options, configCluster, stop)
		return kubeController
	})

	return mc
}

// getMeshConfigMapName returns the mesh ConfigMap name based on the revision
func (m *Multicluster) getMeshConfigMapName() string {
	name := "istio"
	if m.revision == "" || m.revision == "default" {
		return name
	}
	return name + "-" + m.revision
}

// initializeCluster initializes the cluster by setting various handlers.
func (m *Multicluster) initializeCluster(cluster *multicluster.Cluster, kubeController *kubeController, kubeRegistry *Controller,
	options Options, configCluster bool, clusterStopCh <-chan struct{},
) {
	client := cluster.Client

	if m.serviceEntryController != nil && features.EnableServiceEntrySelectPods {
		// Add an instance handler in the kubernetes registry to notify service entry store about pod events
		kubeRegistry.AppendWorkloadHandler(m.serviceEntryController.WorkloadInstanceHandler)
	}

	// TODO implement deduping in aggregate registry to allow multiple k8s registries to handle WorkloadEntry
	if features.EnableK8SServiceSelectWorkloadEntries {
		if m.serviceEntryController != nil && configCluster {
			// Add an instance handler in the service entry store to notify kubernetes about workload entry events
			m.serviceEntryController.AppendWorkloadHandler(kubeRegistry.WorkloadInstanceHandler)
		} else if features.WorkloadEntryCrossCluster {
			// TODO only do this for non-remotes, can't guarantee CRDs in remotes (depends on https://github.com/istio/istio/pull/29824)
			configStore := createWleConfigStore(client, m.revision, options)
			kubeController.workloadEntryController = serviceentry.NewWorkloadEntryController(
				configStore, options.XDSUpdater,
				options.MultiClusterController,
				m.opts.MeshWatcher,
				serviceentry.WithClusterID(cluster.ID),
				serviceentry.WithNetworkIDCb(kubeRegistry.Network),
				serviceentry.WithKRTDebugger(m.opts.KrtDebugger))
			// Services can select WorkloadEntry from the same cluster. We only duplicate the Service to configure kube-dns.
			kubeController.workloadEntryController.AppendWorkloadHandler(kubeRegistry.WorkloadInstanceHandler)
			// ServiceEntry selects WorkloadEntry from remote cluster
			kubeController.workloadEntryController.AppendWorkloadHandler(m.serviceEntryController.WorkloadInstanceHandler)
			m.opts.MeshServiceController.AddRegistryAndRun(kubeController.workloadEntryController, clusterStopCh)
			go configStore.Run(clusterStopCh)
		}
	}

	// run after WorkloadHandler is added
	if cluster.Action == multicluster.Update {
		// Start the new registry here. The registry is swapped in only once this kubeController
		// reports synced, so the old registry keeps serving until then.
		//
		// This swap runs from kubeController.HasSynced rather than cluster.SyncedCh: the
		// multicluster Component's pendingSwap also polls HasSynced to decide when the old
		// kubeController is superseded and closes it, and that poll can observe synced before
		// cluster.SyncedCh fires. If the old registry were closed first, it would still be
		// "current" in the aggregate controller and its EDS shard would be wiped out from under
		// this new registry. Running the swap inside HasSynced guarantees it completes before
		// HasSynced can report true to that poll.
		kubeController.onSynced = func() {
			log.Infof("cluster %s synced, replacing registry", cluster.ID)
			m.opts.MeshServiceController.UpdateRegistry(kubeRegistry, clusterStopCh)
			// The old registry (being replaced) may have written shard entries for services
			// that no longer exist in the remote cluster - e.g. deleted in the same window the
			// old registry's watch stopped observing changes. Those services never appear in
			// kubeRegistry's own sync, so nothing else would ever clean up their stale shard
			// entries. Prune anything under this shard key that kubeRegistry didn't just
			// reaffirm.
			if kubeRegistry.opts.XDSUpdater != nil {
				kubeRegistry.opts.XDSUpdater.PruneShard(model.ShardKeyFromRegistry(kubeRegistry), liveServiceHosts(kubeRegistry))
			}
		}
		go kubeRegistry.Run(clusterStopCh)
	} else {
		// For adds, register immediately
		m.opts.MeshServiceController.AddRegistryAndRun(kubeRegistry, clusterStopCh)
	}

	go func() {
		var shouldLead bool
		if !configCluster {
			shouldLead = m.checkShouldLead(client, options.SystemNamespace, clusterStopCh)
			log.Infof("should join leader-election for cluster %s: %t", cluster.ID, shouldLead)
		}

		if m.distributeCACert && (shouldLead || configCluster) {
			if features.EnableClusterTrustBundles {
				// Block server exit on graceful termination of the leader controller.
				m.s.RunComponentAsyncAndWait("clustertrustbundle controller", func(_ <-chan struct{}) error {
					log.Infof("joining leader-election for %s in %s on cluster %s",
						leaderelection.ClusterTrustBundleController, options.SystemNamespace, options.ClusterID)
					election := leaderelection.
						NewLeaderElectionMulticluster(options.SystemNamespace, m.serverID, leaderelection.NamespaceController, m.revision, !configCluster, client).
						AddRunFunction(func(leaderStop <-chan struct{}) {
							log.Infof("starting clustertrustbundle controller for cluster %s", cluster.ID)
							c := clustertrustbundle.NewController(client, m.caBundleWatcher)
							client.RunAndWait(clusterStopCh)
							c.Run(leaderStop)
						})
					election.Run(clusterStopCh)
					return nil
				})
			} else {
				// Block server exit on graceful termination of the leader controller.
				m.s.RunComponentAsyncAndWait("namespace controller", func(_ <-chan struct{}) error {
					log.Infof("joining leader-election for %s in %s on cluster %s",
						leaderelection.NamespaceController, options.SystemNamespace, options.ClusterID)
					election := leaderelection.
						NewLeaderElectionMulticluster(options.SystemNamespace, m.serverID, leaderelection.NamespaceController, m.revision, !configCluster, client).
						AddRunFunction(func(leaderStop <-chan struct{}) {
							log.Infof("starting namespace controller for cluster %s", cluster.ID)
							nc := NewNamespaceController(client, m.caBundleWatcher)
							// Start informers again. This fixes the case where informers for namespace do not start,
							// as we create them only after acquiring the leader lock
							// Note: stop here should be the overall pilot stop, NOT the leader election stop. We are
							// basically lazy loading the informer, if we stop it when we lose the lock we will never
							// recreate it again.
							client.RunAndWait(clusterStopCh)
							nc.Run(leaderStop)
						})
					election.Run(clusterStopCh)
					return nil
				})
			}
		}
		// Set up injection webhook patching for remote clusters we are controlling.
		// The config cluster has this patching set up elsewhere. We may eventually want to move it here.
		// We can not use leader election for webhook patching because each revision needs to patch its own
		// webhook.
		if shouldLead && !configCluster && m.caBundleWatcher != nil {
			// Patch injection webhook cert
			// This requires RBAC permissions - a low-priv Istiod should not attempt to patch but rely on
			// operator or CI/CD
			if features.InjectionWebhookConfigName != "" {
				log.Infof("initializing injection webhook cert patcher for cluster %s", cluster.ID)
				patcher, err := webhooks.NewWebhookCertPatcher(client, m.revision, webhookName, m.caBundleWatcher)
				if err != nil {
					log.Errorf("could not initialize webhook cert patcher: %v", err)
				} else {
					go patcher.Run(clusterStopCh)
				}
			}
		}
	}()

	// setting up the serviceexport controller if and only if it is turned on in the meshconfig.
	if features.EnableMCSAutoExport {
		log.Infof("joining leader-election for %s in %s on cluster %s",
			leaderelection.ServiceExportController, options.SystemNamespace, options.ClusterID)
		// Block server exit on graceful termination of the leader controller.
		m.s.RunComponentAsyncAndWait("auto serviceexport controller", func(_ <-chan struct{}) error {
			leaderelection.
				NewLeaderElectionMulticluster(options.SystemNamespace, m.serverID, leaderelection.ServiceExportController, m.revision, !configCluster, client).
				AddRunFunction(func(leaderStop <-chan struct{}) {
					serviceExportController := newAutoServiceExportController(autoServiceExportOptions{
						Client:       client,
						ClusterID:    options.ClusterID,
						DomainSuffix: options.DomainSuffix,
						ClusterLocal: m.clusterLocal,
					})
					// Start informers again. This fixes the case where informers do not start,
					// as we create them only after acquiring the leader lock
					// Note: stop here should be the overall pilot stop, NOT the leader election stop. We are
					// basically lazy loading the informer, if we stop it when we lose the lock we will never
					// recreate it again.
					client.RunAndWait(clusterStopCh)
					serviceExportController.Run(leaderStop)
				}).Run(clusterStopCh)
			return nil
		})
	}
}

// checkShouldLead returns true if the caller should attempt leader election for a remote cluster.
func (m *Multicluster) checkShouldLead(client kubelib.Client, systemNamespace string, stop <-chan struct{}) bool {
	var res bool
	if features.ExternalIstiod {
		b := backoff.NewExponentialBackOff(backoff.DefaultOption())
		ctx, cancel := context.WithCancel(context.Background())
		go func() {
			select {
			case <-stop:
				cancel()
			case <-ctx.Done():
			}
		}()
		defer cancel()
		_ = b.RetryWithContext(ctx, func() error {
			namespace, err := client.Kube().CoreV1().Namespaces().Get(context.TODO(), systemNamespace, metav1.GetOptions{})
			if err != nil {
				if errors.IsNotFound(err) {
					return nil
				}
				return err
			}
			// found same system namespace on the remote cluster so check if we are a selected istiod to lead
			istiodCluster, found := namespace.Annotations[annotation.TopologyControlPlaneClusters.Name]
			if found {
				localCluster := string(m.opts.ClusterID)
				for _, cluster := range strings.Split(istiodCluster, ",") {
					if cluster == "*" || cluster == localCluster {
						res = true
						return nil
					}
				}
			}
			return nil
		})
	}
	return res
}

func createWleConfigStore(client kubelib.Client, revision string, opts Options) model.ConfigStoreController {
	log.Infof("Creating WorkloadEntry only config store for %s", opts.ClusterID)
	workloadEntriesSchemas := collection.NewSchemasBuilder().
		MustAdd(collections.WorkloadEntry).
		Build()
	crdOpts := crdclient.Option{
		Revision:     revision,
		DomainSuffix: opts.DomainSuffix,
		Identifier:   "mc-workload-entry-controller",
		KrtDebugger:  opts.KrtDebugger,
	}
	return crdclient.NewForSchemas(client, crdOpts, workloadEntriesSchemas)
}
