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

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/collectors"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/robfig/cron/v3"

	"github.com/apache/skywalking-banyandb/api/common"
	"github.com/apache/skywalking-banyandb/banyand/metadata"
	"github.com/apache/skywalking-banyandb/banyand/observability"
	"github.com/apache/skywalking-banyandb/banyand/queue"
	"github.com/apache/skywalking-banyandb/pkg/logger"
	"github.com/apache/skywalking-banyandb/pkg/meter"
	"github.com/apache/skywalking-banyandb/pkg/meter/native"
	"github.com/apache/skywalking-banyandb/pkg/meter/prom"
	"github.com/apache/skywalking-banyandb/pkg/panicdiag"
	"github.com/apache/skywalking-banyandb/pkg/run"
	"github.com/apache/skywalking-banyandb/pkg/timestamp"
)

const (
	flagNativeMode    = "native"
	flagPromethusMode = "prometheus"
)

var (
	errNoAddr         = errors.New("no address")
	errNoMode         = errors.New("no observability mode")
	errInvalidMode    = errors.New("invalid observability mode")
	errDuplicatedMode = errors.New("duplicated observability mode")
)

var (
	_ run.Service                             = (*metricService)(nil)
	_ run.Config                              = (*metricService)(nil)
	_ observability.PrometheusHandlerProvider = (*metricService)(nil)

	obScope = observability.RootScope.SubScope("observability")
)

// Service type for Metric Service.
type Service interface {
	run.PreRunner
	run.Service
}

// NewMetricService returns a metric service.
func NewMetricService(metadata metadata.Repo, pipeline queue.Client, nodeType string, nodeSelector native.NodeSelector) observability.MetricsRegistry {
	return &metricService{
		closer:       run.NewCloser(1),
		metadata:     metadata,
		pipeline:     pipeline,
		nodeType:     nodeType,
		nodeSelector: nodeSelector,
	}
}

// NewMetricServiceWithoutListener returns a metric service that does not start
// its own HTTP listener. The embedding service is responsible for serving the
// metrics, e.g. by mounting PrometheusHandler on its own router. This lets the
// lifecycle command expose /metrics on its existing HTTP port instead of opening
// a separate observability listener.
func NewMetricServiceWithoutListener(metadata metadata.Repo, pipeline queue.Client, nodeType string, nodeSelector native.NodeSelector) observability.MetricsRegistry {
	// Direct assertion: NewMetricService always returns *metricService, so a
	// mismatch should fail loudly here rather than nil-deref on the next line.
	svc := NewMetricService(metadata, pipeline, nodeType, nodeSelector).(*metricService)
	svc.listenerDisabled = true
	return svc
}

type metricService struct {
	metadata            metadata.Repo
	nodeSelector        native.NodeSelector
	pipeline            queue.Client
	promReg             *prometheus.Registry
	l                   *logger.Logger
	closer              *run.Closer
	svr                 *http.Server
	nCollection         *native.MetricCollection
	scheduler           *timestamp.Scheduler
	schedulerMetrics    *SchedulerMetrics
	listenAddr          string
	nodeType            string
	panicArtifactDir    string
	modes               []string
	npf                 nativeProviderFactory
	metricsInterval     time.Duration
	nativeFlushInterval time.Duration
	panicMaxArtifacts   int
	listenerDisabled    bool
	mutex               sync.Mutex
}

func (p *metricService) FlagSet() *run.FlagSet {
	flagSet := run.NewFlagSet("observability")
	flagSet.StringVar(&p.listenAddr, "observability-listener-addr", ":2121", "listen addr for observability")
	flagSet.StringSliceVar(&p.modes, "observability-modes", []string{"prometheus"}, "modes for observability")
	flagSet.DurationVar(&p.metricsInterval, "observability-metrics-interval", 15*time.Second, "interval for metrics collection")
	flagSet.DurationVar(&p.nativeFlushInterval, "observability-native-flush-interval", 5*time.Second, "interval for native metrics flush")
	flagSet.StringVar(&p.panicArtifactDir, "panic-artifact-dir", "",
		"directory where banyand writes recovered panic artifacts; leave empty to disable")
	flagSet.IntVar(&p.panicMaxArtifacts, "panic-max-artifacts", 0,
		"maximum number of panic artifact directories to retain; 0 disables pruning")
	return flagSet
}

func (p *metricService) Validate() error {
	if !p.listenerDisabled && p.listenAddr == "" {
		return errNoAddr
	}
	if len(p.modes) == 0 {
		return errNoMode
	}
	if p.metricsInterval <= 0 || p.nativeFlushInterval <= 0 {
		return errors.New("observability-metrics-interval and observability-native-flush-interval must be greater than 0")
	}
	set := make(map[string]struct{})
	for _, mode := range p.modes {
		if mode != flagNativeMode && mode != flagPromethusMode {
			return errInvalidMode
		}
		if _, exists := set[mode]; exists {
			return errDuplicatedMode
		}
		set[mode] = struct{}{}
	}
	return nil
}

func (p *metricService) PreRun(ctx context.Context) error {
	p.l = logger.GetLogger(p.Name())
	if containsMode(p.modes, flagPromethusMode) {
		p.promReg = prometheus.NewRegistry()
		p.promReg.MustRegister(collectors.NewGoCollector())
		p.promReg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
	}
	if containsMode(p.modes, flagNativeMode) {
		p.nCollection = native.NewMetricsCollection(p.pipeline, p.nodeSelector)
		val := ctx.Value(common.ContextNodeKey)
		if val == nil {
			return errors.New("metric service native mode, node id is empty")
		}
		node := val.(common.Node)
		nodeInfo := native.NodeInfo{
			Type:        p.nodeType,
			NodeID:      node.NodeID,
			GrpcAddress: node.GrpcAddress,
			HTTPAddress: node.HTTPAddress,
		}
		p.npf = nativeProviderFactory{
			metadata: p.metadata,
			nodeInfo: nodeInfo,
		}
	}
	var promProvider meter.Provider
	if containsMode(p.modes, flagPromethusMode) {
		promProvider = prom.NewProvider(observability.RootScope, p.promReg)
	}
	var nativeProvider meter.Provider
	if containsMode(p.modes, flagNativeMode) {
		nativeProvider = native.NewProvider(observability.RootScope, p.metadata, p.npf.nodeInfo)
		p.npf.mu.Lock()
		p.npf.providers = append(p.npf.providers, nativeProvider)
		p.npf.mu.Unlock()
	}
	// Register the process-wide panic counter so that all goroutines using
	// panicdiag.WithRecovery (including run.Group services and run.Go) increment
	// banyandb_panic_total{component="..."} without needing per-call wiring.
	panicdiag.SetDefaultPanicCounter(NewFactory(promProvider, nativeProvider, p.nCollection).NewCounter("panic_total", "component"))
	if p.panicArtifactDir != "" {
		panicdiag.SetDefaultArtifactRoot(p.panicArtifactDir)
	}
	if p.panicMaxArtifacts > 0 {
		panicdiag.SetDefaultMaxArtifacts(p.panicMaxArtifacts)
	}
	return nil
}

func (p *metricService) Name() string {
	return "metric-service"
}

func (p *metricService) Serve() run.StopNotify {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	if p.listenerDisabled {
		// A listener-suppressed service shares its process with a node's metric
		// service (e.g. the lifecycle sidecar alongside a data node). The host
		// gauges and the MetricsCollector singleton are process-global, so it must
		// not (re)initialize or drive them — that races the node's running
		// collector. It only flushes its own lifecycle/migration families.
		return p.serveWithoutListener()
	}
	p.initMetrics()
	p.initVecFrameMetrics()
	startupCtx, cancelStartup := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancelStartup()
	if containsMode(p.modes, flagNativeMode) {
		p.npf.setServeStarted()
		p.npf.initAllSchemas(startupCtx)
	}
	// First collection on startup so metrics arrive sooner.
	MetricsCollector.collect()
	if containsMode(p.modes, flagNativeMode) {
		p.nCollection.FlushMetrics(startupCtx)
	}
	clock, _ := timestamp.GetClock(context.TODO())
	p.scheduler = timestamp.NewScheduler(p.l, clock)
	p.schedulerMetrics = NewSchedulerMetrics(p.With(obScope))
	metricsCollectorExpr := fmt.Sprintf("@every %s", p.metricsInterval)
	err := p.scheduler.Register(startupCtx, "metrics-collector", cron.Descriptor, metricsCollectorExpr, func(_ context.Context, _ time.Time, _ *logger.Logger) bool {
		MetricsCollector.collect()
		metrics := p.scheduler.Metrics()
		for job, m := range metrics {
			p.schedulerMetrics.Collect(job, m)
		}
		return true
	})
	if err != nil {
		p.l.Fatal().Err(err).Msg("Failed to register metrics collector")
	}
	metricsMux := http.NewServeMux()
	metricsMux.HandleFunc("/_route", p.routeTableHandler)
	metricsMux.HandleFunc("/debug/panic", p.handleDebugPanic)
	if containsMode(p.modes, flagPromethusMode) {
		registerMetricsEndpoint(p.promReg, metricsMux)
	}
	if containsMode(p.modes, flagNativeMode) {
		nativeFlushExpr := fmt.Sprintf("@every %s", p.nativeFlushInterval)
		err = p.scheduler.Register(startupCtx, "native-metric-collection", cron.Descriptor, nativeFlushExpr, func(ctx context.Context, _ time.Time, _ *logger.Logger) bool {
			p.nCollection.FlushMetrics(ctx)
			return true
		})
		if err != nil {
			p.l.Fatal().Err(err).Msg("Failed to register native metric collection")
		}
	}
	p.svr = &http.Server{
		Addr:              p.listenAddr,
		ReadHeaderTimeout: 3 * time.Second,
		Handler:           metricsMux,
	}
	go func() {
		defer p.closer.Done()
		p.l.Info().Str("listenAddr", p.listenAddr).Msg("Start metric server")
		_ = p.svr.ListenAndServe()
	}()
	return p.closer.CloseNotify()
}

// serveWithoutListener brings up the metric service without its own HTTP listener
// and without touching the process-global host gauges (cpu/mem/net/disk) or the
// shared MetricsCollector singleton. The embedding service (e.g. the lifecycle
// command) serves /metrics via PrometheusHandler; this path only drives
// native-mode flushing of the lifecycle and migration metric families. Skipping
// the global host collectors avoids racing a co-located node's metric service in
// single-process deployments.
func (p *metricService) serveWithoutListener() run.StopNotify {
	startupCtx, cancelStartup := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancelStartup()
	if containsMode(p.modes, flagNativeMode) {
		p.npf.setServeStarted()
		p.npf.initAllSchemas(startupCtx)
		p.nCollection.FlushMetrics(startupCtx)
		clock, _ := timestamp.GetClock(context.TODO())
		p.scheduler = timestamp.NewScheduler(p.l, clock)
		nativeFlushExpr := fmt.Sprintf("@every %s", p.nativeFlushInterval)
		if err := p.scheduler.Register(startupCtx, "native-metric-collection", cron.Descriptor, nativeFlushExpr,
			func(ctx context.Context, _ time.Time, _ *logger.Logger) bool {
				p.nCollection.FlushMetrics(ctx)
				return true
			}); err != nil {
			p.l.Fatal().Err(err).Msg("Failed to register native metric collection")
		}
	}
	// Release the closer slot so GracefulStop's CloseThenWait does not block.
	p.l.Info().Msg("metric server listener disabled; metrics served by the embedding service")
	p.closer.Done()
	return p.closer.CloseNotify()
}

func (p *metricService) GracefulStop() {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	if p.scheduler != nil {
		p.scheduler.Close()
	}
	if p.svr != nil {
		_ = p.svr.Close()
	}
	p.closer.CloseThenWait()
}

func (p *metricService) NativeEnabled() bool {
	return containsMode(p.modes, flagNativeMode)
}

func (p *metricService) routeTableHandler(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	if p.nodeSelector == nil {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte(p.nodeSelector.String()))
}

// PrometheusHandler returns an http.Handler exposing this service's Prometheus
// registry, for mounting on an external router (see PrometheusHandlerProvider).
// It returns a 404 handler when prometheus mode is not enabled.
func (p *metricService) PrometheusHandler() http.Handler {
	if p.promReg == nil {
		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			http.Error(w, "prometheus mode not enabled", http.StatusNotFound)
		})
	}
	return promhttp.HandlerFor(p.promReg, promhttp.HandlerOpts{})
}

func containsMode(modes []string, mode string) bool {
	for _, item := range modes {
		if item == mode {
			return true
		}
	}
	return false
}

// SchedulerMetrics is the metrics for scheduler.
type SchedulerMetrics struct {
	totalJobsStarted   meter.Gauge
	totalJobsFinished  meter.Gauge
	totalTasksStarted  meter.Gauge
	totalTasksFinished meter.Gauge
	totalTasksPanic    meter.Gauge
	totalTaskLatency   meter.Gauge
}

// NewSchedulerMetrics creates a new scheduler metrics.
func NewSchedulerMetrics(factory observability.Factory) *SchedulerMetrics {
	return &SchedulerMetrics{
		totalJobsStarted:   factory.NewGauge("scheduler_jobs_started", "job"),
		totalJobsFinished:  factory.NewGauge("scheduler_jobs_finished", "job"),
		totalTasksStarted:  factory.NewGauge("scheduler_tasks_started", "job"),
		totalTasksFinished: factory.NewGauge("scheduler_tasks_finished", "job"),
		totalTasksPanic:    factory.NewGauge("scheduler_tasks_panic", "job"),
		totalTaskLatency:   factory.NewGauge("scheduler_task_latency", "job"),
	}
}

// Collect collects the scheduler metrics.
func (sm *SchedulerMetrics) Collect(job string, m *timestamp.SchedulerMetrics) {
	sm.totalJobsStarted.Set(float64(m.TotalJobsStarted.Load()), job)
	sm.totalJobsFinished.Set(float64(m.TotalJobsFinished.Load()), job)
	sm.totalTasksStarted.Set(float64(m.TotalTasksStarted.Load()), job)
	sm.totalTasksFinished.Set(float64(m.TotalTasksFinished.Load()), job)
	sm.totalTasksPanic.Set(float64(m.TotalTasksPanic.Load()), job)
	sm.totalTaskLatency.Set(float64(m.TotalTaskLatencyInNanoseconds.Load())/float64(time.Second), job)
}

func (p *metricService) handleDebugPanic(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	component := r.URL.Query().Get("component")
	if component == "" {
		component = "debug-panic"
	}
	dumper := panicdiag.StateDumperFunc(func(_ context.Context) (any, error) {
		return map[string]any{
			"listenAddr":  p.listenAddr,
			"nodeType":    p.nodeType,
			"modes":       p.modes,
			"component":   component,
			"artifactDir": p.panicArtifactDir,
		}, nil
	})
	panicCtx := panicdiag.WithMutableBreadcrumbs(r.Context())
	panicdiag.GoWithRecovery(panicCtx, panicdiag.RecoveryOptions{
		Component:   component,
		Logger:      p.l,
		StateDumper: dumper,
		ProcessMetadata: map[string]string{
			"listenAddr":  p.listenAddr,
			"nodeType":    p.nodeType,
			"component":   component,
			"artifactDir": p.panicArtifactDir,
		},
	}, nil, func(ctx *context.Context) {
		panicdiag.WithBreadcrumb(*ctx, "received debug panic request", component, map[string]string{
			"method": r.Method,
			"path":   r.URL.Path,
		})
		panicdiag.WithBreadcrumb(*ctx, "trigger debug panic", component, nil)
		panic("diagnostic test panic triggered via /debug/panic")
	})
	w.WriteHeader(http.StatusAccepted)
	_, _ = w.Write([]byte("panic triggered; check --panic-artifact-dir for artifacts\n"))
}

func (p *metricService) With(scope meter.Scope) observability.Factory {
	var promProvider, nativeProvider meter.Provider
	if containsMode(p.modes, flagPromethusMode) {
		promProvider = prom.NewProvider(scope, p.promReg)
	}
	if containsMode(p.modes, flagNativeMode) {
		nativeProvider = p.npf.provider(scope)
	}
	return NewFactory(promProvider, nativeProvider, p.nCollection)
}
