// config.go
//
// This source file is part of the FoundationDB open source project
//
// Copyright 2021 Apple Inc. and the FoundationDB project authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package api

import (
	"fmt"
	"net"
	"os"
	"strconv"
	"strings"
)

// ProcessConfiguration models the configuration for starting a FoundationDB
// process.
type ProcessConfiguration struct {
	// Version provides the version of FoundationDB the process should run.
	Version string `json:"version"`

	// RunServers defines whether we should run the server processes.
	// This defaults to true, but you can set it to false to prevent starting
	// new fdbserver processes.
	RunServers *bool `json:"runServers,omitempty"`

	// BinaryPath provides the path to the binary to launch.
	BinaryPath string `json:"-"`

	// Arguments provides the arguments to the process.
	Arguments []Argument `json:"arguments,omitempty"`
}

// Argument defines an argument to the process.
type Argument struct {
	// ArgumentType determines how the value is generated.
	ArgumentType ArgumentType `json:"type,omitempty"`

	// Value provides the value for a Literal type argument.
	Value string `json:"value,omitempty"`

	// Values provides the sub-values for a Concatenate type argument.
	Values []Argument `json:"values,omitempty"`

	// Source provides the name of the environment variable to use for an
	// Environment type argument.
	Source string `json:"source,omitempty"`

	// Multiplier provides a multiplier for the process number for ProcessNumber
	// type arguments.
	Multiplier int `json:"multiplier,omitempty"`

	// Offset provides an offset to add to the process number for ProcessNumber
	// type arguments.
	Offset int `json:"offset,omitempty"`

	// IPFamily provides the family to use for IPList type arguments.
	IPFamily int `json:"ipFamily,omitempty"`
}

// ArgumentType defines the types for arguments.
type ArgumentType string

const (
	// LiteralArgumentType defines an argument with a literal string value.
	LiteralArgumentType ArgumentType = "Literal"

	// ConcatenateArgumentType defines an argument composed of other arguments.
	ConcatenateArgumentType = "Concatenate"

	// EnvironmentArgumentType defines an argument that is pulled from an
	// environment variable.
	EnvironmentArgumentType = "Environment"

	// ProcessNumberArgumentType defines an argument that is calculated using
	// the number of the process in the process list.
	ProcessNumberArgumentType = "ProcessNumber"

	// IPListArgumentType defines an argument that is a comma-separated list of
	// IP addresses, provided through an environment variable.
	IPListArgumentType = "IPList"
)

// GenerateArgument processes an argument and generates its string
// representation.
func (argument Argument) GenerateArgument(processNumber int, env map[string]string) (string, error) {
	switch argument.ArgumentType {
	case "":
		fallthrough
	case LiteralArgumentType:
		return argument.Value, nil
	case ConcatenateArgumentType:
		concatenated := ""
		for _, childArgument := range argument.Values {
			childValue, err := childArgument.GenerateArgument(processNumber, env)
			if err != nil {
				return "", err
			}
			concatenated += childValue
		}
		return concatenated, nil
	case ProcessNumberArgumentType:
		number := processNumber
		if argument.Multiplier != 0 {
			number = number * argument.Multiplier
		}
		number = number + argument.Offset
		return strconv.Itoa(number), nil
	case EnvironmentArgumentType, IPListArgumentType:
		return argument.LookupEnv(env)
	default:
		return "", fmt.Errorf("unsupported argument type %s", argument.ArgumentType)
	}
}

// LookupEnv looks up the value for an argument from the environment.
func (argument Argument) LookupEnv(env map[string]string) (string, error) {
	var value string
	var present bool
	if env != nil {
		value, present = env[argument.Source]
	}
	if !present {
		value, present = os.LookupEnv(argument.Source)
	}
	if !present {
		return "", fmt.Errorf("missing environment variable %s", argument.Source)
	}

	if argument.ArgumentType == IPListArgumentType {
		ips := strings.Split(value, ",")
		for _, ipString := range ips {
			ip := net.ParseIP(ipString)
			if ip == nil {
				continue
			}
			switch argument.IPFamily {
			case 4:
				if ip.To4() != nil {
					return ipString, nil
				}
			case 6:
				if ip.To16() != nil && ip.To4() == nil {
					return ipString, nil
				}
			default:
				return "", fmt.Errorf("unsupported IP family %d", argument.IPFamily)
			}
		}
		return "", fmt.Errorf("could not find IP with family %d", argument.IPFamily)
	}
	return value, nil
}

// GenerateArguments interprets the arguments in the process configuration and
// generates a command invocation.
func (configuration *ProcessConfiguration) GenerateArguments(processNumber int, env map[string]string) ([]string, error) {
	results := make([]string, 0, len(configuration.Arguments)+1)
	if configuration.BinaryPath != "" {
		results = append(results, configuration.BinaryPath)
	}
	for _, argument := range configuration.Arguments {
		result, err := argument.GenerateArgument(processNumber, env)
		if err != nil {
			return nil, err
		}
		results = append(results, result)
	}
	return results, nil
}