//go:build !windows

package metrics

import (
	"context"
	"net"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/containerd/log"
	"github.com/docker/docker/pkg/plugingetter"
	"github.com/docker/docker/pkg/plugins"
	"github.com/docker/docker/plugin"
	gometrics "github.com/docker/go-metrics"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
)

const pluginType = "MetricsCollector"

// Plugin represents a metrics collector plugin
type Plugin interface {
	StartMetrics() error
	StopMetrics() error
}

type metricsPluginAdapter struct {
	client *plugins.Client
}

func (a *metricsPluginAdapter) StartMetrics() error {
	return a.client.Call("/MetricsCollector.StartMetrics", nil, nil)
}

func (a *metricsPluginAdapter) StopMetrics() error {
	return a.client.Call("/MetricsCollector.StopMetrics", nil, nil)
}

func makePluginAdapter(p plugingetter.CompatPlugin) (Plugin, error) {
	adapted := p.Client()
	return &metricsPluginAdapter{adapted}, nil
}

// RegisterPlugin starts the metrics server listener and registers the metrics plugin
// callback with the plugin store
func RegisterPlugin(store *plugin.Store, path string) error {
	if err := listen(path); err != nil {
		return err
	}

	store.RegisterRuntimeOpt(pluginType, func(s *specs.Spec) {
		f := plugin.WithSpecMounts([]specs.Mount{
			{Type: "bind", Source: path, Destination: "/run/docker/metrics.sock", Options: []string{"bind", "ro"}},
		})
		f(s)
	})
	store.Handle(pluginType, func(name string, client *plugins.Client) {
		// Use lookup since nothing in the system can really reference it, no need
		// to protect against removal
		p, err := store.Get(name, pluginType, plugingetter.Lookup)
		if err != nil {
			return
		}

		adapter, err := makePluginAdapter(p)
		if err != nil {
			log.G(context.TODO()).WithError(err).WithField("plugin", p.Name()).Error("Error creating plugin adapter")
		}
		if err := adapter.StartMetrics(); err != nil {
			log.G(context.TODO()).WithError(err).WithField("plugin", p.Name()).Error("Error starting metrics collector plugin")
		}
	})

	return nil
}

// CleanupPlugin stops metrics collection for all plugins
func CleanupPlugin(store plugingetter.PluginGetter) {
	ls := store.GetAllManagedPluginsByCap(pluginType)
	var wg sync.WaitGroup
	wg.Add(len(ls))

	for _, plugin := range ls {
		p := plugin
		go func() {
			defer wg.Done()

			adapter, err := makePluginAdapter(p)
			if err != nil {
				log.G(context.TODO()).WithError(err).WithField("plugin", p.Name()).Error("Error creating metrics plugin adapter")
				return
			}
			if err := adapter.StopMetrics(); err != nil {
				log.G(context.TODO()).WithError(err).WithField("plugin", p.Name()).Error("Error stopping plugin metrics collection")
			}
		}()
	}
	wg.Wait()

	if listener != nil {
		_ = listener.Close()
	}
}

var listener net.Listener

func listen(path string) error {
	_ = os.Remove(path)
	l, err := net.Listen("unix", path)
	if err != nil {
		return errors.Wrap(err, "error setting up metrics plugin listener")
	}

	mux := http.NewServeMux()
	mux.Handle("/metrics", gometrics.Handler())
	go func() {
		log.G(context.TODO()).Debugf("metrics API listening on %s", l.Addr())
		srv := &http.Server{
			Handler:           mux,
			ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout.
		}
		if err := srv.Serve(l); err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
			log.G(context.TODO()).WithError(err).Error("error serving metrics API")
		}
	}()
	listener = l
	return nil
}
