// 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"
	"k8s.io/apimachinery/pkg/runtime/schema"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	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"
	gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
	"sigs.k8s.io/gateway-api/apis/v1beta1"

	"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"
)

var (
	scheme = runtime.NewScheme()
)

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

	if err := gatewayv1.Install(scheme); err != nil {
		panic(err)
	}
	if err := gatewayv1alpha2.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 := v1beta1.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")

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

	readier := readiness.NewReadinessManager(mgr.GetClient(), logger)
	registerReadinessGVK(mgr.GetClient(), readier)

	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,
		BackendMode:   string(config.ControllerConfig.ProviderConfig.Type),
	}
	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
	}

	setupLog.Info("check ReferenceGrants is enabled")
	_, err = mgr.GetRESTMapper().KindsFor(schema.GroupVersionResource{
		Group:    v1beta1.GroupVersion.Group,
		Version:  v1beta1.GroupVersion.Version,
		Resource: "referencegrants",
	})
	if err != nil {
		setupLog.Info("CRD ReferenceGrants is not installed", "err", err)
	}
	controller.SetEnableReferenceGrant(err == nil)

	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
		}
	}

	// +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(ctrl.SetupSignalHandler())
}
