// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package rpcapi

import (
	"context"
	"fmt"

	"github.com/hashicorp/go-plugin"
	svchost "github.com/hashicorp/terraform-svchost"
	"github.com/hashicorp/terraform-svchost/auth"
	"github.com/hashicorp/terraform-svchost/disco"
	"google.golang.org/grpc"

	"github.com/hashicorp/terraform/internal/command/cliconfig"
	pluginDiscovery "github.com/hashicorp/terraform/internal/plugin/discovery"
	"github.com/hashicorp/terraform/internal/rpcapi/dynrpcserver"
	"github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies"
	"github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages"
	"github.com/hashicorp/terraform/internal/rpcapi/terraform1/setup"
	"github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks"
)

type corePlugin struct {
	plugin.Plugin

	experimentsAllowed bool
}

func (p *corePlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
	// This codebase only provides a server implementation of this plugin.
	// Clients must live elsewhere.
	return nil, fmt.Errorf("there is no client implementation in this codebase")
}

func (p *corePlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
	generalOpts := &serviceOpts{
		experimentsAllowed: p.experimentsAllowed,
	}
	registerGRPCServices(s, generalOpts)
	return nil
}

func registerGRPCServices(s *grpc.Server, opts *serviceOpts) {
	// We initially only register the setup server, because the registration
	// of other services can vary depending on the capabilities negotiated
	// during handshake.
	server := newSetupServer(serverHandshake(s, opts))
	setup.RegisterSetupServer(s, server)
}

func serverHandshake(s *grpc.Server, opts *serviceOpts) func(context.Context, *setup.Handshake_Request, *stopper) (*setup.ServerCapabilities, error) {
	dependenciesStub := dynrpcserver.NewDependenciesStub()
	dependencies.RegisterDependenciesServer(s, dependenciesStub)
	stacksStub := dynrpcserver.NewStacksStub()
	stacks.RegisterStacksServer(s, stacksStub)
	packagesStub := dynrpcserver.NewPackagesStub()
	packages.RegisterPackagesServer(s, packagesStub)

	return func(ctx context.Context, request *setup.Handshake_Request, stopper *stopper) (*setup.ServerCapabilities, error) {
		// All of our servers will share a common handles table so that objects
		// can be passed from one service to another.
		handles := newHandleTable()

		// NOTE: This is intentionally not the same disco that "package main"
		// instantiates for Terraform CLI, because the RPC API is
		// architecturally independent from CLI despite being launched through
		// it, and so it is not subject to any ambient CLI configuration files
		// that might be in scope. If we later discover requirements for
		// callers to customize the service discovery settings, consider
		// adding new fields to terraform1.ClientCapabilities (even though
		// this isn't strictly a "capability") so that the RPC caller has
		// full control without needing to also tinker with the current user's
		// CLI configuration.
		services, err := newServiceDisco(request.GetConfig())
		if err != nil {
			return &setup.ServerCapabilities{}, err
		}

		// If handshaking is successful (which it currently always is, because
		// we don't have any special capabilities to negotiate yet) then we
		// will initialize all of the other services so the client can begin
		// doing real work. In future the details of what we register here
		// might vary based on the negotiated capabilities.
		dependenciesStub.ActivateRPCServer(newDependenciesServer(handles, services))
		stacksStub.ActivateRPCServer(newStacksServer(stopper, handles, services, opts))
		packagesStub.ActivateRPCServer(newPackagesServer(services))

		// If the client requested any extra capabililties that we're going
		// to honor then we should announce them in this result.
		return &setup.ServerCapabilities{}, nil
	}
}

// serviceOpts are options that could potentially apply to all of our
// individual RPC services.
//
// This could potentially be embedded inside a service-specific options
// structure, if needed.
type serviceOpts struct {
	experimentsAllowed bool
}

func newServiceDisco(config *setup.Config) (*disco.Disco, error) {
	// First, we'll try and load any credentials that might have been available
	// to the UI. It's perfectly fine if there are none so any errors we find
	// are from malformed credentials rather than missing ones.

	file, diags := cliconfig.LoadConfig()
	if diags.HasErrors() {
		return nil, fmt.Errorf("problem loading CLI configuration: %w", diags.ErrWithWarnings())
	}

	helperPlugins := pluginDiscovery.FindPlugins("credentials", cliconfig.GlobalPluginDirs())
	src, err := file.CredentialsSource(helperPlugins)
	if err != nil {
		return nil, fmt.Errorf("problem creating credentials source: %w", err)
	}
	services := disco.NewWithCredentialsSource(src)

	// Second, we'll side-load any credentials that might have been passed in.

	credSrc := services.CredentialsSource()
	if config != nil {
		for host, cred := range config.GetCredentials() {
			if err := credSrc.StoreForHost(svchost.Hostname(host), auth.HostCredentialsToken(cred.Token)); err != nil {
				return nil, fmt.Errorf("problem storing credential for host %s with: %w", host, err)
			}
		}
		services.SetCredentialsSource(credSrc)
	}

	return services, nil
}
