// Copyright IBM Corp. 2015, 2025
// SPDX-License-Identifier: MPL-2.0

package executor

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"

	"github.com/golang/protobuf/ptypes"
	hclog "github.com/hashicorp/go-hclog"
	plugin "github.com/hashicorp/go-plugin"
	"github.com/hashicorp/nomad/client/lib/cpustats"
	"github.com/hashicorp/nomad/drivers/shared/executor/proto"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/plugins/base"
)

const (
	// ExecutorDefaultMaxPort is the default max port used by the executor for
	// searching for an available port
	ExecutorDefaultMaxPort = 14512

	// ExecutorDefaultMinPort is the default min port used by the executor for
	// searching for an available port
	ExecutorDefaultMinPort = 14000
)

// CreateExecutor launches an executor plugin and returns an instance of the
// Executor interface
func CreateExecutor(
	logger hclog.Logger,
	driverConfig *base.ClientDriverConfig,
	executorConfig *ExecutorConfig,
) (Executor, *plugin.Client, error) {

	c, err := json.Marshal(executorConfig)
	if err != nil {
		return nil, nil, fmt.Errorf("unable to create executor config: %v", err)
	}
	bin, err := os.Executable()
	if err != nil {
		return nil, nil, fmt.Errorf("unable to find the nomad binary: %v", err)
	}

	p := &ExecutorPlugin{
		logger:      logger,
		fsIsolation: executorConfig.FSIsolation,
		compute:     driverConfig.Topology.Compute(),
	}

	config := &plugin.ClientConfig{
		HandshakeConfig:  base.Handshake,
		Plugins:          map[string]plugin.Plugin{"executor": p},
		Cmd:              exec.Command(bin, "executor", string(c)),
		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
		Logger:           logger.Named("executor"),
	}

	if driverConfig != nil {
		config.MaxPort = driverConfig.ClientMaxPort
		config.MinPort = driverConfig.ClientMinPort
	} else {
		config.MaxPort = ExecutorDefaultMaxPort
		config.MinPort = ExecutorDefaultMinPort
	}

	// setting the setsid of the plugin process so that it doesn't get signals sent to
	// the nomad client.
	if config.Cmd != nil {
		isolateCommand(config.Cmd)
	}

	return newExecutorClient(config, logger)
}

// ReattachToExecutor launches a plugin with a given plugin config and validates it can call the executor.
// Note: On Windows, go-plugin listens on a localhost port. It is possible on a reboot that another process
// is listening on that port, and a process is running with the previous executors PID, leading the Nomad
// TaskRunner to kill the PID after it errors calling the Wait RPC. So, fail early via the Version RPC if
// we detect the listener isn't actually an Executor.
func ReattachToExecutor(reattachConfig *plugin.ReattachConfig, logger hclog.Logger, compute cpustats.Compute) (Executor, *plugin.Client, error) {
	config := &plugin.ClientConfig{
		HandshakeConfig:  base.Handshake,
		Reattach:         reattachConfig,
		Plugins:          GetPluginMap(logger, false, compute),
		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
		Logger:           logger.Named("executor"),
	}
	exec, pluginClient, err := newExecutorClient(config, logger)
	if err != nil {
		return nil, nil, err
	}
	if _, err := exec.Version(); err != nil {
		return nil, nil, err
	}
	return exec, pluginClient, nil
}

func newExecutorClient(config *plugin.ClientConfig, logger hclog.Logger) (Executor, *plugin.Client, error) {
	executorClient := plugin.NewClient(config)
	rpcClient, err := executorClient.Client()
	if err != nil {
		return nil, nil, fmt.Errorf("error creating rpc client for executor plugin: %v", err)
	}

	raw, err := rpcClient.Dispense("executor")
	if err != nil {
		return nil, nil, fmt.Errorf("unable to dispense the executor plugin: %v", err)
	}
	executorPlugin, ok := raw.(Executor)
	if !ok {
		return nil, nil, fmt.Errorf("unexpected executor rpc type: %T", raw)
	}
	return executorPlugin, executorClient, nil
}

func processStateToProto(ps *ProcessState) (*proto.ProcessState, error) {
	timestamp, err := ptypes.TimestampProto(ps.Time)
	if err != nil {
		return nil, err
	}
	pb := &proto.ProcessState{
		Pid:       int32(ps.Pid),
		ExitCode:  int32(ps.ExitCode),
		Signal:    int32(ps.Signal),
		OomKilled: ps.OOMKilled,
		Time:      timestamp,
	}

	return pb, nil
}

func processStateFromProto(pb *proto.ProcessState) (*ProcessState, error) {
	timestamp, err := ptypes.Timestamp(pb.Time)
	if err != nil {
		return nil, err
	}

	return &ProcessState{
		Pid:       int(pb.Pid),
		ExitCode:  int(pb.ExitCode),
		Signal:    int(pb.Signal),
		OOMKilled: pb.OomKilled,
		Time:      timestamp,
	}, nil
}

// IsolationMode returns the namespace isolation mode as determined from agent
// plugin configuration and task driver configuration. The task configuration
// takes precedence, if it is configured.
func IsolationMode(plugin, task string) string {
	if task != "" {
		return task
	}
	return plugin
}

const (
	// MemoryNoLimit is a sentinel value for resources.memory_max that indicates
	// the driver should not enforce a maximum memory limit and treat
	// resources.memory as a reservation. For Linux drivers that implement
	// memory allocation via cgroups, this means setting `memory.max = max` and
	// `memory.low` to the value of resources.memory (multiplied to bytes)
	MemoryNoLimit = -1
)

func mbToBytes(n int64) int64 {
	return n * 1024 * 1024
}

// memoryLimits returns the hard and soft memory limits for the task The
// resources.memory field in the jobspec is normally interpreted as a hard limit
// (cgroup memory.max). If oversubscription is enabled and resources.memory_max
// is set, resources.memory_max is treated as the hard limit and
// resources.memory is the reserve (cgroup memory.low). If resources.memory_max
// = -1, there is no hard limit.
//
// Returns (memory (hard) and reservation (soft)) values in bytes.
func memoryLimits(memory structs.AllocatedMemoryResources) (int64, int64) {
	memHard, memReserved := memory.MemoryMaxMB, memory.MemoryMB

	if memHard == MemoryNoLimit {
		// special oversub case where 'memory' is soft limit and there is no
		// hard limit
		return -1, mbToBytes(memReserved)
	}
	if memHard <= 0 {
		// typical case where 'memory' is the hard limit
		memHard = memReserved
		memReserved = 0
	}
	return mbToBytes(memHard), mbToBytes(memReserved)
}
