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

import (
	"context"
	"fmt"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/pkg/errors"

	"github.com/apache/skywalking-banyandb/api/common"
	"github.com/apache/skywalking-banyandb/api/data"
	commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
	databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
	"github.com/apache/skywalking-banyandb/banyand/internal/storage"
	"github.com/apache/skywalking-banyandb/banyand/metadata"
	"github.com/apache/skywalking-banyandb/banyand/observability"
	obsservice "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/pkg/fs"
	"github.com/apache/skywalking-banyandb/pkg/logger"
	banyandbpath "github.com/apache/skywalking-banyandb/pkg/path"
	"github.com/apache/skywalking-banyandb/pkg/run"
	resourceSchema "github.com/apache/skywalking-banyandb/pkg/schema"
	"github.com/apache/skywalking-banyandb/pkg/timestamp"
)

var (
	errEmptyRootPath = errors.New("root path is empty")
	// ErrMeasureNotExist denotes a measure doesn't exist in the metadata repo.
	ErrMeasureNotExist = errors.New("measure doesn't exist")
)

// Service allows inspecting the measure data points.
type Service interface {
	run.PreRunner
	run.Config
	run.Service
	Query
	CollectDataInfo(context.Context, string) (*databasev1.DataInfo, error)
	CollectLiaisonInfo(context.Context, string) (*databasev1.LiaisonInfo, error)
}

var _ Service = (*standalone)(nil)

type standalone struct {
	lfs                fs.FileSystem
	c                  storage.Cache
	pipeline           queue.Server
	localPipeline      queue.Queue
	metricPipeline     queue.Server
	omr                observability.MetricsRegistry
	metadata           metadata.Repo
	pm                 protector.Memory
	cm                 *cacheMetrics
	l                  *logger.Logger
	schemaRepo         *schemaRepo
	diskMonitor        *storage.DiskMonitor
	root               string
	snapshotDir        string
	dataPath           string
	option             option
	retentionConfig    storage.RetentionConfig
	cc                 storage.CacheConfig
	maxFileSnapshotNum int
	minFileSnapshotAge time.Duration
}

func (s *standalone) Measure(metadata *commonv1.Metadata) (Measure, error) {
	sm, ok := s.schemaRepo.loadMeasure(metadata)
	if !ok {
		return nil, errors.WithStack(ErrMeasureNotExist)
	}
	return sm, nil
}

func (s *standalone) LoadGroup(name string) (resourceSchema.Group, bool) {
	return s.schemaRepo.LoadGroup(name)
}

func (s *standalone) DropGroup(_ context.Context, groupName string) error {
	return s.schemaRepo.DropGroup(groupName)
}

func (s *standalone) GetRemovalSegmentsTimeRange(group string) *timestamp.TimeRange {
	return s.schemaRepo.GetRemovalSegmentsTimeRange(group)
}

// RetentionService interface implementation.

func (s *standalone) GetDataPath() string {
	return s.dataPath
}

func (s *standalone) GetSnapshotDir() string {
	return s.snapshotDir
}

func (s *standalone) LoadAllGroups() []resourceSchema.Group {
	return s.schemaRepo.LoadAllGroups()
}

func (s *standalone) PeekOldestSegmentEndTimeInGroup(group string) (time.Time, bool) {
	g, ok := s.schemaRepo.LoadGroup(group)
	if !ok {
		return time.Time{}, false
	}

	db := g.SupplyTSDB()
	if db == nil {
		return time.Time{}, false
	}

	// Type assert to the storage interface that has PeekOldestSegmentEndTime
	if dbWithPeek, ok := db.(interface{ PeekOldestSegmentEndTime() (time.Time, bool) }); ok {
		return dbWithPeek.PeekOldestSegmentEndTime()
	}

	return time.Time{}, false
}

func (s *standalone) DeleteOldestSegmentInGroup(group string) (bool, error) {
	g, ok := s.schemaRepo.LoadGroup(group)
	if !ok {
		return false, nil
	}

	db := g.SupplyTSDB()
	if db == nil {
		return false, nil
	}

	// Type assert to the storage interface that has DeleteOldestSegment
	if dbWithDelete, ok := db.(interface{ DeleteOldestSegment() (bool, error) }); ok {
		return dbWithDelete.DeleteOldestSegment()
	}

	s.l.Debug().Str("group", group).Msg("database does not support DeleteOldestSegment")
	return false, nil
}

func (s *standalone) CleanupOldSnapshots(maxAge time.Duration) error {
	if s.snapshotDir == "" {
		return nil
	}
	// Use age-based cleanup during forced retention cleanup
	storage.DeleteOldSnapshots(s.snapshotDir, maxAge, s.lfs)
	return nil
}

func (s *standalone) GetServiceName() string {
	return serviceName
}

func (s *standalone) FlagSet() *run.FlagSet {
	flagS := run.NewFlagSet("storage")
	flagS.StringVar(&s.root, "measure-root-path", "/tmp", "the root path of measure")
	flagS.StringVar(&s.dataPath, "measure-data-path", "", "the data directory path of measure. If not set, <measure-root-path>/measure/data will be used")
	flagS.DurationVar(&s.option.flushTimeout, "measure-flush-timeout", defaultFlushTimeout, "the memory data timeout of measure")
	s.option.mergePolicy = newDefaultMergePolicy()
	flagS.VarP(&s.option.mergePolicy.maxFanOutSize, "measure-max-fan-out-size", "", "the upper bound of a single file size after merge of measure")
	s.option.seriesCacheMaxSize = run.Bytes(32 << 20)
	flagS.VarP(&s.option.seriesCacheMaxSize, "measure-series-cache-max-size", "", "the max size of series cache in each group")

	// Retention configuration flags
	flagS.Float64Var(&s.retentionConfig.HighWatermark, "measure-retention-high-watermark", 95.0, "disk usage high watermark for forced retention cleanup")
	flagS.Float64Var(&s.retentionConfig.LowWatermark, "measure-retention-low-watermark", 85.0, "disk usage low watermark percentage where forced retention cleanup stops")
	flagS.DurationVar(&s.retentionConfig.CheckInterval, "measure-retention-check-interval", 5*time.Minute, "interval for checking disk usage")
	flagS.DurationVar(&s.retentionConfig.Cooldown, "measure-retention-cooldown", 30*time.Second, "cooldown period between forced segment deletions")
	flagS.BoolVar(&s.retentionConfig.ForceCleanupEnabled, "measure-retention-force-cleanup-enabled", false,
		"enable forced retention cleanup when disk usage exceeds high watermark")

	flagS.IntVar(&s.maxFileSnapshotNum, "measure-max-file-snapshot-num", 10, "the maximum number of file snapshots allowed")
	flagS.DurationVar(&s.minFileSnapshotAge, "measure-min-file-snapshot-age", time.Hour, "minimum age for file snapshots to be eligible for deletion")
	s.cc.MaxCacheSize = run.Bytes(100 * 1024 * 1024)
	flagS.VarP(&s.cc.MaxCacheSize, "service-cache-max-size", "", "maximum service cache size (e.g., 100M)")
	flagS.DurationVar(&s.cc.CleanupInterval, "service-cache-cleanup-interval", 30*time.Second, "service cache cleanup interval")
	flagS.DurationVar(&s.cc.IdleTimeout, "service-cache-idle-timeout", 2*time.Minute, "service cache entry idle timeout")
	return flagS
}

func (s *standalone) Validate() error {
	if s.root == "" {
		return errEmptyRootPath
	}

	// Validate retention configuration
	if s.retentionConfig.HighWatermark < 0 || s.retentionConfig.HighWatermark > 100 {
		return errors.New("measure-retention-high-watermark must be between 0 and 100")
	}
	if s.retentionConfig.LowWatermark < 0 || s.retentionConfig.LowWatermark > 100 {
		return errors.New("measure-retention-low-watermark must be between 0 and 100")
	}
	if s.retentionConfig.LowWatermark > s.retentionConfig.HighWatermark {
		return errors.New("measure-retention-low-watermark must be less than measure-retention-high-watermark")
	}
	if s.retentionConfig.CheckInterval <= 0 && s.retentionConfig.ForceCleanupEnabled {
		return errors.New("measure-retention-check-interval must be greater than 0 when force cleanup is enabled")
	}
	if s.retentionConfig.Cooldown <= 0 {
		return errors.New("measure-retention-cooldown must be greater than 0")
	}

	if s.cc.MaxCacheSize < 0 {
		return errors.New("service-cache-max-size must be greater than or equal to 0")
	}
	if s.cc.CleanupInterval <= 0 {
		return errors.New("service-cache-cleanup-interval must be greater than 0")
	}
	if s.cc.IdleTimeout <= 0 {
		return errors.New("service-cache-idle-timeout must be greater than 0")
	}
	return nil
}

func (s *standalone) Name() string {
	return serviceName
}

func (s *standalone) Role() databasev1.Role {
	return databasev1.Role_ROLE_DATA
}

func (s *standalone) PreRun(ctx context.Context) error {
	s.l = logger.GetLogger(s.Name())
	s.l.Info().Msg("memory protector is initialized in PreRun")
	s.lfs = fs.NewLocalFileSystemWithLoggerAndLimit(s.l, s.pm.GetLimit())
	var err error
	if s.root, err = banyandbpath.Get(s.root); err != nil {
		return err
	}
	if s.dataPath != "" {
		if s.dataPath, err = banyandbpath.Get(s.dataPath); err != nil {
			return err
		}
	}
	path := path.Join(s.root, s.Name())
	s.snapshotDir = filepath.Join(path, storage.SnapshotsDir)
	obsservice.UpdatePath(path)
	if s.dataPath == "" {
		s.dataPath = filepath.Join(path, storage.DataDir)
	}
	if !strings.HasPrefix(filepath.VolumeName(s.dataPath), filepath.VolumeName(path)) {
		obsservice.UpdatePath(s.dataPath)
	}
	s.localPipeline = queue.Local()
	val := ctx.Value(common.ContextNodeKey)
	if val == nil {
		return errors.New("node id is empty")
	}
	if s.cc.MaxCacheSize == 0 {
		s.c = storage.NewBypassCache()
	} else {
		s.c = storage.NewServiceCacheWithConfig(s.cc)
	}
	node := val.(common.Node)
	s.schemaRepo = newSchemaRepo(s.dataPath, s, node.Labels, node.NodeID)
	if metaSvc, ok := s.metadata.(metadata.Service); ok {
		metaSvc.RegisterDataCollector(commonv1.Catalog_CATALOG_MEASURE, s.schemaRepo)
		metaSvc.RegisterLiaisonCollector(commonv1.Catalog_CATALOG_MEASURE, s)
		metaSvc.RegisterGroupDropHandler(commonv1.Catalog_CATALOG_MEASURE, s)
	}

	s.cm = newCacheMetrics(s.omr)
	obsservice.MetricsCollector.Register("measure_cache", s.collectCacheMetrics)

	if s.pipeline == nil {
		return nil
	}

	if err = s.createNativeObservabilityGroup(ctx); err != nil {
		return err
	}

	if err = s.pipeline.Subscribe(data.TopicSnapshot, &snapshotListener{s: s}); err != nil {
		return err
	}

	// Initialize disk monitor for forced retention
	s.diskMonitor = storage.NewDiskMonitor(s, s.retentionConfig, s.omr)
	s.diskMonitor.Start()

	// For now, keep the original write throttling behavior based on high watermark
	writeListener := setUpWriteCallback(s.l, s.schemaRepo, int(s.retentionConfig.HighWatermark))
	// only subscribe metricPipeline for data node
	if s.metricPipeline != nil {
		err = s.metricPipeline.Subscribe(data.TopicMeasureWrite, writeListener)
		if err != nil {
			return err
		}
	}
	err = s.pipeline.Subscribe(data.TopicMeasureWrite, writeListener)
	if err != nil {
		return err
	}
	// Register chunked sync handler for measure data.
	s.pipeline.RegisterChunkedSyncHandler(data.TopicMeasurePartSync, setUpChunkedSyncCallback(s.l, s.schemaRepo))
	return s.localPipeline.Subscribe(data.TopicMeasureWrite, writeListener)
}

func (s *standalone) Serve() run.StopNotify {
	return s.schemaRepo.StopCh()
}

func (s *standalone) GracefulStop() {
	// Stop disk monitor
	if s.diskMonitor != nil {
		s.diskMonitor.Stop()
	}

	obsservice.MetricsCollector.Unregister("measure_cache")
	// Stop local pipeline first to prevent new metadata/write events creating processors during shutdown
	if s.localPipeline != nil {
		s.localPipeline.GracefulStop()
	}
	s.schemaRepo.Close()
	s.c.Close()
}

func (s *standalone) collectCacheMetrics() {
	if s.cm == nil || s.c == nil {
		return
	}

	requests := s.c.Requests()
	misses := s.c.Misses()
	length := s.c.Entries()
	size := s.c.Size()
	var hitRatio float64
	if requests > 0 {
		hitRatio = float64(requests-misses) / float64(requests)
	}

	s.cm.requests.Set(float64(requests))
	s.cm.misses.Set(float64(misses))
	s.cm.hitRatio.Set(hitRatio)
	s.cm.entries.Set(float64(length))
	s.cm.size.Set(float64(size))
}

// NewStandalone returns a new standalone service.
func NewStandalone(metadata metadata.Repo, pipeline queue.Server, metricPipeline queue.Server, omr observability.MetricsRegistry, pm protector.Memory) (Service, error) {
	return &standalone{
		metadata:       metadata,
		pipeline:       pipeline,
		metricPipeline: metricPipeline,
		omr:            omr,
		pm:             pm,
	}, nil
}

func (s *standalone) CollectDataInfo(ctx context.Context, group string) (*databasev1.DataInfo, error) {
	return s.schemaRepo.CollectDataInfo(ctx, group)
}

func (s *standalone) CollectLiaisonInfo(_ context.Context, group string) (*databasev1.LiaisonInfo, error) {
	info := &databasev1.LiaisonInfo{}
	pendingWriteCount, writeErr := s.schemaRepo.collectPendingWriteInfo(group)
	if writeErr != nil {
		return nil, fmt.Errorf("failed to collect pending write info: %w", writeErr)
	}
	info.PendingWriteDataCount = pendingWriteCount
	pendingSyncPartCount, pendingSyncDataSizeBytes, syncErr := s.schemaRepo.collectPendingSyncInfo(group)
	if syncErr != nil {
		return nil, fmt.Errorf("failed to collect pending sync info: %w", syncErr)
	}
	info.PendingSyncPartCount = pendingSyncPartCount
	info.PendingSyncDataSizeBytes = pendingSyncDataSizeBytes
	return info, nil
}
