// SPDX-License-Identifier: GPL-3.0-or-later

package pipeline

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"time"

	"github.com/netdata/netdata/go/plugins/logger"
	"github.com/netdata/netdata/go/plugins/plugin/agent/discovery/sd/model"
	"github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
	"github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
)

type DiscovererFactory func(payload DiscovererPayload, source string) ([]model.Discoverer, error)

func New(cfg Config, makeDiscoverers DiscovererFactory) (*Pipeline, error) {
	if err := ValidateConfig(cfg); err != nil {
		return nil, err
	}
	if makeDiscoverers == nil {
		return nil, errors.New("discoverer factory is not set")
	}

	p := &Pipeline{
		Logger: logger.New().With(
			slog.String("component", "service discovery"),
			slog.String("pipeline", cfg.Name),
		),
		configDefaults: cfg.ConfigDefaults,
		accum:          newAccumulator(),
		discoverers:    make([]model.Discoverer, 0),
		configs:        make(map[string]map[uint64][]confgroup.Config),
	}

	p.accum.Logger = p.Logger

	svr, err := newServiceEngine(cfg.Services)
	if err != nil {
		return nil, fmt.Errorf("services rules: %v", err)
	}
	p.svr = svr
	svr.Logger = p.Logger

	if err := p.registerDiscoverers(cfg, makeDiscoverers); err != nil {
		return nil, err
	}

	return p, nil
}

type (
	Pipeline struct {
		*logger.Logger

		configDefaults confgroup.Registry
		discoverers    []model.Discoverer
		accum          *accumulator

		configs map[string]map[uint64][]confgroup.Config // [targetSource][targetHash]

		// new
		svr composer
	}
	composer interface {
		compose(model.Target) []confgroup.Config
	}
)

func (p *Pipeline) registerDiscoverers(conf Config, makeDiscoverers DiscovererFactory) error {
	discs, err := makeDiscoverers(conf.Discoverer, conf.Source)
	if err != nil {
		return fmt.Errorf("failed to create %q discoverer: %w", conf.Discoverer.Type(), err)
	}
	p.discoverers = append(p.discoverers, discs...)

	if len(p.discoverers) == 0 {
		return errors.New("no discoverers registered")
	}

	return nil
}

// Test runs the optional operational test of every configured discoverer.
// The bool reports whether every discoverer provided an operational test.
func (p *Pipeline) Test(ctx context.Context) (bool, error) {
	// Test is a public capability boundary and rejects invalid operands.
	if p == nil || ctx == nil {
		return false, errors.New("service discovery pipeline: invalid test")
	}
	if cause := context.Cause(ctx); cause != nil {
		return false, cause
	}
	fullyTested := len(p.discoverers) != 0
	for _, discoverer := range p.discoverers {
		testable, ok := discoverer.(dyncfg.Testable)
		if !ok {
			fullyTested = false
			continue
		}
		if cause := context.Cause(ctx); cause != nil {
			return false, cause
		}
		err := testable.Test(ctx)
		// Caller cancellation wins even when it races a resource failure.
		if cause := context.Cause(ctx); cause != nil {
			return false, cause
		}
		if errors.Is(err, dyncfg.ErrTestUnsupported) {
			fullyTested = false
			continue
		}
		if err != nil {
			return false, fmt.Errorf("discoverer operational test: %w", err)
		}
	}
	return fullyTested, nil
}

func (p *Pipeline) Run(ctx context.Context, in chan<- []*confgroup.Group) {
	p.Info("instance is started")
	defer p.Info("instance is stopped")

	p.accum.discoverers = p.discoverers

	updates := make(chan []model.TargetGroup)
	done := make(chan struct{})

	go func() { defer close(done); p.accum.run(ctx, updates) }()

	for {
		select {
		case <-ctx.Done():
			select {
			case <-done:
			case <-time.After(time.Second * 10):
			}
			return
		case <-done:
			return
		case tggs := <-updates:
			p.Debugf("received %d target groups", len(tggs))
			if cfggs := p.processGroups(tggs); len(cfggs) > 0 {
				select {
				case <-ctx.Done():
				case in <- cfggs: // FIXME: potentially stale configs if upstream cannot receive (blocking)
				}
			}
		}
	}
}

func (p *Pipeline) processGroups(tggs []model.TargetGroup) []*confgroup.Group {
	var groups []*confgroup.Group
	// updates come from the accumulator; this ensures that all groups have different sources
	for _, tgg := range tggs {
		p.Debugf("processing group '%s' with %d target(s)", tgg.Source(), len(tgg.Targets()))
		if v := p.processGroup(tgg); v != nil {
			groups = append(groups, v)
		}
	}
	return groups
}

func (p *Pipeline) processGroup(tgg model.TargetGroup) *confgroup.Group {
	if len(tgg.Targets()) == 0 {
		if _, ok := p.configs[tgg.Source()]; !ok {
			return nil
		}
		delete(p.configs, tgg.Source())

		return &confgroup.Group{Source: tgg.Source()}
	}

	targetsCache, ok := p.configs[tgg.Source()]
	if !ok {
		targetsCache = make(map[uint64][]confgroup.Config)
		p.configs[tgg.Source()] = targetsCache
	}

	var changed bool
	seen := make(map[uint64]bool)

	for _, tgt := range tgg.Targets() {
		if tgt == nil {
			continue
		}

		hash := tgt.Hash()
		seen[hash] = true

		if _, ok := targetsCache[hash]; ok {
			continue
		}

		targetsCache[hash] = nil

		if p.svr != nil {
			if cfgs := p.svr.compose(tgt); len(cfgs) > 0 {
				targetsCache[hash] = cfgs
				changed = true
				for _, cfg := range cfgs {
					cfg.SetProvider(tgg.Provider())
					cfg.SetSource(tgg.Source())
					cfg.SetSourceType(confgroup.TypeDiscovered)
					if def, ok := p.configDefaults.Lookup(cfg.Module()); ok {
						cfg.ApplyDefaults(def)
					}
				}
			}
			continue
		}
	}

	for hash := range targetsCache {
		if seen[hash] {
			continue
		}
		if cfgs := targetsCache[hash]; len(cfgs) > 0 {
			changed = true
		}
		delete(targetsCache, hash)
	}

	if !changed {
		return nil
	}

	// TODO: deepcopy?
	cfgGroup := &confgroup.Group{Source: tgg.Source()}

	for _, cfgs := range targetsCache {
		cfgGroup.Configs = append(cfgGroup.Configs, cfgs...)
	}

	return cfgGroup
}
