// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package cgroup

import (
	"context"

	"github.com/gitpod-io/gitpod/common-go/cgroups"
	"github.com/gitpod-io/gitpod/common-go/log"
	"github.com/gitpod-io/gitpod/ws-daemon/pkg/dispatch"
	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/xerrors"
)

func NewPluginHost(cgroupBasePath string, plugins ...Plugin) (*PluginHost, error) {
	var version Version
	unified, err := cgroups.IsUnifiedCgroupSetup()
	if err != nil {
		return nil, xerrors.Errorf("could not determine cgroup setup: %w", err)
	}
	if unified {
		version = Version2
	} else {
		version = Version1
	}

	return &PluginHost{
		CGroupBasePath: cgroupBasePath,
		CGroupVersion:  version,
		Plugins:        plugins,

		pluginActivationTotalVec: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: "cgroup_plugin_activation_total",
			Help: "counts the total activation of cgroup plugins",
		}, []string{"plugin", "success"}),
	}, nil
}

type PluginHost struct {
	CGroupBasePath string
	CGroupVersion  Version
	Plugins        []Plugin

	pluginActivationTotalVec *prometheus.CounterVec
}

var _ dispatch.Listener = &PluginHost{}
var _ prometheus.Collector = &PluginHost{}

func (host *PluginHost) Describe(c chan<- *prometheus.Desc) {
	host.pluginActivationTotalVec.Describe(c)
	for _, p := range host.Plugins {
		col, ok := p.(prometheus.Collector)
		if !ok {
			continue
		}

		col.Describe(c)
	}
}

func (host *PluginHost) Collect(c chan<- prometheus.Metric) {
	host.pluginActivationTotalVec.Collect(c)
	for _, p := range host.Plugins {
		col, ok := p.(prometheus.Collector)
		if !ok {
			continue
		}

		col.Collect(c)
	}
}

func (host *PluginHost) WorkspaceAdded(ctx context.Context, ws *dispatch.Workspace) (err error) {
	disp := dispatch.GetFromContext(ctx)
	if disp == nil {
		return xerrors.Errorf("no dispatch available")
	}

	cgroupPath, err := disp.Runtime.ContainerCGroupPath(context.Background(), ws.ContainerID)
	if err != nil {
		return xerrors.Errorf("cannot get cgroup path for container %s: %w", ws.ContainerID, err)
	}

	opts := &PluginOptions{
		BasePath:    host.CGroupBasePath,
		CgroupPath:  cgroupPath,
		InstanceId:  ws.InstanceID,
		Annotations: ws.Pod.Annotations,
	}

	for _, plg := range host.Plugins {
		if plg.Type() != host.CGroupVersion {
			continue
		}

		go func(plg Plugin) {
			err := plg.Apply(ctx, opts)
			if err == context.Canceled || err == context.DeadlineExceeded {
				err = nil
			}
			if err != nil {
				log.WithError(err).WithFields(ws.OWI()).WithField("plugin", plg.Name()).Error("cgroup plugin failure")
				host.pluginActivationTotalVec.WithLabelValues(plg.Name(), "false").Inc()
			} else {
				host.pluginActivationTotalVec.WithLabelValues(plg.Name(), "true").Inc()
			}
		}(plg)
	}

	return nil
}

type Plugin interface {
	Name() string
	Type() Version
	Apply(ctx context.Context, options *PluginOptions) error
}

type Version int

const (
	Version1 Version = iota
	Version2
)

type PluginOptions struct {
	BasePath    string
	CgroupPath  string
	InstanceId  string
	Annotations map[string]string
}
