// Licensed to 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. Apache Software Foundation (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 cmdsetup

import (
	"context"

	"github.com/spf13/cobra"

	"github.com/apache/skywalking-banyandb/api/common"
	"github.com/apache/skywalking-banyandb/api/data"
	databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
	"github.com/apache/skywalking-banyandb/banyand/dquery"
	"github.com/apache/skywalking-banyandb/banyand/liaison/grpc"
	"github.com/apache/skywalking-banyandb/banyand/liaison/grpc/route"
	"github.com/apache/skywalking-banyandb/banyand/liaison/http"
	"github.com/apache/skywalking-banyandb/banyand/measure"
	"github.com/apache/skywalking-banyandb/banyand/metadata"
	"github.com/apache/skywalking-banyandb/banyand/observability"
	"github.com/apache/skywalking-banyandb/banyand/observability/services"
	"github.com/apache/skywalking-banyandb/banyand/protector"
	"github.com/apache/skywalking-banyandb/banyand/queue"
	"github.com/apache/skywalking-banyandb/banyand/queue/pub"
	"github.com/apache/skywalking-banyandb/banyand/queue/sub"
	"github.com/apache/skywalking-banyandb/banyand/stream"
	"github.com/apache/skywalking-banyandb/banyand/trace"
	"github.com/apache/skywalking-banyandb/pkg/logger"
	"github.com/apache/skywalking-banyandb/pkg/node"
	"github.com/apache/skywalking-banyandb/pkg/run"
	"github.com/apache/skywalking-banyandb/pkg/version"
)

func newLiaisonCmd(runners ...run.Unit) *cobra.Command {
	l := logger.GetLogger("bootstrap")
	ctx := context.Background()
	metaSvc, err := metadata.NewClient()
	if err != nil {
		l.Fatal().Err(err).Msg("failed to initiate metadata service")
	}
	tire1Client := pub.New(metaSvc, databasev1.Role_ROLE_LIAISON)
	tire2Client := pub.New(metaSvc, databasev1.Role_ROLE_DATA)
	metaSvc.SetDataBroadcaster(tire2Client)
	metaSvc.SetLiaisonBroadcaster(tire1Client)

	localPipeline := queue.Local()

	measureLiaisonNodeSel := node.NewRoundRobinSelector(data.TopicMeasureWrite.String(), metaSvc)
	measureLiaisonNodeRegistry := grpc.NewClusterNodeRegistry(data.TopicMeasureWrite, tire1Client, measureLiaisonNodeSel)
	measureDataNodeSel := node.NewRoundRobinSelector(data.TopicMeasureWrite.String(), metaSvc)
	metricSvc := services.NewMetricService(metaSvc, tire1Client, "liaison", measureLiaisonNodeRegistry)
	metaSvc.SetMetricsRegistry(metricSvc)
	pm := protector.NewMemory(metricSvc)
	internalPipeline := sub.NewServerWithPorts(metricSvc, "liaison-server", 18912, 18913)
	measureSVC, err := measure.NewLiaison(metaSvc, internalPipeline, metricSvc, pm, measureDataNodeSel, tire2Client)
	if err != nil {
		l.Fatal().Err(err).Msg("failed to initiate measure liaison service")
	}

	streamLiaisonNodeSel := node.NewRoundRobinSelector(data.TopicStreamWrite.String(), metaSvc)
	streamDataNodeSel := node.NewRoundRobinSelector(data.TopicStreamPartSync.String(), metaSvc)
	streamSVC, err := stream.NewLiaison(metaSvc, internalPipeline, metricSvc, pm, streamDataNodeSel, tire2Client)
	if err != nil {
		l.Fatal().Err(err).Msg("failed to initiate stream liaison service")
	}

	traceLiaisonNodeSel := node.NewRoundRobinSelector(data.TopicTraceWrite.String(), metaSvc)
	traceDataNodeSel := node.NewRoundRobinSelector(data.TopicTracePartSync.String(), metaSvc)
	traceSVC, err := trace.NewLiaison(metaSvc, internalPipeline, metricSvc, pm, traceDataNodeSel, tire2Client)
	if err != nil {
		l.Fatal().Err(err).Msg("failed to initiate trace liaison service")
	}

	propertyNodeSel := node.NewRoundRobinSelector(data.TopicPropertyUpdate.String(), metaSvc)

	dQuery, err := dquery.NewService(metaSvc, localPipeline, tire2Client, metricSvc, streamSVC, measureSVC, traceSVC)
	if err != nil {
		l.Fatal().Err(err).Msg("failed to initiate distributed query service")
	}

	routeProviders := map[string]route.TableProvider{
		"tire1": tire1Client,
		"tire2": tire2Client,
	}

	grpcServer := grpc.NewServer(ctx, tire1Client, tire2Client, localPipeline, metaSvc, grpc.NodeRegistries{
		MeasureLiaisonNodeRegistry: measureLiaisonNodeRegistry,
		StreamLiaisonNodeRegistry:  grpc.NewClusterNodeRegistry(data.TopicStreamWrite, tire1Client, streamLiaisonNodeSel),
		PropertyNodeRegistry:       grpc.NewClusterNodeRegistry(data.TopicPropertyUpdate, tire2Client, propertyNodeSel),
		TraceLiaisonNodeRegistry:   grpc.NewClusterNodeRegistry(data.TopicTraceWrite, tire1Client, traceLiaisonNodeSel),
	}, metricSvc, pm, routeProviders)
	internalPipeline.SetMetadataRepo(metaSvc)
	internalPipeline.SetNodeSchemaStatusRepo(metaSvc)
	profSvc := observability.NewProfService()
	httpServer := http.NewServer(grpcServer.GetAuthReloader())
	var units []run.Unit
	units = append(units, runners...)
	units = append(units,
		metricSvc,
		metaSvc,
		localPipeline,
		internalPipeline,
		tire1Client,
		tire2Client,
		measureLiaisonNodeSel,
		measureDataNodeSel,
		streamLiaisonNodeSel,
		streamDataNodeSel,
		traceLiaisonNodeSel,
		traceDataNodeSel,
		propertyNodeSel,
		streamSVC,
		measureSVC,
		traceSVC,
		dQuery,
		grpcServer,
		httpServer,
		profSvc,
	)
	liaisonGroup := run.NewGroup("liaison")
	liaisonGroup.Register(units...)
	var nodeSelector string
	liaisonCmd := &cobra.Command{
		Use:     "liaison",
		Version: version.Build(),
		Short:   "Run as the liaison server",
		RunE: func(_ *cobra.Command, _ []string) (err error) {
			// See standalone.go for the rationale: Cobra short-circuits
			// PersistentPostRunE on a non-nil RunE error, so we must
			// drain the artifact sink here.
			defer ShutdownSupervisor()
			// Derive from SupervisorContext so that a panic recovered anywhere
			// in the process, including goroutines spawned via run.Go, fires
			// cancellation here.
			runCtx := SupervisorContext()
			if nodeSelector != "" {
				runCtx = context.WithValue(runCtx, common.ContextNodeSelectorKey, nodeSelector)
				var ls *pub.LabelSelector
				ls, err = pub.ParseLabelSelector(nodeSelector)
				if err != nil {
					return err
				}
				for _, sel := range []node.Selector{measureDataNodeSel, streamDataNodeSel, propertyNodeSel, traceDataNodeSel} {
					sel.SetNodeSelector(ls)
				}
			}
			node, err := common.GenerateNode(internalPipeline.GetPort(), httpServer.GetPort(), nil, nil, nil)
			if err != nil {
				return err
			}
			// Stamp the liaison's own identity onto the cluster publishers so the
			// receiving data nodes can label queue metrics by sender (remote_node/
			// role/tier), enabling the liaison<->data topology to be drawn.
			liaisonTier := node.Labels["type"]
			for _, c := range []queue.Client{tire1Client, tire2Client} {
				if setter, ok := c.(interface{ SetSelfNode(string, string, string) }); ok {
					setter.SetSelfNode(node.NodeID, "liaison", liaisonTier)
				}
			}
			logger.GetLogger().Info().Msg("starting as a liaison server")
			runCtx = context.WithValue(runCtx, common.ContextNodeKey, node)
			// Spawn our go routines and wait for shutdown.
			if err := liaisonGroup.Run(runCtx); err != nil {
				logger.GetLogger().Error().Err(err).Stack().Str("name", liaisonGroup.Name()).Msg("Exit")
				return err
			}
			return nil
		},
	}
	liaisonCmd.Flags().AddFlagSet(liaisonGroup.RegisterFlags().FlagSet)
	liaisonCmd.PersistentFlags().StringVar(&nodeSelector, "data-node-selector", "",
		"the data node selector. e.g. key1=value1,key2=value2. If not set, all nodes are selected")
	return liaisonCmd
}
