// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: MPL-2.0

package external_plugins

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"syscall"

	pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
	"github.com/hashicorp/go-plugin"
	"google.golang.org/grpc"
)

const (
	hostServicePluginSetName    = "host-plugin"
	storageServicePluginSetName = "storage-plugin"
)

// HandshakeConfig is a shared config that can be used regardless of plugin, to
// avoid having to know type-specific things about each plugin
var HandshakeConfig = plugin.HandshakeConfig{
	MagicCookieKey:   "HASHICORP_BOUNDARY_PLUGIN",
	MagicCookieValue: "boundary-plugin",
}

// ServeHostPlugin is a generic function to start serving a host plugin service as a
// plugin
func ServePlugin(svc any, opt ...Option) error {
	opts, err := getOpts(opt...)
	if err != nil {
		return err
	}

	signalCh := make(chan os.Signal, 1)
	signal.Notify(signalCh, syscall.SIGHUP)
	go func() {
		for {
			<-signalCh
		}
	}()

	plugins := make(map[string]plugin.Plugin)
	if hostSvc, ok := svc.(pb.HostPluginServiceServer); ok {
		hostServiceServer, err := NewHostPluginServiceServer(hostSvc)
		if err != nil {
			return err
		}
		plugins[hostServicePluginSetName] = hostServiceServer
	}
	if storageSvc, ok := svc.(pb.StoragePluginServiceServer); ok {
		storageServiceServer, err := NewStoragePluginServiceServer(storageSvc)
		if err != nil {
			return err
		}
		plugins[storageServicePluginSetName] = storageServiceServer
	}
	if len(plugins) == 0 {
		return errors.New("no valid plugin server provided")
	}

	plugin.Serve(&plugin.ServeConfig{
		HandshakeConfig: HandshakeConfig,
		VersionedPlugins: map[int]plugin.PluginSet{
			1: plugins,
		},
		Logger:     opts.withLogger,
		GRPCServer: plugin.DefaultGRPCServer,
	})
	return nil
}

func NewPluginClient(pluginPath, setName string, opt ...Option) (*plugin.Client, error) {
	opts, err := getOpts(opt...)
	if err != nil {
		return nil, err
	}

	var set plugin.PluginSet
	switch setName {
	case hostServicePluginSetName:
		set = plugin.PluginSet{hostServicePluginSetName: &hostPlugin{}}
	case storageServicePluginSetName:
		set = plugin.PluginSet{storageServicePluginSetName: &storagePlugin{}}
	}

	return plugin.NewClient(&plugin.ClientConfig{
		HandshakeConfig: HandshakeConfig,
		VersionedPlugins: map[int]plugin.PluginSet{
			1: set,
		},
		Cmd: exec.Command(pluginPath),
		AllowedProtocols: []plugin.Protocol{
			plugin.ProtocolGRPC,
		},
		Logger:   opts.withLogger,
		AutoMTLS: true,
	}), nil
}

type hostPlugin struct {
	plugin.Plugin

	impl pb.HostPluginServiceServer
}

func NewHostPluginServiceServer(impl pb.HostPluginServiceServer) (*hostPlugin, error) {
	if impl == nil {
		return nil, fmt.Errorf("empty underlying host plugin passed in")
	}
	return &hostPlugin{
		impl: impl,
	}, nil
}

func (h *hostPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
	pb.RegisterHostPluginServiceServer(s, h.impl)
	return nil
}

func (h *hostPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (any, error) {
	return pb.NewHostPluginServiceClient(c), nil
}

type storagePlugin struct {
	plugin.Plugin

	impl pb.StoragePluginServiceServer
}

func NewStoragePluginServiceServer(impl pb.StoragePluginServiceServer) (*storagePlugin, error) {
	if impl == nil {
		return nil, fmt.Errorf("empty underlying storage plugin passed in")
	}
	return &storagePlugin{
		impl: impl,
	}, nil
}

func (p *storagePlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
	pb.RegisterStoragePluginServiceServer(s, p.impl)
	return nil
}

func (p *storagePlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (any, error) {
	return pb.NewStoragePluginServiceClient(c), nil
}
