// 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 ingress provides a read-only view of Kubernetes ingress resources
// as an ingress rule configuration type store
package ingress

import (
	"errors"

	corev1 "k8s.io/api/core/v1"
	knetworking "k8s.io/api/networking/v1"

	"istio.io/istio/pilot/pkg/model"
	kubecontroller "istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
	"istio.io/istio/pilot/pkg/status"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/config/mesh/meshwatcher"
	"istio.io/istio/pkg/config/schema/collection"
	"istio.io/istio/pkg/config/schema/collections"
	"istio.io/istio/pkg/config/schema/gvk"
	"istio.io/istio/pkg/config/schema/kind"
	"istio.io/istio/pkg/env"
	"istio.io/istio/pkg/kube"
	"istio.io/istio/pkg/kube/kclient"
	"istio.io/istio/pkg/kube/krt"
	"istio.io/istio/pkg/log"
	"istio.io/istio/pkg/revisions"
	"istio.io/istio/pkg/util/sets"
)

// In 1.0, the Gateway is defined in the namespace where the actual controller runs, and needs to be managed by
// user.
// The gateway is named by appending "-istio-autogenerated-k8s-ingress" to the name of the ingress.
//
// Currently the gateway namespace is hardcoded to istio-system (model.IstioIngressNamespace)
//
// VirtualServices are also auto-generated in the model.IstioIngressNamespace.
//
// The sync of Ingress objects to IP is done by status.go
// the 'ingress service' name is used to get the IP of the Service
// If ingress service is empty, it falls back to NodeExternalIP list, selected using the labels.
// This is using 'namespace' of pilot - but seems to be broken (never worked), since it uses Pilot's pod labels
// instead of the ingress labels.

// Follows mesh.IngressControllerMode setting to enable - OFF|STRICT|DEFAULT.
// STRICT requires "kubernetes.io/ingress.class" == mesh.IngressClass
// DEFAULT allows Ingress without explicit class.

// In 1.1:
// - K8S_INGRESS_NS - namespace of the Gateway that will act as ingress.
// - labels of the gateway set to "app=ingressgateway" for node_port, service set to 'ingressgateway' (matching default install)
//   If we need more flexibility - we can add it (but likely we'll deprecate ingress support first)
// -

var schemas = collection.SchemasFor(
	collections.VirtualService,
	collections.Gateway)

type xdsConfigUpdater interface {
	ConfigUpdate(req *model.PushRequest)
}

var IngressNamespace = env.Register("K8S_INGRESS_NS", constants.IstioSystemNamespace,
	"The namespace where ingress controller runs, by default it is istio-system").Get()

var errUnsupportedOp = errors.New("unsupported operation: the ingress config store is a read-only view")

type Controller struct {
	// client for accessing Kubernetes
	client kube.Client

	stop chan struct{}

	xdsUpdater xdsConfigUpdater

	// Handlers tracks all registered handlers, so that syncing can be detected
	handlers []krt.HandlerRegistration

	inputs Inputs

	// outputs contains all the output collections for this controller.
	outputs Outputs

	status     *status.StatusCollections
	tagWatcher krt.RecomputeProtected[revisions.TagWatcher]
}

type Inputs struct {
	Ingresses      krt.Collection[*knetworking.Ingress]
	IngressClasses krt.Collection[*knetworking.IngressClass]
	Services       krt.Collection[*corev1.Service]
	Nodes          krt.Collection[*corev1.Node]
	Pods           krt.Collection[*corev1.Pod]
	MeshConfig     krt.Collection[meshwatcher.MeshConfigResource]
}

type Outputs struct {
	VirtualServices krt.Collection[config.Config]
	Gateways        krt.Collection[config.Config]
}

func NewController(
	client kube.Client,
	meshConfig meshwatcher.WatcherCollection,
	options kubecontroller.Options,
	xdsUpdater xdsConfigUpdater,
) *Controller {
	stop := make(chan struct{})
	opts := krt.NewOptionsBuilder(stop, "ingress", options.KrtDebugger)

	tw := revisions.NewTagWatcher(client, options.Revision, options.SystemNamespace)
	c := &Controller{
		client:     client,
		stop:       stop,
		status:     &status.StatusCollections{},
		xdsUpdater: xdsUpdater,
		tagWatcher: krt.NewRecomputeProtected(tw, false, opts.WithName("tagWatcher")...),
	}
	tw.AddHandler(func(s sets.String) {
		c.tagWatcher.TriggerRecomputation()
	})

	c.inputs = Inputs{
		IngressClasses: krt.NewInformer[*knetworking.IngressClass](client, opts.WithName("informer/IngressClasses")...),
		Ingresses: krt.WrapClient(
			kclient.NewFiltered[*knetworking.Ingress](client, kclient.Filter{
				ObjectFilter: client.ObjectFilter(),
			}),
			opts.WithName("informer/Ingresses")...,
		),
		Services: krt.WrapClient(
			kclient.NewFiltered[*corev1.Service](client, kclient.Filter{
				ObjectFilter: client.ObjectFilter(),
			}),
			opts.WithName("informer/Services")...,
		),
		Nodes: krt.NewFilteredInformer[*corev1.Node](client, kclient.Filter{
			ObjectFilter:    client.ObjectFilter(),
			ObjectTransform: kube.StripNodeUnusedFields,
		}, opts.WithName("informer/Nodes")...),
		Pods: krt.NewFilteredInformer[*corev1.Pod](client, kclient.Filter{
			ObjectFilter:    client.ObjectFilter(),
			ObjectTransform: kube.StripPodUnusedFields,
			FieldSelector:   "status.phase!=Failed",
		}, opts.WithName("informer/Pods")...),
		MeshConfig: meshConfig.AsCollection(),
	}

	ServicesWithPorts := ServicesWithPorts(
		c.inputs.Services,
		opts,
	)

	Status, SupportedIngresses := SupportedIngresses(
		c.inputs.IngressClasses,
		c.inputs.Ingresses,
		meshConfig,
		c.inputs.Services,
		c.inputs.Nodes,
		c.inputs.Pods,
		opts,
	)
	status.RegisterStatus(c.status, Status, func(ingress *knetworking.Ingress) knetworking.IngressStatus {
		return ingress.Status
	}, c.tagWatcher.AccessUnprotected())

	_, RuleHostIndex := RuleCollection(
		SupportedIngresses,
		opts,
	)

	c.outputs = Outputs{
		VirtualServices: VirtualServices(
			RuleHostIndex,
			ServicesWithPorts,
			options.DomainSuffix,
			opts,
		),
		Gateways: Gateways(
			SupportedIngresses,
			meshConfig,
			options.DomainSuffix,
			opts,
		),
	}

	// we don't need to register the virtual services here because they are handled by the virtual service controller
	c.handlers = append(
		c.handlers,
		c.outputs.Gateways.RegisterBatch(pushXds(c.xdsUpdater,
			func(t config.Config) model.ConfigKey {
				return model.ConfigKey{
					Kind:      kind.Gateway,
					Name:      t.Name,
					Namespace: t.Namespace,
				}
			}), false),
	)

	return c
}

func (c *Controller) SetStatusWrite(enabled bool, statusManager *status.Manager) {
	if enabled && statusManager != nil {
		var q status.Queue = statusManager.CreateGenericController(func(status status.Manipulator, context any) {
			status.SetInner(context)
		})
		c.status.SetQueue(q)
	} else {
		c.status.UnsetQueue()
	}
}

func (c *Controller) Run(stop <-chan struct{}) {
	log.Infof("Starting ingress controller")

	tw := c.tagWatcher.AccessUnprotected()
	go tw.Run(stop)
	go func() {
		kube.WaitForCacheSync("ingress tag watcher", stop, tw.HasSynced)
		c.tagWatcher.MarkSynced()
	}()

	<-stop
	close(c.stop)
}

func (c *Controller) RegisterEventHandler(kind config.GroupVersionKind, f model.EventHandler) {
}

func (c *Controller) HasSynced() bool {
	if !c.outputs.VirtualServices.HasSynced() ||
		!c.outputs.Gateways.HasSynced() {
		return false
	}

	for _, h := range c.handlers {
		if !h.HasSynced() {
			return false
		}
	}

	return true
}

func (c *Controller) Schemas() collection.Schemas {
	return schemas
}

func (c *Controller) KrtCollection(kind config.GroupVersionKind) krt.Collection[config.Config] {
	if kind == gvk.Gateway {
		return c.outputs.Gateways
	}

	if kind == gvk.VirtualService {
		return c.outputs.VirtualServices
	}

	return nil
}

func (c *Controller) Get(typ config.GroupVersionKind, name, namespace string) *config.Config {
	return nil
}

func (c *Controller) List(typ config.GroupVersionKind, namespace string) []config.Config {
	if typ == gvk.Gateway {
		return c.outputs.Gateways.List()
	}

	if typ == gvk.VirtualService {
		return c.outputs.VirtualServices.List()
	}

	return nil
}

func (c *Controller) Create(_ config.Config) (string, error) {
	return "", errUnsupportedOp
}

func (c *Controller) Update(_ config.Config) (string, error) {
	return "", errUnsupportedOp
}

func (c *Controller) UpdateStatus(config.Config) (string, error) {
	return "", errUnsupportedOp
}

func (c *Controller) Patch(_ config.Config, _ config.PatchFunc) (string, error) {
	return "", errUnsupportedOp
}

func (c *Controller) Delete(_ config.GroupVersionKind, _, _ string, _ *string) error {
	return errUnsupportedOp
}

func pushXds[T any](xds xdsConfigUpdater, f func(T) model.ConfigKey) func(events []krt.Event[T]) {
	return func(events []krt.Event[T]) {
		if xds == nil {
			return
		}
		cu := sets.New[model.ConfigKey]()
		for _, e := range events {
			for _, i := range e.Items() {
				c := f(i)
				if c != (model.ConfigKey{}) {
					cu.Insert(c)
				}
			}
		}
		if len(cu) == 0 {
			return
		}
		xds.ConfigUpdate(&model.PushRequest{
			Full:           true,
			ConfigsUpdated: cu,
			Reason:         model.NewReasonStats(model.ConfigUpdate),
		})
	}
}
