package docker

import (
	"context"
	"fmt"
	"github.com/Clymene-project/Clymene/cmd/agent/app/discovery"
	"github.com/Clymene-project/Clymene/cmd/promtail/app/api"
	"github.com/Clymene-project/Clymene/cmd/promtail/app/logentry/stages"
	"github.com/Clymene-project/Clymene/cmd/promtail/app/positions"
	"github.com/Clymene-project/Clymene/cmd/promtail/app/scrapeconfig"
	"github.com/Clymene-project/Clymene/cmd/promtail/app/targets/target"
	util "github.com/Clymene-project/Clymene/pkg/lokiutil"
	"go.uber.org/zap"

	"github.com/prometheus/common/model"
)

const (
	// See github.com/prometheus/prometheus/discovery/moby
	dockerLabel                = model.MetaLabelPrefix + "docker_"
	dockerLabelContainerPrefix = dockerLabel + "container_"
	dockerLabelContainerID     = dockerLabelContainerPrefix + "id"
	dockerLabelLogStream       = dockerLabelContainerPrefix + "_log_stream"
)

type TargetManager struct {
	metrics    *Metrics
	logger     *zap.Logger
	positions  positions.Positions
	cancel     context.CancelFunc
	done       chan struct{}
	manager    *discovery.Manager
	pushClient api.EntryHandler
	groups     map[string]*targetGroup
}

func NewTargetManager(
	metrics *Metrics,
	logger *zap.Logger,
	positions positions.Positions,
	pushClient api.EntryHandler,
	scrapeConfigs []scrapeconfig.Config,
) (*TargetManager, error) {
	ctx, cancel := context.WithCancel(context.Background())
	tm := &TargetManager{
		metrics:    metrics,
		logger:     logger,
		cancel:     cancel,
		done:       make(chan struct{}),
		positions:  positions,
		manager:    discovery.NewManager(ctx, logger.With(zap.String("component", "docker_discovery"))),
		pushClient: pushClient,
		groups:     make(map[string]*targetGroup),
	}
	configs := map[string]discovery.Configs{}
	for _, cfg := range scrapeConfigs {
		if cfg.DockerSDConfigs != nil {
			pipeline, err := stages.NewPipeline(
				logger.With(zap.String("component", "docker_pipeline")),
				cfg.PipelineStages,
				&cfg.JobName,
				metrics.reg,
			)
			if err != nil {
				return nil, err
			}

			for _, sdConfig := range cfg.DockerSDConfigs {
				syncerKey := fmt.Sprintf("%s/%s:%d", cfg.JobName, sdConfig.Host, sdConfig.Port)
				_, ok := tm.groups[syncerKey]
				if !ok {
					tm.groups[syncerKey] = &targetGroup{
						metrics:       metrics,
						logger:        logger,
						positions:     positions,
						targets:       make(map[string]*Target),
						entryHandler:  pipeline.Wrap(pushClient),
						defaultLabels: model.LabelSet{},
						relabelConfig: cfg.RelabelConfigs,
						host:          sdConfig.Host,
					}
				}
				configs[syncerKey] = append(configs[syncerKey], sdConfig)
			}
		} else {
			tm.logger.Debug("Docker service discovery configs are empty")
		}
	}

	go tm.run(ctx)
	go util.LogError("running target manager", logger, tm.manager.Run)

	return tm, tm.manager.ApplyConfig(configs)
}

// run listens on the service discovery and adds new targets.
func (tm *TargetManager) run(ctx context.Context) {
	defer close(tm.done)
	for {
		select {
		case targetGroups := <-tm.manager.SyncCh():
			for jobName, groups := range targetGroups {
				tg, ok := tm.groups[jobName]
				if !ok {
					tm.logger.Debug("unknown target for job", zap.String("job", jobName))
					continue
				}
				tg.sync(groups)
			}
		case <-ctx.Done():
			return
		}
	}
}

// Ready returns true if at least one Docker target is active.
func (tm *TargetManager) Ready() bool {
	for _, s := range tm.groups {
		if s.Ready() {
			return true
		}
	}
	return false
}

func (tm *TargetManager) Stop() {
	tm.cancel()
	<-tm.done
	for _, s := range tm.groups {
		s.Stop()
	}
}

func (tm *TargetManager) ActiveTargets() map[string][]target.Target {
	result := make(map[string][]target.Target, len(tm.groups))
	for k, s := range tm.groups {
		result[k] = s.ActiveTargets()
	}
	return result
}

func (tm *TargetManager) AllTargets() map[string][]target.Target {
	result := make(map[string][]target.Target, len(tm.groups))
	for k, s := range tm.groups {
		result[k] = s.AllTargets()
	}
	return result
}
