// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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 manager

import (
	"context"
	"crypto/tls"
	"os"

	"github.com/go-logr/logr"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/client-go/discovery"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

	"github.com/apache/apisix-ingress-controller/api/v1alpha1"
	apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
	"github.com/apache/apisix-ingress-controller/internal/controller"
	"github.com/apache/apisix-ingress-controller/internal/controller/config"
	"github.com/apache/apisix-ingress-controller/internal/controller/status"
	"github.com/apache/apisix-ingress-controller/internal/manager/readiness"
	"github.com/apache/apisix-ingress-controller/internal/manager/server"
	"github.com/apache/apisix-ingress-controller/internal/provider"
	_ "github.com/apache/apisix-ingress-controller/internal/provider/init"
	_ "github.com/apache/apisix-ingress-controller/pkg/metrics"
	"github.com/apache/apisix-ingress-controller/pkg/utils"
)

var (
	scheme = runtime.NewScheme()
)

var (
	// set value during compilation
	_minK8sVersion string
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))

	if err := gatewayv1.Install(scheme); err != nil {
		panic(err)
	}
	if err := v1alpha1.AddToScheme(scheme); err != nil {
		panic(err)
	}
	if err := apiv2.AddToScheme(scheme); err != nil {
		panic(err)
	}
	if err := gatewayv1.Install(scheme); err != nil {
		panic(err)
	}
	// +kubebuilder:scaffold:scheme
}

func Run(ctx context.Context, logger logr.Logger) error {
	cfg := config.ControllerConfig

	setupLog := ctrl.LoggerFrom(ctx).WithName("setup")

	// SetupSignalHandler must be called exactly once. Install it before setup
	// starts so that a shutdown signal is honored while waiting for the API
	// server, not only once the manager is running.
	signalCtx := ctrl.SetupSignalHandler()

	// if the enable-http2 flag is false (the default), http/2 should be disabled
	// due to its vulnerabilities. More specifically, disabling http/2 will
	// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
	// Rapid Reset CVEs. For more information see:
	// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
	// - https://github.com/advisories/GHSA-4374-p667-p6c8
	disableHTTP2 := func(c *tls.Config) {
		setupLog.Info("disabling http/2")
		c.NextProtos = []string{"http/1.1"}
	}

	var tlsOpts []func(*tls.Config)

	if !cfg.EnableHTTP2 {
		tlsOpts = append(tlsOpts, disableHTTP2)
	}

	// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
	// More info:
	// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/server
	// - https://book.kubebuilder.io/reference/metrics.html
	metricsServerOptions := metricsserver.Options{
		BindAddress:   cfg.MetricsAddr,
		SecureServing: cfg.SecureMetrics,
		// TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are
		// not provided, self-signed certificates will be generated by default. This option is not recommended for
		// production environments as self-signed certificates do not offer the same level of trust and security
		// as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing
		// unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName
		// to provide certificates, ensuring the server communicates using trusted and secure certificates.
		TLSOpts: tlsOpts,
	}

	if cfg.SecureMetrics {
		// FilterProvider is used to protect the metrics endpoint with authn/authz.
		// These configurations ensure that only authorized users and service accounts
		// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
		// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/filters#WithAuthenticationAndAuthorization
		metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
	}

	namespace := os.Getenv("POD_NAMESPACE")
	if namespace == "" {
		namespace = "default"
	}

	mgrOptions := ctrl.Options{
		Scheme:                  scheme,
		Metrics:                 metricsServerOptions,
		HealthProbeBindAddress:  cfg.ProbeAddr,
		LeaderElection:          !config.ControllerConfig.LeaderElection.Disable,
		LeaderElectionID:        cfg.LeaderElectionID,
		LeaderElectionNamespace: namespace,
		LeaseDuration:           ptr.To(config.ControllerConfig.LeaderElection.LeaseDuration.Duration),
		RenewDeadline:           ptr.To(config.ControllerConfig.LeaderElection.RenewDeadline.Duration),
		RetryPeriod:             ptr.To(config.ControllerConfig.LeaderElection.RetryPeriod.Duration),
		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
		// when the Manager ends. This requires the binary to immediately end when the
		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
		// speeds up voluntary leader transitions as the new leader don't have to wait
		// LeaseDuration time first.
		//
		// In the default scaffold provided, the program ends immediately after
		// the manager stops, so would be fine to enable this option. However,
		// if you are doing or is intended to do any operation such as perform cleanups
		// after the manager stops then its usage might be unsafe.
		// LeaderElectionReleaseOnCancel: true,
	}

	if cfg.Webhook != nil && cfg.Webhook.Enable {
		webhookServer := webhook.NewServer(webhook.Options{
			Port:     cfg.Webhook.Port,
			CertDir:  cfg.Webhook.TLSCertDir,
			CertName: cfg.Webhook.TLSCertFile,
			KeyName:  cfg.Webhook.TLSKeyFile,
		})
		mgrOptions.WebhookServer = webhookServer
	}

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions)
	if err != nil {
		setupLog.Error(err, "unable to start manager")
		return err
	}

	// Which API resources are installed is detected once, below, and gates the
	// registration of field indexes, controllers and readiness checks for the
	// whole lifetime of the process. Detecting them against an unreachable API
	// server would silently classify everything as "not installed" and leave the
	// controller permanently degraded until it is restarted, so wait for the API
	// server to answer first.
	if err := utils.WaitForAPIServer(signalCtx, mgr.GetConfig(), setupLog); err != nil {
		setupLog.Error(err, "unable to reach the Kubernetes API server")
		return err
	}

	// API resource detection runs outside the manager and does not observe
	// signalCtx, so check between setup phases: now that the signal handler is
	// installed this early, a shutdown signal would otherwise be swallowed until
	// mgr.Start.
	checkShutdown := func() error {
		if err := signalCtx.Err(); err != nil {
			setupLog.Info("shutdown requested during setup, stopping", "reason", err)
			return err
		}
		return nil
	}

	// Check Kubernetes cluster version
	checkK8sVersion(mgr, setupLog)

	readier := readiness.NewReadinessManager(mgr.GetClient(), logger)
	if err := registerReadiness(mgr, readier); err != nil {
		setupLog.Error(err, "unable to register readiness checks")
		return err
	}
	if err := checkShutdown(); err != nil {
		return err
	}

	if err := mgr.Add(readier); err != nil {
		setupLog.Error(err, "unable to add readiness manager")
	}

	updater := status.NewStatusUpdateHandler(ctrl.LoggerFrom(ctx).WithName("status").WithName("updater"), mgr.GetClient())
	if err := mgr.Add(updater); err != nil {
		setupLog.Error(err, "unable to add status updater")
		return err
	}

	providerType := string(config.ControllerConfig.ProviderConfig.Type)

	providerOptions := &provider.Options{
		SyncTimeout:           config.ControllerConfig.ExecADCTimeout.Duration,
		SyncPeriod:            config.ControllerConfig.ProviderConfig.SyncPeriod.Duration,
		InitSyncDelay:         config.ControllerConfig.ProviderConfig.InitSyncDelay.Duration,
		ListenerPortMatchMode: config.ControllerConfig.ListenerPortMatchMode,
	}
	provider, err := provider.New(providerType, logger, updater.Writer(), readier, providerOptions)
	if err != nil {
		setupLog.Error(err, "unable to create provider")
		return err
	}

	if cfg.EnableServer {
		srv := server.NewServer(config.ControllerConfig.ServerAddr)
		srv.Register("/debug", provider)
		if err := mgr.Add(srv); err != nil {
			setupLog.Error(err, "unable to add debug server to manager")
			return err
		}
	}
	if err := mgr.Add(provider); err != nil {
		setupLog.Error(err, "unable to add provider to manager")
		return err
	}

	// ReferenceGrant is a Gateway API kind and only consulted by Gateway API
	// paths, so skip the detection entirely when Gateway API is disabled.
	hasReferenceGrant := false
	if config.ControllerConfig.DisableGatewayAPI {
		setupLog.Info("Gateway API is disabled, skipping the ReferenceGrants check")
	} else {
		setupLog.Info("check ReferenceGrants is enabled")
		if hasReferenceGrant, err = utils.HasAPIResource(mgr, &gatewayv1.ReferenceGrant{}); err != nil {
			setupLog.Error(err, "unable to detect whether ReferenceGrants is installed")
			return err
		}
		if !hasReferenceGrant {
			setupLog.Info("CRD ReferenceGrants is not installed, cross-namespace references will be rejected",
				"gvk", utils.FormatGVK(&gatewayv1.ReferenceGrant{}))
		}
	}
	controller.SetEnableReferenceGrant(hasReferenceGrant)
	if err := checkShutdown(); err != nil {
		return err
	}

	setupLog.Info("setting up controllers")
	controllers, err := setupControllers(ctx, mgr, provider, updater.Writer(), readier)
	if err != nil {
		setupLog.Error(err, "unable to set up controllers")
		return err
	}

	for _, c := range controllers {
		if err := c.SetupWithManager(mgr); err != nil {
			return err
		}
		if err := checkShutdown(); err != nil {
			return err
		}
	}

	// +kubebuilder:scaffold:builder

	if cfg.Webhook != nil && cfg.Webhook.Enable {
		setupLog.Info("setting up webhooks")
		if err := setupWebhooks(ctx, mgr); err != nil {
			setupLog.Error(err, "unable to create webhook", "webhook", "Ingress")
			return err
		}
	} else {
		setupLog.Info("webhooks disabled, skipping webhook setup")
	}

	setupLog.Info("setting up health checks")
	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		setupLog.Error(err, "unable to set up health check")
		return err
	}

	setupLog.Info("setting up ready checks")
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		setupLog.Error(err, "unable to set up ready check")
		return err
	}

	setupLog.Info("starting controller manager")
	return mgr.Start(signalCtx)
}

func checkK8sVersion(mgr ctrl.Manager, logger logr.Logger) {
	minV, err := version.ParseSemantic(_minK8sVersion)
	if err != nil {
		logger.Info("failed to parse minimum version", "error", err)
		return
	}

	discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
	if err != nil {
		logger.Info("failed to create discovery client for version check", "error", err)
		return
	}

	serverVersion, err := discoveryClient.ServerVersion()
	if err != nil {
		logger.Info("failed to get Kubernetes server version", "error", err)
		return
	}

	currentVersion, err := version.ParseSemantic(serverVersion.GitVersion)
	if err != nil {
		logger.Info("failed to parse server version", "error", err)
		return
	}

	if !currentVersion.AtLeast(minV) {
		logger.Info("WARNING: Kubernetes cluster version does not meet minimum requirement",
			"currentVersion", currentVersion.String(),
			"minimumVersion", minV.String(),
		)
	}
}
