// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package structs

import (
	"crypto/sha1"
	"encoding/binary"
	"errors"
	"fmt"
	"hash"
	"io"
	"net/url"
	"reflect"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/go-set"
	"github.com/hashicorp/nomad/helper"
	"github.com/hashicorp/nomad/helper/args"
	"github.com/hashicorp/nomad/helper/pointer"
	"github.com/mitchellh/copystructure"
	"golang.org/x/exp/maps"
	"golang.org/x/exp/slices"
)

const (
	EnvoyBootstrapPath = "${NOMAD_SECRETS_DIR}/envoy_bootstrap.json"

	ServiceCheckHTTP   = "http"
	ServiceCheckTCP    = "tcp"
	ServiceCheckScript = "script"
	ServiceCheckGRPC   = "grpc"

	OnUpdateRequireHealthy = "require_healthy"
	OnUpdateIgnoreWarn     = "ignore_warnings"
	OnUpdateIgnore         = "ignore"

	// minCheckInterval is the minimum check interval permitted.  Consul
	// currently has its MinInterval set to 1s.  Mirror that here for
	// consistency.
	minCheckInterval = 1 * time.Second

	// minCheckTimeout is the minimum check timeout permitted for Consul
	// script TTL checks.
	minCheckTimeout = 1 * time.Second
)

// ServiceCheck represents a Nomad or Consul service health check.
//
// The fields available depend on the service provider the check is being
// registered into.
type ServiceCheck struct {
	Name                   string              // Name of the check, defaults to a generated label
	Type                   string              // Type of the check - tcp, http, docker and script
	Command                string              // Command is the command to run for script checks
	Args                   []string            // Args is a list of arguments for script checks
	Path                   string              // path of the health check url for http type check
	Protocol               string              // Protocol to use if check is http, defaults to http
	PortLabel              string              // The port to use for tcp/http checks
	Expose                 bool                // Whether to have Envoy expose the check path (connect-enabled group-services only)
	AddressMode            string              // Must be empty, "alloc", "host", or "driver"
	Interval               time.Duration       // Interval of the check
	Timeout                time.Duration       // Timeout of the response from the check before consul fails the check
	InitialStatus          string              // Initial status of the check
	TLSServerName          string              // ServerName to use for SNI and TLS verification when (Type=https and Protocol=https) or (Type=grpc and GRPCUseTLS=true)
	TLSSkipVerify          bool                // Skip TLS verification when (type=https and Protocol=https) or (type=grpc and grpc_use_tls=true)
	Method                 string              // HTTP Method to use (GET by default)
	Header                 map[string][]string // HTTP Headers for Consul to set when making HTTP checks
	CheckRestart           *CheckRestart       // If and when a task should be restarted based on checks
	GRPCService            string              // Service for GRPC checks
	GRPCUseTLS             bool                // Whether or not to use TLS for GRPC checks
	TaskName               string              // What task to execute this check in
	SuccessBeforePassing   int                 // Number of consecutive successes required before considered healthy
	FailuresBeforeCritical int                 // Number of consecutive failures required before considered unhealthy
	Body                   string              // Body to use in HTTP check
	OnUpdate               string
}

// IsReadiness returns whether the configuration of the ServiceCheck is effectively
// a readiness check - i.e. check failures do not affect a deployment.
func (sc *ServiceCheck) IsReadiness() bool {
	return sc != nil && sc.OnUpdate == OnUpdateIgnore
}

// Copy the block recursively. Returns nil if nil.
func (sc *ServiceCheck) Copy() *ServiceCheck {
	if sc == nil {
		return nil
	}
	nsc := new(ServiceCheck)
	*nsc = *sc
	nsc.Args = slices.Clone(sc.Args)
	nsc.Header = helper.CopyMapOfSlice(sc.Header)
	nsc.CheckRestart = sc.CheckRestart.Copy()
	return nsc
}

// Equal returns true if the structs are recursively equal.
func (sc *ServiceCheck) Equal(o *ServiceCheck) bool {
	if sc == nil || o == nil {
		return sc == o
	}

	if sc.Name != o.Name {
		return false
	}

	if sc.AddressMode != o.AddressMode {
		return false
	}

	if !helper.SliceSetEq(sc.Args, o.Args) {
		return false
	}

	if !sc.CheckRestart.Equal(o.CheckRestart) {
		return false
	}

	if sc.TaskName != o.TaskName {
		return false
	}

	if sc.SuccessBeforePassing != o.SuccessBeforePassing {
		return false
	}

	if sc.FailuresBeforeCritical != o.FailuresBeforeCritical {
		return false
	}

	if sc.Command != o.Command {
		return false
	}

	if sc.GRPCService != o.GRPCService {
		return false
	}

	if sc.GRPCUseTLS != o.GRPCUseTLS {
		return false
	}

	// Use DeepEqual here as order of slice values could matter
	if !reflect.DeepEqual(sc.Header, o.Header) {
		return false
	}

	if sc.InitialStatus != o.InitialStatus {
		return false
	}

	if sc.Interval != o.Interval {
		return false
	}

	if sc.Method != o.Method {
		return false
	}

	if sc.Path != o.Path {
		return false
	}

	if sc.PortLabel != o.Path {
		return false
	}

	if sc.Expose != o.Expose {
		return false
	}

	if sc.Protocol != o.Protocol {
		return false
	}

	if sc.TLSSkipVerify != o.TLSSkipVerify {
		return false
	}

	if sc.TLSServerName != o.TLSServerName {
		return false
	}

	if sc.Timeout != o.Timeout {
		return false
	}

	if sc.Type != o.Type {
		return false
	}

	if sc.Body != o.Body {
		return false
	}

	if sc.OnUpdate != o.OnUpdate {
		return false
	}

	return true
}

func (sc *ServiceCheck) Canonicalize(serviceName, taskName string) {
	// Ensure empty maps/slices are treated as null to avoid scheduling
	// issues when using DeepEquals.
	if len(sc.Args) == 0 {
		sc.Args = nil
	}

	// Ensure empty slices are nil
	if len(sc.Header) == 0 {
		sc.Header = nil
	} else {
		for k, v := range sc.Header {
			if len(v) == 0 {
				sc.Header[k] = nil
			}
		}
	}

	// Ensure a default name for the check
	if sc.Name == "" {
		sc.Name = fmt.Sprintf("service: %q check", serviceName)
	}

	// Set task name if not already set
	if sc.TaskName == "" && taskName != "group" {
		sc.TaskName = taskName
	}

	// Ensure OnUpdate defaults to require_healthy (i.e. healthiness check)
	if sc.OnUpdate == "" {
		sc.OnUpdate = OnUpdateRequireHealthy
	}
}

// validateCommon validates the parts of ServiceCheck shared across providers.
func (sc *ServiceCheck) validateCommon(allowableTypes []string) error {
	// validate the type is allowable (different between nomad, consul checks)
	checkType := strings.ToLower(sc.Type)
	if !slices.Contains(allowableTypes, checkType) {
		s := strings.Join(allowableTypes, ", ")
		return fmt.Errorf(`invalid check type (%q), must be one of %s`, checkType, s)
	}

	// validate specific check types
	switch checkType {
	case ServiceCheckHTTP:
		if sc.Path == "" {
			return fmt.Errorf("http type must have http path")
		}
		checkPath, pathErr := url.Parse(sc.Path)
		if pathErr != nil {
			return fmt.Errorf("http type must have valid http path")
		}
		if checkPath.IsAbs() {
			return fmt.Errorf("http type must have relative http path")
		}
	case ServiceCheckScript:
		if sc.Command == "" {
			return fmt.Errorf("script type must have a valid script path")
		}
	}

	// validate interval
	if sc.Interval == 0 {
		return fmt.Errorf("missing required value interval. Interval cannot be less than %v", minCheckInterval)
	} else if sc.Interval < minCheckInterval {
		return fmt.Errorf("interval (%v) cannot be lower than %v", sc.Interval, minCheckInterval)
	}

	// validate timeout
	if sc.Timeout == 0 {
		return fmt.Errorf("missing required value timeout. Timeout cannot be less than %v", minCheckInterval)
	} else if sc.Timeout < minCheckTimeout {
		return fmt.Errorf("timeout (%v) is lower than required minimum timeout %v", sc.Timeout, minCheckInterval)
	}

	// validate the initial status
	switch sc.InitialStatus {
	case "":
	case api.HealthPassing:
	case api.HealthWarning:
	case api.HealthCritical:
	default:
		return fmt.Errorf(`invalid initial check state (%s), must be one of %q, %q, %q or empty`, sc.InitialStatus, api.HealthPassing, api.HealthWarning, api.HealthCritical)
	}

	// validate address_mode
	switch sc.AddressMode {
	case "", AddressModeHost, AddressModeDriver, AddressModeAlloc:
		// Ok
	case AddressModeAuto:
		return fmt.Errorf("invalid address_mode %q - %s only valid for services", sc.AddressMode, AddressModeAuto)
	default:
		return fmt.Errorf("invalid address_mode %q", sc.AddressMode)
	}

	// validate on_update
	switch sc.OnUpdate {
	case "", OnUpdateIgnore, OnUpdateRequireHealthy, OnUpdateIgnoreWarn:
		// OK
	default:
		return fmt.Errorf("on_update must be %q, %q, or %q; got %q", OnUpdateRequireHealthy, OnUpdateIgnoreWarn, OnUpdateIgnore, sc.OnUpdate)
	}

	// validate check_restart and on_update do not conflict
	if sc.CheckRestart != nil {
		// CheckRestart and OnUpdate Ignore are incompatible If OnUpdate treats
		// an error has healthy, and the deployment succeeds followed by check
		// restart restarting failing checks, the deployment is left in an odd
		// state
		if sc.OnUpdate == OnUpdateIgnore {
			return fmt.Errorf("on_update value %q is not compatible with check_restart", sc.OnUpdate)
		}
		// CheckRestart IgnoreWarnings must be true if a check has defined OnUpdate
		// ignore_warnings
		if !sc.CheckRestart.IgnoreWarnings && sc.OnUpdate == OnUpdateIgnoreWarn {
			return fmt.Errorf("on_update value %q not supported with check_restart ignore_warnings value %q", sc.OnUpdate, strconv.FormatBool(sc.CheckRestart.IgnoreWarnings))
		}
	}

	// validate check_restart
	if err := sc.CheckRestart.Validate(); err != nil {
		return err
	}

	return nil
}

// validate a Service's ServiceCheck in the context of the Nomad provider.
func (sc *ServiceCheck) validateNomad() error {
	allowable := []string{ServiceCheckTCP, ServiceCheckHTTP}
	if err := sc.validateCommon(allowable); err != nil {
		return err
	}

	// expose is connect (consul) specific
	if sc.Expose {
		return errors.New("expose may only be set for Consul service checks")
	}

	// nomad checks do not have warnings
	if sc.OnUpdate == OnUpdateIgnoreWarn {
		return errors.New("on_update may only be set to ignore_warnings for Consul service checks")
	}

	// below are temporary limitations on checks in nomad
	// https://github.com/hashicorp/team-nomad/issues/354

	// check_restart.ignore_warnings is not a thing in Nomad (which has no warnings in checks)
	if sc.CheckRestart != nil {
		if sc.CheckRestart.IgnoreWarnings {
			return errors.New("ignore_warnings on check_restart only supported for Consul service checks")
		}
	}

	// address_mode="driver" not yet supported on nomad
	if sc.AddressMode == "driver" {
		return errors.New("address_mode = driver may only be set for Consul service checks")
	}

	if sc.Type == "http" {
		if sc.Method != "" && !helper.IsMethodHTTP(sc.Method) {
			return fmt.Errorf("method type %q not supported in Nomad http check", sc.Method)
		}
	}

	// success_before_passing is consul only
	if sc.SuccessBeforePassing != 0 {
		return errors.New("success_before_passing may only be set for Consul service checks")
	}

	// failures_before_critical is consul only
	if sc.FailuresBeforeCritical != 0 {
		return errors.New("failures_before_critical may only be set for Consul service checks")
	}

	// tls_server_name is consul only
	if sc.TLSServerName != "" {
		return errors.New("tls_server_name may only be set for Consul service checks")
	}

	// tls_skip_verify is consul only
	if sc.TLSSkipVerify {
		return errors.New("tls_skip_verify may only be set for Consul service checks")
	}

	return nil
}

// validate a Service's ServiceCheck in the context of the Consul provider.
func (sc *ServiceCheck) validateConsul() error {
	allowable := []string{ServiceCheckGRPC, ServiceCheckTCP, ServiceCheckHTTP, ServiceCheckScript}
	if err := sc.validateCommon(allowable); err != nil {
		return err
	}

	checkType := strings.ToLower(sc.Type)

	// Note that we cannot completely validate the Expose field yet - we do not
	// know whether this ServiceCheck belongs to a connect-enabled group-service.
	// Instead, such validation will happen in a job admission controller.
	//
	// Consul only.
	if sc.Expose {
		// We can however immediately ensure expose is configured only for HTTP
		// and gRPC checks.
		switch checkType {
		case ServiceCheckGRPC, ServiceCheckHTTP: // ok
		default:
			return fmt.Errorf("expose may only be set on HTTP or gRPC checks")
		}
	}

	// passFailCheckTypes are intersection of check types supported by both Consul
	// and Nomad when using the pass/fail check threshold features.
	//
	// Consul only.
	passFailCheckTypes := []string{"tcp", "http", "grpc"}

	if sc.SuccessBeforePassing < 0 {
		return fmt.Errorf("success_before_passing must be non-negative")
	} else if sc.SuccessBeforePassing > 0 && !slices.Contains(passFailCheckTypes, sc.Type) {
		return fmt.Errorf("success_before_passing not supported for check of type %q", sc.Type)
	}

	if sc.FailuresBeforeCritical < 0 {
		return fmt.Errorf("failures_before_critical must be non-negative")
	} else if sc.FailuresBeforeCritical > 0 && !slices.Contains(passFailCheckTypes, sc.Type) {
		return fmt.Errorf("failures_before_critical not supported for check of type %q", sc.Type)
	}

	return nil
}

// RequiresPort returns whether the service check requires the task has a port.
func (sc *ServiceCheck) RequiresPort() bool {
	switch sc.Type {
	case ServiceCheckGRPC, ServiceCheckHTTP, ServiceCheckTCP:
		return true
	default:
		return false
	}
}

// TriggersRestarts returns true if this check should be watched and trigger a restart
// on failure.
func (sc *ServiceCheck) TriggersRestarts() bool {
	return sc.CheckRestart != nil && sc.CheckRestart.Limit > 0
}

// Hash all ServiceCheck fields and the check's corresponding service ID to
// create an identifier. The identifier is not guaranteed to be unique as if
// the PortLabel is blank, the Service's PortLabel will be used after Hash is
// called.
func (sc *ServiceCheck) Hash(serviceID string) string {
	h := sha1.New()
	hashString(h, serviceID)
	hashString(h, sc.Name)
	hashString(h, sc.Type)
	hashString(h, sc.Command)
	hashString(h, strings.Join(sc.Args, ""))
	hashString(h, sc.Path)
	hashString(h, sc.Protocol)
	hashString(h, sc.PortLabel)
	hashString(h, sc.Interval.String())
	hashString(h, sc.Timeout.String())
	hashString(h, sc.Method)
	hashString(h, sc.Body)
	hashString(h, sc.OnUpdate)

	// use name "true" to maintain ID stability
	hashBool(h, sc.TLSSkipVerify, "true")

	// Only include TLSServerName if set to maintain ID stability with Nomad <1.6.0
	hashStringIfNonEmpty(h, sc.TLSServerName)

	// maintain artisanal map hashing to maintain ID stability
	hashHeader(h, sc.Header)

	// Only include AddressMode if set to maintain ID stability with Nomad <0.7.1
	hashStringIfNonEmpty(h, sc.AddressMode)

	// Only include gRPC if set to maintain ID stability with Nomad <0.8.4
	hashStringIfNonEmpty(h, sc.GRPCService)

	// use name "true" to maintain ID stability
	hashBool(h, sc.GRPCUseTLS, "true")

	// Only include pass/fail if non-zero to maintain ID stability with Nomad < 0.12
	hashIntIfNonZero(h, "success", sc.SuccessBeforePassing)
	hashIntIfNonZero(h, "failures", sc.FailuresBeforeCritical)

	// Hash is used for diffing against the Consul check definition, which does
	// not have an expose parameter. Instead we rely on implied changes to
	// other fields if the Expose setting is changed in a nomad service.
	// hashBool(h, sc.Expose, "Expose")

	// maintain use of hex (i.e. not b32) to maintain ID stability
	return fmt.Sprintf("%x", h.Sum(nil))
}

func hashStringIfNonEmpty(h hash.Hash, s string) {
	if len(s) > 0 {
		hashString(h, s)
	}
}

func hashIntIfNonZero(h hash.Hash, name string, i int) {
	if i != 0 {
		hashString(h, fmt.Sprintf("%s:%d", name, i))
	}
}

func hashDuration(h hash.Hash, dur time.Duration) {
	_ = binary.Write(h, binary.LittleEndian, dur)
}

func hashHeader(h hash.Hash, m map[string][]string) {
	// maintain backwards compatibility for ID stability
	// using the %v formatter on a map with string keys produces consistent
	// output, but our existing format here is incompatible
	if len(m) > 0 {
		headers := make([]string, 0, len(m))
		for k, v := range m {
			headers = append(headers, k+strings.Join(v, ""))
		}
		sort.Strings(headers)
		hashString(h, strings.Join(headers, ""))
	}
}

const (
	AddressModeAuto   = "auto"
	AddressModeHost   = "host"
	AddressModeDriver = "driver"
	AddressModeAlloc  = "alloc"

	// ServiceProviderConsul is the default service provider and the way Nomad
	// worked before native service discovery.
	ServiceProviderConsul = "consul"

	// ServiceProviderNomad is the native service discovery provider. At the
	// time of writing, there are a number of restrictions around its
	// functionality and use.
	ServiceProviderNomad = "nomad"
)

// Service represents a Consul service definition
type Service struct {
	// Name of the service registered with Consul. Consul defaults the
	// Name to ServiceID if not specified.  The Name if specified is used
	// as one of the seed values when generating a Consul ServiceID.
	Name string

	// Name of the Task associated with this service.
	// Group services do not have a task name, unless they are a connect native
	// service specifying the task implementing the service.
	// Task-level services automatically have the task name plumbed through
	// down to checks for convenience.
	TaskName string

	// PortLabel is either the numeric port number or the `host:port`.
	// To specify the port number using the host's Consul Advertise
	// address, specify an empty host in the PortLabel (e.g. `:port`).
	PortLabel string

	// AddressMode specifies how the address in service registration is
	// determined. Must be "auto" (default), "host", "driver", or "alloc".
	AddressMode string

	// Address enables explicitly setting a custom address to use in service
	// registration. AddressMode must be "auto" if Address is set.
	Address string

	// EnableTagOverride will disable Consul's anti-entropy mechanism for the
	// tags of this service. External updates to the service definition via
	// Consul will not be corrected to match the service definition set in the
	// Nomad job specification.
	//
	// https://www.consul.io/docs/agent/services.html#service-definition
	EnableTagOverride bool

	Tags       []string          // List of tags for the service
	CanaryTags []string          // List of tags for the service when it is a canary
	Checks     []*ServiceCheck   // List of checks associated with the service
	Connect    *ConsulConnect    // Consul Connect configuration
	Meta       map[string]string // Consul service meta
	CanaryMeta map[string]string // Consul service meta when it is a canary

	// The values to set for tagged_addresses in Consul service registration.
	// Does not affect Nomad networking, these are for Consul service discovery.
	TaggedAddresses map[string]string

	// The consul namespace in which this service will be registered. Namespace
	// at the service.check level is not part of the Nomad API - it must be
	// set at the job or group level. This field is managed internally so
	// that Hash can work correctly.
	Namespace string

	// OnUpdate Specifies how the service and its checks should be evaluated
	// during an update
	OnUpdate string

	// Provider dictates which service discovery provider to use. This can be
	// either ServiceProviderConsul or ServiceProviderNomad and defaults to the former when
	// left empty by the operator.
	Provider string
}

// Copy the block recursively. Returns nil if nil.
func (s *Service) Copy() *Service {
	if s == nil {
		return nil
	}
	ns := new(Service)
	*ns = *s
	ns.Tags = slices.Clone(ns.Tags)
	ns.CanaryTags = slices.Clone(ns.CanaryTags)

	if s.Checks != nil {
		checks := make([]*ServiceCheck, len(ns.Checks))
		for i, c := range ns.Checks {
			checks[i] = c.Copy()
		}
		ns.Checks = checks
	}

	ns.Connect = s.Connect.Copy()

	ns.Meta = maps.Clone(s.Meta)
	ns.CanaryMeta = maps.Clone(s.CanaryMeta)
	ns.TaggedAddresses = maps.Clone(s.TaggedAddresses)

	return ns
}

// Canonicalize interpolates values of Job, Task Group and Task in the Service
// Name. This also generates check names, service id and check ids.
func (s *Service) Canonicalize(job, taskGroup, task, jobNamespace string) {
	// Ensure empty lists are treated as null to avoid scheduler issues when
	// using DeepEquals
	if len(s.Tags) == 0 {
		s.Tags = nil
	}
	if len(s.CanaryTags) == 0 {
		s.CanaryTags = nil
	}
	if len(s.Checks) == 0 {
		s.Checks = nil
	}
	if len(s.TaggedAddresses) == 0 {
		s.TaggedAddresses = nil
	}

	// Set the task name if not already set
	if s.TaskName == "" && task != "group" {
		s.TaskName = task
	}

	s.Name = args.ReplaceEnv(s.Name, map[string]string{
		"JOB":       job,
		"TASKGROUP": taskGroup,
		"TASK":      task,
		"BASE":      fmt.Sprintf("%s-%s-%s", job, taskGroup, task),
	})

	for _, check := range s.Checks {
		check.Canonicalize(s.Name, s.TaskName)
	}

	// Set the provider to its default value. The value of consul ensures this
	// new feature and parameter behaves in the same manner a previous versions
	// which did not include this.
	if s.Provider == "" {
		s.Provider = ServiceProviderConsul
	}

	// Consul API returns "default" whether the namespace is empty or set as
	// such, so we coerce our copy of the service to be the same if using the
	// consul provider.
	//
	// When using ServiceProviderNomad, set the namespace to that of the job. This
	// makes modifications and diffs on the service correct.
	if s.Namespace == "" && s.Provider == ServiceProviderConsul {
		s.Namespace = "default"
	} else if s.Provider == ServiceProviderNomad {
		s.Namespace = jobNamespace
	}
}

// Validate checks if the Service definition is valid
func (s *Service) Validate() error {
	var mErr multierror.Error

	// Ensure the service name is valid per the below RFCs but make an exception
	// for our interpolation syntax by first stripping any environment variables from the name

	serviceNameStripped := args.ReplaceEnvWithPlaceHolder(s.Name, "ENV-VAR")

	if err := s.ValidateName(serviceNameStripped); err != nil {
		// Log actual service name, not the stripped version.
		mErr.Errors = append(mErr.Errors, fmt.Errorf("%v: %q", err, s.Name))
	}

	switch s.AddressMode {
	case "", AddressModeAuto:
	case AddressModeHost, AddressModeDriver, AddressModeAlloc:
		if s.Address != "" {
			mErr.Errors = append(mErr.Errors, fmt.Errorf("Service address_mode must be %q if address is set", AddressModeAuto))
		}
	default:
		mErr.Errors = append(mErr.Errors, fmt.Errorf("Service address_mode must be %q, %q, or %q; not %q", AddressModeAuto, AddressModeHost, AddressModeDriver, s.AddressMode))
	}

	switch s.OnUpdate {
	case "", OnUpdateIgnore, OnUpdateRequireHealthy, OnUpdateIgnoreWarn:
		// OK
	default:
		mErr.Errors = append(mErr.Errors, fmt.Errorf("Service on_update must be %q, %q, or %q; not %q", OnUpdateRequireHealthy, OnUpdateIgnoreWarn, OnUpdateIgnore, s.OnUpdate))
	}

	// Up until this point, all service validation has been independent of the
	// provider. From this point on, we have different validation paths. We can
	// also catch an incorrect provider parameter.
	switch s.Provider {
	case ServiceProviderConsul:
		s.validateConsulService(&mErr)
	case ServiceProviderNomad:
		s.validateNomadService(&mErr)
	default:
		mErr.Errors = append(mErr.Errors, fmt.Errorf("Service provider must be %q, or %q; not %q",
			ServiceProviderConsul, ServiceProviderNomad, s.Provider))
	}

	return mErr.ErrorOrNil()
}

func (s *Service) validateCheckPort(c *ServiceCheck) error {
	if s.PortLabel == "" && c.PortLabel == "" && c.RequiresPort() {
		return fmt.Errorf("Check %s invalid: check requires a port but neither check nor service %+q have a port", c.Name, s.Name)
	}
	return nil
}

// validateConsulService performs validation on a service which is using the
// consul provider.
func (s *Service) validateConsulService(mErr *multierror.Error) {
	// check checks
	for _, c := range s.Checks {
		// validat ethe check port
		if err := s.validateCheckPort(c); err != nil {
			mErr.Errors = append(mErr.Errors, err)
			continue
		}

		// TCP checks against a Consul Connect enabled service are not supported
		// due to the service being bound to the loopback interface inside the
		// network namespace
		if c.Type == ServiceCheckTCP && s.Connect != nil && s.Connect.SidecarService != nil {
			mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: tcp checks are not valid for Connect enabled services", c.Name))
			continue
		}

		// validate the consul check
		if err := c.validateConsul(); err != nil {
			mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: %v", c.Name, err))
		}
	}

	// check connect
	if s.Connect != nil {
		if err := s.Connect.Validate(); err != nil {
			mErr.Errors = append(mErr.Errors, err)
		}

		// if service is connect native, service task must be set (which may
		// happen implicitly in a job mutation if there is only one task)
		if s.Connect.IsNative() && len(s.TaskName) == 0 {
			mErr.Errors = append(mErr.Errors, fmt.Errorf("Service %s is Connect Native and requires setting the task", s.Name))
		}
	}
}

// validateNomadService performs validation on a service which is using the
// nomad provider.
func (s *Service) validateNomadService(mErr *multierror.Error) {
	// check checks
	for _, c := range s.Checks {
		// validate the check port
		if err := s.validateCheckPort(c); err != nil {
			mErr.Errors = append(mErr.Errors, err)
			continue
		}

		// validate the nomad check
		if err := c.validateNomad(); err != nil {
			mErr.Errors = append(mErr.Errors, err)
		}
	}

	// Services using the Nomad provider do not support Consul connect.
	if s.Connect != nil {
		mErr.Errors = append(mErr.Errors, errors.New("Service with provider nomad cannot include Connect blocks"))
	}
}

// ValidateName checks if the service Name is valid and should be called after
// the name has been interpolated
func (s *Service) ValidateName(name string) error {
	// Ensure the service name is valid per RFC-952 §1
	// (https://tools.ietf.org/html/rfc952), RFC-1123 §2.1
	// (https://tools.ietf.org/html/rfc1123), and RFC-2782
	// (https://tools.ietf.org/html/rfc2782).
	//  This validation is enforced on Nomad, but not on Consul, however if
	//  consul-template is being used, service names with dots in them wont be
	//  admissible.
	re := regexp.MustCompile(`^(?i:[a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])$`)
	if !re.MatchString(name) {
		return fmt.Errorf("Service name must be valid per RFC 1123 and can contain only alphanumeric characters or dashes and must be no longer than 63 characters")
	}
	return nil
}

// Hash returns a base32 encoded hash of a Service's contents excluding checks
// as they're hashed independently and the provider in order to not cause churn
// during cluster upgrades.
func (s *Service) Hash(allocID, taskName string, canary bool) string {
	h := sha1.New()
	hashString(h, allocID)
	hashString(h, taskName)
	hashString(h, s.Name)
	hashString(h, s.PortLabel)
	hashString(h, s.AddressMode)
	hashString(h, s.Address)
	hashTags(h, s.Tags)
	hashTags(h, s.CanaryTags)
	hashBool(h, canary, "Canary")
	hashBool(h, s.EnableTagOverride, "ETO")
	hashMeta(h, s.Meta)
	hashMeta(h, s.CanaryMeta)
	hashMeta(h, s.TaggedAddresses)
	hashConnect(h, s.Connect)
	hashString(h, s.OnUpdate)
	hashString(h, s.Namespace)

	// Don't hash the provider parameter, so we don't cause churn of all
	// registered services when upgrading Nomad versions. The provider is not
	// used at the level the hash is and therefore is not needed to tell
	// whether the service has changed.

	// Base32 is used for encoding the hash as sha1 hashes can always be
	// encoded without padding, only 4 bytes larger than base64, and saves
	// 8 bytes vs hex. Since these hashes are used in Consul URLs it's nice
	// to have a reasonably compact URL-safe representation.
	return b32.EncodeToString(h.Sum(nil))
}

func hashConnect(h hash.Hash, connect *ConsulConnect) {
	if connect != nil && connect.SidecarService != nil {
		hashString(h, connect.SidecarService.Port)
		hashTags(h, connect.SidecarService.Tags)
		if p := connect.SidecarService.Proxy; p != nil {
			hashString(h, p.LocalServiceAddress)
			hashString(h, strconv.Itoa(p.LocalServicePort))
			hashConfig(h, p.Config)
			for _, upstream := range p.Upstreams {
				hashString(h, upstream.DestinationName)
				hashString(h, upstream.DestinationNamespace)
				hashString(h, strconv.Itoa(upstream.LocalBindPort))
				hashStringIfNonEmpty(h, upstream.Datacenter)
				hashStringIfNonEmpty(h, upstream.LocalBindAddress)
				hashString(h, upstream.DestinationPeer)
				hashString(h, upstream.DestinationType)
				hashString(h, upstream.LocalBindSocketPath)
				hashString(h, upstream.LocalBindSocketMode)
				hashConfig(h, upstream.Config)
			}
		}
	}
}

func hashString(h hash.Hash, s string) {
	_, _ = io.WriteString(h, s)
}

func hashBool(h hash.Hash, b bool, name string) {
	if b {
		hashString(h, name)
	}
}

func hashTags(h hash.Hash, tags []string) {
	for _, tag := range tags {
		hashString(h, tag)
	}
}

func hashMeta(h hash.Hash, m map[string]string) {
	_, _ = fmt.Fprintf(h, "%v", m)
}

func hashConfig(h hash.Hash, c map[string]interface{}) {
	_, _ = fmt.Fprintf(h, "%v", c)
}

// Equal returns true if the structs are recursively equal.
func (s *Service) Equal(o *Service) bool {
	if s == nil || o == nil {
		return s == o
	}

	if s.Provider != o.Provider {
		return false
	}

	if s.Namespace != o.Namespace {
		return false
	}

	if s.AddressMode != o.AddressMode {
		return false
	}

	if s.Address != o.Address {
		return false
	}

	if s.OnUpdate != o.OnUpdate {
		return false
	}

	if !helper.SliceSetEq(s.CanaryTags, o.CanaryTags) {
		return false
	}

	if !helper.ElementsEqual(s.Checks, o.Checks) {
		return false
	}

	if !s.Connect.Equal(o.Connect) {
		return false
	}

	if s.Name != o.Name {
		return false
	}

	if s.PortLabel != o.PortLabel {
		return false
	}

	if !maps.Equal(s.Meta, o.Meta) {
		return false
	}

	if !maps.Equal(s.CanaryMeta, o.CanaryMeta) {
		return false
	}

	if !maps.Equal(s.TaggedAddresses, o.TaggedAddresses) {
		return false
	}

	if !helper.SliceSetEq(s.Tags, o.Tags) {
		return false
	}

	if s.EnableTagOverride != o.EnableTagOverride {
		return false
	}

	return true
}

// ConsulConnect represents a Consul Connect jobspec block.
type ConsulConnect struct {
	// Native indicates whether the service is Consul Connect Native enabled.
	Native bool

	// SidecarService is non-nil if a service requires a sidecar.
	SidecarService *ConsulSidecarService

	// SidecarTask is non-nil if sidecar overrides are set
	SidecarTask *SidecarTask

	// Gateway is a Consul Connect Gateway Proxy.
	Gateway *ConsulGateway
}

// Copy the block recursively. Returns nil if nil.
func (c *ConsulConnect) Copy() *ConsulConnect {
	if c == nil {
		return nil
	}

	return &ConsulConnect{
		Native:         c.Native,
		SidecarService: c.SidecarService.Copy(),
		SidecarTask:    c.SidecarTask.Copy(),
		Gateway:        c.Gateway.Copy(),
	}
}

// Equal returns true if the connect blocks are deeply equal.
func (c *ConsulConnect) Equal(o *ConsulConnect) bool {
	if c == nil || o == nil {
		return c == o
	}

	if c.Native != o.Native {
		return false
	}

	if !c.SidecarService.Equal(o.SidecarService) {
		return false
	}

	if !c.SidecarTask.Equal(o.SidecarTask) {
		return false
	}

	if !c.Gateway.Equal(o.Gateway) {
		return false
	}

	return true
}

// HasSidecar checks if a sidecar task is configured.
func (c *ConsulConnect) HasSidecar() bool {
	return c != nil && c.SidecarService != nil
}

// IsNative checks if the service is connect native.
func (c *ConsulConnect) IsNative() bool {
	return c != nil && c.Native
}

// IsGateway checks if the service is any type of connect gateway.
func (c *ConsulConnect) IsGateway() bool {
	return c != nil && c.Gateway != nil
}

// IsIngress checks if the service is an ingress gateway.
func (c *ConsulConnect) IsIngress() bool {
	return c.IsGateway() && c.Gateway.Ingress != nil
}

// IsTerminating checks if the service is a terminating gateway.
func (c *ConsulConnect) IsTerminating() bool {
	return c.IsGateway() && c.Gateway.Terminating != nil
}

// IsCustomizedTLS checks if the service customizes ingress tls config.
func (c *ConsulConnect) IsCustomizedTLS() bool {
	return c.IsIngress() && c.Gateway.Ingress.TLS != nil &&
		(c.Gateway.Ingress.TLS.TLSMinVersion != "" ||
			c.Gateway.Ingress.TLS.TLSMaxVersion != "" ||
			len(c.Gateway.Ingress.TLS.CipherSuites) != 0)
}

func (c *ConsulConnect) IsMesh() bool {
	return c.IsGateway() && c.Gateway.Mesh != nil
}

// Validate that the Connect block represents exactly one of:
// - Connect non-native service sidecar proxy
// - Connect native service
// - Connect gateway (any type)
func (c *ConsulConnect) Validate() error {
	if c == nil {
		return nil
	}

	// Count the number of things actually configured. If that number is not 1,
	// the config is not valid.
	count := 0

	if c.HasSidecar() {
		count++
	}

	if c.IsNative() {
		count++
	}

	if c.IsGateway() {
		count++
	}

	if count != 1 {
		return fmt.Errorf("Consul Connect must be exclusively native, make use of a sidecar, or represent a Gateway")
	}

	if c.IsGateway() {
		if err := c.Gateway.Validate(); err != nil {
			return err
		}
	}

	// The Native and Sidecar cases are validated up at the service level.

	return nil
}

// ConsulSidecarService represents a Consul Connect SidecarService jobspec
// block.
type ConsulSidecarService struct {
	// Tags are optional service tags that get registered with the sidecar service
	// in Consul. If unset, the sidecar service inherits the parent service tags.
	Tags []string

	// Port is the service's port that the sidecar will connect to. May be
	// a port label or a literal port number.
	Port string

	// Proxy block defining the sidecar proxy configuration.
	Proxy *ConsulProxy

	// DisableDefaultTCPCheck, if true, instructs Nomad to avoid setting a
	// default TCP check for the sidecar service.
	DisableDefaultTCPCheck bool

	// Meta specifies arbitrary KV metadata linked to the sidecar service.
	Meta map[string]string
}

// HasUpstreams checks if the sidecar service has any upstreams configured
func (s *ConsulSidecarService) HasUpstreams() bool {
	return s != nil && s.Proxy != nil && len(s.Proxy.Upstreams) > 0
}

// Copy the block recursively. Returns nil if nil.
func (s *ConsulSidecarService) Copy() *ConsulSidecarService {
	if s == nil {
		return nil
	}
	return &ConsulSidecarService{
		Tags:                   slices.Clone(s.Tags),
		Port:                   s.Port,
		Proxy:                  s.Proxy.Copy(),
		DisableDefaultTCPCheck: s.DisableDefaultTCPCheck,
		Meta:                   maps.Clone(s.Meta),
	}
}

// Equal returns true if the structs are recursively equal.
func (s *ConsulSidecarService) Equal(o *ConsulSidecarService) bool {
	if s == nil || o == nil {
		return s == o
	}

	if s.Port != o.Port {
		return false
	}

	if s.DisableDefaultTCPCheck != o.DisableDefaultTCPCheck {
		return false
	}

	if !helper.SliceSetEq(s.Tags, o.Tags) {
		return false
	}

	if !maps.Equal(s.Meta, o.Meta) {
		return false
	}

	return s.Proxy.Equal(o.Proxy)
}

// SidecarTask represents a subset of Task fields that are able to be overridden
// from the sidecar_task block
type SidecarTask struct {
	// Name of the task
	Name string

	// Driver is used to control which driver is used
	Driver string

	// User is used to determine which user will run the task. It defaults to
	// the same user the Nomad client is being run as.
	User string

	// Config is provided to the driver to initialize
	Config map[string]interface{}

	// Map of environment variables to be used by the driver
	Env map[string]string

	// Resources is the resources needed by this task
	Resources *Resources

	// Meta is used to associate arbitrary metadata with this
	// task. This is opaque to Nomad.
	Meta map[string]string

	// KillTimeout is the time between signaling a task that it will be
	// killed and killing it.
	KillTimeout *time.Duration

	// LogConfig provides configuration for log rotation
	LogConfig *LogConfig

	// ShutdownDelay is the duration of the delay between deregistering a
	// task from Consul and sending it a signal to shutdown. See #2441
	ShutdownDelay *time.Duration

	// KillSignal is the kill signal to use for the task. This is an optional
	// specification and defaults to SIGINT
	KillSignal string
}

func (t *SidecarTask) Equal(o *SidecarTask) bool {
	if t == nil || o == nil {
		return t == o
	}

	if t.Name != o.Name {
		return false
	}

	if t.Driver != o.Driver {
		return false
	}

	if t.User != o.User {
		return false
	}

	// task config, use opaque maps equal
	if !helper.OpaqueMapsEqual(t.Config, o.Config) {
		return false
	}

	if !maps.Equal(t.Env, o.Env) {
		return false
	}

	if !t.Resources.Equal(o.Resources) {
		return false
	}

	if !maps.Equal(t.Meta, o.Meta) {
		return false
	}

	if !pointer.Eq(t.KillTimeout, o.KillTimeout) {
		return false
	}

	if !t.LogConfig.Equal(o.LogConfig) {
		return false
	}

	if !pointer.Eq(t.ShutdownDelay, o.ShutdownDelay) {
		return false
	}

	if t.KillSignal != o.KillSignal {
		return false
	}

	return true
}

func (t *SidecarTask) Copy() *SidecarTask {
	if t == nil {
		return nil
	}
	nt := new(SidecarTask)
	*nt = *t
	nt.Env = maps.Clone(nt.Env)

	nt.Resources = nt.Resources.Copy()
	nt.LogConfig = nt.LogConfig.Copy()
	nt.Meta = maps.Clone(nt.Meta)

	if i, err := copystructure.Copy(nt.Config); err != nil {
		panic(err.Error())
	} else {
		nt.Config = i.(map[string]interface{})
	}

	if t.KillTimeout != nil {
		nt.KillTimeout = pointer.Of(*t.KillTimeout)
	}

	if t.ShutdownDelay != nil {
		nt.ShutdownDelay = pointer.Of(*t.ShutdownDelay)
	}

	return nt
}

// MergeIntoTask merges the SidecarTask fields over the given task
func (t *SidecarTask) MergeIntoTask(task *Task) {
	if t.Name != "" {
		task.Name = t.Name
	}

	// If the driver changes then the driver config can be overwritten.
	// Otherwise we'll merge the driver config together
	if t.Driver != "" && t.Driver != task.Driver {
		task.Driver = t.Driver
		task.Config = t.Config
	} else {
		for k, v := range t.Config {
			task.Config[k] = v
		}
	}

	if t.User != "" {
		task.User = t.User
	}

	if t.Env != nil {
		if task.Env == nil {
			task.Env = t.Env
		} else {
			for k, v := range t.Env {
				task.Env[k] = v
			}
		}
	}

	if t.Resources != nil {
		task.Resources.Merge(t.Resources)
	}

	if t.Meta != nil {
		if task.Meta == nil {
			task.Meta = t.Meta
		} else {
			for k, v := range t.Meta {
				task.Meta[k] = v
			}
		}
	}

	if t.KillTimeout != nil {
		task.KillTimeout = *t.KillTimeout
	}

	if t.LogConfig != nil {
		if task.LogConfig == nil {
			task.LogConfig = t.LogConfig
		} else {
			if t.LogConfig.MaxFiles > 0 {
				task.LogConfig.MaxFiles = t.LogConfig.MaxFiles
			}
			if t.LogConfig.MaxFileSizeMB > 0 {
				task.LogConfig.MaxFileSizeMB = t.LogConfig.MaxFileSizeMB
			}
		}
	}

	if t.ShutdownDelay != nil {
		task.ShutdownDelay = *t.ShutdownDelay
	}

	if t.KillSignal != "" {
		task.KillSignal = t.KillSignal
	}
}

// ConsulProxy represents a Consul Connect sidecar proxy jobspec block.
type ConsulProxy struct {

	// LocalServiceAddress is the address the local service binds to.
	// Usually 127.0.0.1 it is useful to customize in clusters with mixed
	// Connect and non-Connect services.
	LocalServiceAddress string

	// LocalServicePort is the port the local service binds to. Usually
	// the same as the parent service's port, it is useful to customize
	// in clusters with mixed Connect and non-Connect services
	LocalServicePort int

	// Upstreams configures the upstream services this service intends to
	// connect to.
	Upstreams []ConsulUpstream

	// Expose configures the consul proxy.expose block to "open up" endpoints
	// used by task-group level service checks using HTTP or gRPC protocols.
	Expose *ConsulExposeConfig

	// Config is a proxy configuration. It is opaque to Nomad and passed
	// directly to Consul.
	Config map[string]interface{}
}

// Copy the block recursively. Returns nil if nil.
func (p *ConsulProxy) Copy() *ConsulProxy {
	if p == nil {
		return nil
	}

	return &ConsulProxy{
		LocalServiceAddress: p.LocalServiceAddress,
		LocalServicePort:    p.LocalServicePort,
		Expose:              p.Expose.Copy(),
		Upstreams:           slices.Clone(p.Upstreams),
		Config:              maps.Clone(p.Config),
	}
}

// Equal returns true if the structs are recursively equal.
func (p *ConsulProxy) Equal(o *ConsulProxy) bool {
	if p == nil || o == nil {
		return p == o
	}

	if p.LocalServiceAddress != o.LocalServiceAddress {
		return false
	}

	if p.LocalServicePort != o.LocalServicePort {
		return false
	}

	if !p.Expose.Equal(o.Expose) {
		return false
	}

	if !upstreamsEquals(p.Upstreams, o.Upstreams) {
		return false
	}

	// envoy config, use reflect
	if !reflect.DeepEqual(p.Config, o.Config) {
		return false
	}

	return true
}

// ConsulMeshGateway is used to configure mesh gateway usage when connecting to
// a connect upstream in another datacenter.
type ConsulMeshGateway struct {
	// Mode configures how an upstream should be accessed with regard to using
	// mesh gateways.
	//
	// local - the connect proxy makes outbound connections through mesh gateway
	// originating in the same datacenter.
	//
	// remote - the connect proxy makes outbound connections to a mesh gateway
	// in the destination datacenter.
	//
	// none (default) - no mesh gateway is used, the proxy makes outbound connections
	// directly to destination services.
	//
	// https://www.consul.io/docs/connect/gateways/mesh-gateway#modes-of-operation
	Mode string
}

func (c *ConsulMeshGateway) Copy() ConsulMeshGateway {
	return ConsulMeshGateway{
		Mode: c.Mode,
	}
}

func (c *ConsulMeshGateway) Equal(o ConsulMeshGateway) bool {
	return c.Mode == o.Mode
}

func (c *ConsulMeshGateway) Validate() error {
	if c == nil {
		return nil
	}

	switch c.Mode {
	case "local", "remote", "none":
		return nil
	default:
		return fmt.Errorf("Connect mesh_gateway mode %q not supported", c.Mode)
	}
}

// ConsulUpstream represents a Consul Connect upstream jobspec block.
type ConsulUpstream struct {
	// DestinationName is the name of the upstream service.
	DestinationName string

	// DestinationNamespace is the namespace of the upstream service.
	DestinationNamespace string

	// DestinationPeer the destination service address
	DestinationPeer string

	// DestinationType is the type of destination. It can be an IP address,
	// a DNS hostname, or a service name.
	DestinationType string

	// LocalBindPort is the port the proxy will receive connections for the
	// upstream on.
	LocalBindPort int

	// Datacenter is the datacenter in which to issue the discovery query to.
	Datacenter string

	// LocalBindAddress is the address the proxy will receive connections for the
	// upstream on.
	LocalBindAddress string

	// LocalBindSocketPath is the path of the local socket file that will be used
	// to connect to the destination service
	LocalBindSocketPath string

	// LocalBindSocketMode defines access permissions to the local socket file
	LocalBindSocketMode string

	// MeshGateway is the optional configuration of the mesh gateway for this
	// upstream to use.
	MeshGateway ConsulMeshGateway

	// Config is an upstream configuration. It is opaque to Nomad and passed
	// directly to Consul.
	Config map[string]any
}

// Equal returns true if the structs are recursively equal.
func (u *ConsulUpstream) Equal(o *ConsulUpstream) bool {
	if u == nil || o == nil {
		return u == o
	}
	switch {
	case u.DestinationName != o.DestinationName:
		return false
	case u.DestinationNamespace != o.DestinationNamespace:
		return false
	case u.DestinationPeer != o.DestinationPeer:
		return false
	case u.DestinationType != o.DestinationType:
		return false
	case u.LocalBindPort != o.LocalBindPort:
		return false
	case u.LocalBindSocketPath != o.LocalBindSocketPath:
		return false
	case u.LocalBindSocketMode != o.LocalBindSocketMode:
		return false
	case u.Datacenter != o.Datacenter:
		return false
	case u.LocalBindAddress != o.LocalBindAddress:
		return false
	case !u.MeshGateway.Equal(o.MeshGateway):
		return false
	case !reflect.DeepEqual(u.Config, o.Config):
		// envoy config, use reflect
		return false
	}
	return true
}

// Hash implements a GoString based "hash" function for ConsulUpstream; because
// this struct now contains an opaque map we cannot do much better than this.
func (u ConsulUpstream) Hash() string {
	return fmt.Sprintf("%#v", u)
}

func upstreamsEquals(a, b []ConsulUpstream) bool {
	setA := set.HashSetFrom[ConsulUpstream, string](a)
	setB := set.HashSetFrom[ConsulUpstream, string](b)
	return setA.Equal(setB)
}

// ConsulExposeConfig represents a Consul Connect expose jobspec block.
type ConsulExposeConfig struct {
	Paths []ConsulExposePath
}

type ConsulExposePath struct {
	Path          string
	Protocol      string
	LocalPathPort int
	ListenerPort  string
}

func exposePathsEqual(a, b []ConsulExposePath) bool {
	return helper.SliceSetEq(a, b)
}

// Copy the block. Returns nil if e is nil.
func (e *ConsulExposeConfig) Copy() *ConsulExposeConfig {
	if e == nil {
		return nil
	}
	paths := make([]ConsulExposePath, len(e.Paths))
	copy(paths, e.Paths)
	return &ConsulExposeConfig{
		Paths: paths,
	}
}

// Equal returns true if the structs are recursively equal.
func (e *ConsulExposeConfig) Equal(o *ConsulExposeConfig) bool {
	if e == nil || o == nil {
		return e == o
	}
	return exposePathsEqual(e.Paths, o.Paths)
}

// ConsulGateway is used to configure one of the Consul Connect Gateway types.
type ConsulGateway struct {
	// Proxy is used to configure the Envoy instance acting as the gateway.
	Proxy *ConsulGatewayProxy

	// Ingress represents the Consul Configuration Entry for an Ingress Gateway.
	Ingress *ConsulIngressConfigEntry

	// Terminating represents the Consul Configuration Entry for a Terminating Gateway.
	Terminating *ConsulTerminatingConfigEntry

	// Mesh indicates the Consul service should be a Mesh Gateway.
	Mesh *ConsulMeshConfigEntry
}

func (g *ConsulGateway) Prefix() string {
	switch {
	case g.Mesh != nil:
		return ConnectMeshPrefix
	case g.Ingress != nil:
		return ConnectIngressPrefix
	default:
		return ConnectTerminatingPrefix
	}
}

func (g *ConsulGateway) Copy() *ConsulGateway {
	if g == nil {
		return nil
	}

	return &ConsulGateway{
		Proxy:       g.Proxy.Copy(),
		Ingress:     g.Ingress.Copy(),
		Terminating: g.Terminating.Copy(),
		Mesh:        g.Mesh.Copy(),
	}
}

func (g *ConsulGateway) Equal(o *ConsulGateway) bool {
	if g == nil || o == nil {
		return g == o
	}

	if !g.Proxy.Equal(o.Proxy) {
		return false
	}

	if !g.Ingress.Equal(o.Ingress) {
		return false
	}

	if !g.Terminating.Equal(o.Terminating) {
		return false
	}

	if !g.Mesh.Equal(o.Mesh) {
		return false
	}

	return true
}

func (g *ConsulGateway) Validate() error {
	if g == nil {
		return nil
	}

	if err := g.Proxy.Validate(); err != nil {
		return err
	}

	if err := g.Ingress.Validate(); err != nil {
		return err
	}

	if err := g.Terminating.Validate(); err != nil {
		return err
	}

	if err := g.Mesh.Validate(); err != nil {
		return err
	}

	// Exactly 1 of ingress/terminating/mesh must be set.
	count := 0
	if g.Ingress != nil {
		count++
	}
	if g.Terminating != nil {
		count++
	}
	if g.Mesh != nil {
		count++
	}
	if count != 1 {
		return fmt.Errorf("One Consul Gateway Configuration must be set")
	}
	return nil
}

// ConsulGatewayBindAddress is equivalent to Consul's api/catalog.go ServiceAddress
// struct, as this is used to encode values to pass along to Envoy (i.e. via
// JSON encoding).
type ConsulGatewayBindAddress struct {
	Address string
	Port    int
}

func (a *ConsulGatewayBindAddress) Equal(o *ConsulGatewayBindAddress) bool {
	if a == nil || o == nil {
		return a == o
	}

	if a.Address != o.Address {
		return false
	}

	if a.Port != o.Port {
		return false
	}

	return true
}

func (a *ConsulGatewayBindAddress) Copy() *ConsulGatewayBindAddress {
	if a == nil {
		return nil
	}

	return &ConsulGatewayBindAddress{
		Address: a.Address,
		Port:    a.Port,
	}
}

func (a *ConsulGatewayBindAddress) Validate() error {
	if a == nil {
		return nil
	}

	if a.Address == "" {
		return fmt.Errorf("Consul Gateway Bind Address must be set")
	}

	if a.Port <= 0 && a.Port != -1 { // port -1 => nomad autofill
		return fmt.Errorf("Consul Gateway Bind Address must set valid Port")
	}

	return nil
}

// ConsulGatewayProxy is used to tune parameters of the proxy instance acting as
// one of the forms of Connect gateways that Consul supports.
//
// https://www.consul.io/docs/connect/proxies/envoy#gateway-options
type ConsulGatewayProxy struct {
	ConnectTimeout                  *time.Duration
	EnvoyGatewayBindTaggedAddresses bool
	EnvoyGatewayBindAddresses       map[string]*ConsulGatewayBindAddress
	EnvoyGatewayNoDefaultBind       bool
	EnvoyDNSDiscoveryType           string
	Config                          map[string]interface{}
}

func (p *ConsulGatewayProxy) Copy() *ConsulGatewayProxy {
	if p == nil {
		return nil
	}

	return &ConsulGatewayProxy{
		ConnectTimeout:                  pointer.Of(*p.ConnectTimeout),
		EnvoyGatewayBindTaggedAddresses: p.EnvoyGatewayBindTaggedAddresses,
		EnvoyGatewayBindAddresses:       p.copyBindAddresses(),
		EnvoyGatewayNoDefaultBind:       p.EnvoyGatewayNoDefaultBind,
		EnvoyDNSDiscoveryType:           p.EnvoyDNSDiscoveryType,
		Config:                          maps.Clone(p.Config),
	}
}

func (p *ConsulGatewayProxy) copyBindAddresses() map[string]*ConsulGatewayBindAddress {
	if p.EnvoyGatewayBindAddresses == nil {
		return nil
	}

	bindAddresses := make(map[string]*ConsulGatewayBindAddress, len(p.EnvoyGatewayBindAddresses))
	for k, v := range p.EnvoyGatewayBindAddresses {
		bindAddresses[k] = v.Copy()
	}

	return bindAddresses
}

func (p *ConsulGatewayProxy) equalBindAddresses(o map[string]*ConsulGatewayBindAddress) bool {
	if len(p.EnvoyGatewayBindAddresses) != len(o) {
		return false
	}

	for listener, addr := range p.EnvoyGatewayBindAddresses {
		if !o[listener].Equal(addr) {
			return false
		}
	}

	return true
}

func (p *ConsulGatewayProxy) Equal(o *ConsulGatewayProxy) bool {
	if p == nil || o == nil {
		return p == o
	}

	if !pointer.Eq(p.ConnectTimeout, o.ConnectTimeout) {
		return false
	}

	if p.EnvoyGatewayBindTaggedAddresses != o.EnvoyGatewayBindTaggedAddresses {
		return false
	}

	if !p.equalBindAddresses(o.EnvoyGatewayBindAddresses) {
		return false
	}

	if p.EnvoyGatewayNoDefaultBind != o.EnvoyGatewayNoDefaultBind {
		return false
	}

	if p.EnvoyDNSDiscoveryType != o.EnvoyDNSDiscoveryType {
		return false
	}

	// envoy config, use reflect
	if !reflect.DeepEqual(p.Config, o.Config) {
		return false
	}

	return true
}

const (
	strictDNS  = "STRICT_DNS"
	logicalDNS = "LOGICAL_DNS"
)

func (p *ConsulGatewayProxy) Validate() error {
	if p == nil {
		return nil
	}

	if p.ConnectTimeout == nil {
		return fmt.Errorf("Consul Gateway Proxy connection_timeout must be set")
	}

	switch p.EnvoyDNSDiscoveryType {
	case "", strictDNS, logicalDNS:
		// Consul defaults to logical DNS, suitable for large scale workloads.
		// https://www.envoyproxy.io/docs/envoy/v1.16.1/intro/arch_overview/upstream/service_discovery
	default:
		return fmt.Errorf("Consul Gateway Proxy Envoy DNS Discovery type must be %s or %s", strictDNS, logicalDNS)
	}

	for _, bindAddr := range p.EnvoyGatewayBindAddresses {
		if err := bindAddr.Validate(); err != nil {
			return err
		}
	}

	return nil
}

// ConsulGatewayTLSConfig is used to configure TLS for a gateway.
type ConsulGatewayTLSConfig struct {
	Enabled       bool
	TLSMinVersion string
	TLSMaxVersion string
	CipherSuites  []string
}

func (c *ConsulGatewayTLSConfig) Copy() *ConsulGatewayTLSConfig {
	if c == nil {
		return nil
	}

	return &ConsulGatewayTLSConfig{
		Enabled:       c.Enabled,
		TLSMinVersion: c.TLSMinVersion,
		TLSMaxVersion: c.TLSMaxVersion,
		CipherSuites:  slices.Clone(c.CipherSuites),
	}
}

func (c *ConsulGatewayTLSConfig) Equal(o *ConsulGatewayTLSConfig) bool {
	if c == nil || o == nil {
		return c == o
	}

	return c.Enabled == o.Enabled &&
		c.TLSMinVersion == o.TLSMinVersion &&
		c.TLSMaxVersion == o.TLSMaxVersion &&
		helper.SliceSetEq(c.CipherSuites, o.CipherSuites)
}

// ConsulIngressService is used to configure a service fronted by the ingress gateway.
type ConsulIngressService struct {
	Name  string
	Hosts []string
}

func (s *ConsulIngressService) Copy() *ConsulIngressService {
	if s == nil {
		return nil
	}

	var hosts []string = nil
	if n := len(s.Hosts); n > 0 {
		hosts = make([]string, n)
		copy(hosts, s.Hosts)
	}

	return &ConsulIngressService{
		Name:  s.Name,
		Hosts: hosts,
	}
}

func (s *ConsulIngressService) Equal(o *ConsulIngressService) bool {
	if s == nil || o == nil {
		return s == o
	}

	if s.Name != o.Name {
		return false
	}

	return helper.SliceSetEq(s.Hosts, o.Hosts)
}

func (s *ConsulIngressService) Validate(protocol string) error {
	if s == nil {
		return nil
	}

	// pre-validate service Name and Hosts before passing along to consul:
	// https://developer.hashicorp.com/consul/docs/connect/config-entries/ingress-gateway#services

	if s.Name == "" {
		return errors.New("Consul Ingress Service requires a name")
	}

	switch protocol {
	case "tcp":
		if s.Name == "*" {
			return errors.New(`Consul Ingress Service doesn't support wildcard name for "tcp" protocol`)
		}

		if len(s.Hosts) != 0 {
			return errors.New(`Consul Ingress Service doesn't support associating hosts to a service for the "tcp" protocol`)
		}
	default:
		if s.Name == "*" && len(s.Hosts) != 0 {
			return errors.New(`Consul Ingress Service with a wildcard "*" service name can not also specify hosts`)
		}
	}

	return nil
}

// ConsulIngressListener is used to configure a listener on a Consul Ingress
// Gateway.
type ConsulIngressListener struct {
	Port     int
	Protocol string
	Services []*ConsulIngressService
}

func (l *ConsulIngressListener) Copy() *ConsulIngressListener {
	if l == nil {
		return nil
	}

	var services []*ConsulIngressService = nil
	if n := len(l.Services); n > 0 {
		services = make([]*ConsulIngressService, n)
		for i := 0; i < n; i++ {
			services[i] = l.Services[i].Copy()
		}
	}

	return &ConsulIngressListener{
		Port:     l.Port,
		Protocol: l.Protocol,
		Services: services,
	}
}

func (l *ConsulIngressListener) Equal(o *ConsulIngressListener) bool {
	if l == nil || o == nil {
		return l == o
	}

	if l.Port != o.Port {
		return false
	}

	if l.Protocol != o.Protocol {
		return false
	}

	return ingressServicesEqual(l.Services, o.Services)
}

func (l *ConsulIngressListener) Validate() error {
	if l == nil {
		return nil
	}

	if l.Port <= 0 {
		return fmt.Errorf("Consul Ingress Listener requires valid Port")
	}

	protocols := []string{"tcp", "http", "http2", "grpc"}
	if !slices.Contains(protocols, l.Protocol) {
		return fmt.Errorf(`Consul Ingress Listener requires protocol of %s, got %q`, strings.Join(protocols, ", "), l.Protocol)
	}

	if len(l.Services) == 0 {
		return fmt.Errorf("Consul Ingress Listener requires one or more services")
	}

	for _, service := range l.Services {
		if err := service.Validate(l.Protocol); err != nil {
			return err
		}
	}

	return nil
}

func ingressServicesEqual(a, b []*ConsulIngressService) bool {
	return helper.ElementsEqual(a, b)
}

// ConsulIngressConfigEntry represents the Consul Configuration Entry type for
// an Ingress Gateway.
//
// https://www.consul.io/docs/agent/config-entries/ingress-gateway#available-fields
type ConsulIngressConfigEntry struct {
	TLS       *ConsulGatewayTLSConfig
	Listeners []*ConsulIngressListener
}

func (e *ConsulIngressConfigEntry) Copy() *ConsulIngressConfigEntry {
	if e == nil {
		return nil
	}

	var listeners []*ConsulIngressListener = nil
	if n := len(e.Listeners); n > 0 {
		listeners = make([]*ConsulIngressListener, n)
		for i := 0; i < n; i++ {
			listeners[i] = e.Listeners[i].Copy()
		}
	}

	return &ConsulIngressConfigEntry{
		TLS:       e.TLS.Copy(),
		Listeners: listeners,
	}
}

func (e *ConsulIngressConfigEntry) Equal(o *ConsulIngressConfigEntry) bool {
	if e == nil || o == nil {
		return e == o
	}

	if !e.TLS.Equal(o.TLS) {
		return false
	}

	return ingressListenersEqual(e.Listeners, o.Listeners)
}

func (e *ConsulIngressConfigEntry) Validate() error {
	if e == nil {
		return nil
	}

	if len(e.Listeners) == 0 {
		return fmt.Errorf("Consul Ingress Gateway requires at least one listener")
	}

	for _, listener := range e.Listeners {
		if err := listener.Validate(); err != nil {
			return err
		}
	}

	return nil
}

func ingressListenersEqual(a, b []*ConsulIngressListener) bool {
	return helper.ElementsEqual(a, b)
}

type ConsulLinkedService struct {
	Name     string
	CAFile   string
	CertFile string
	KeyFile  string
	SNI      string
}

func (s *ConsulLinkedService) Copy() *ConsulLinkedService {
	if s == nil {
		return nil
	}

	return &ConsulLinkedService{
		Name:     s.Name,
		CAFile:   s.CAFile,
		CertFile: s.CertFile,
		KeyFile:  s.KeyFile,
		SNI:      s.SNI,
	}
}

func (s *ConsulLinkedService) Equal(o *ConsulLinkedService) bool {
	if s == nil || o == nil {
		return s == o
	}

	switch {
	case s.Name != o.Name:
		return false
	case s.CAFile != o.CAFile:
		return false
	case s.CertFile != o.CertFile:
		return false
	case s.KeyFile != o.KeyFile:
		return false
	case s.SNI != o.SNI:
		return false
	}

	return true
}

func (s *ConsulLinkedService) Validate() error {
	if s == nil {
		return nil
	}

	if s.Name == "" {
		return fmt.Errorf("Consul Linked Service requires Name")
	}

	caSet := s.CAFile != ""
	certSet := s.CertFile != ""
	keySet := s.KeyFile != ""
	sniSet := s.SNI != ""

	if (certSet || keySet) && !caSet {
		return fmt.Errorf("Consul Linked Service TLS requires CAFile")
	}

	if certSet != keySet {
		return fmt.Errorf("Consul Linked Service TLS Cert and Key must both be set")
	}

	if sniSet && !caSet {
		return fmt.Errorf("Consul Linked Service TLS SNI requires CAFile")
	}

	return nil
}

func linkedServicesEqual(a, b []*ConsulLinkedService) bool {
	return helper.ElementsEqual(a, b)
}

type ConsulTerminatingConfigEntry struct {
	Services []*ConsulLinkedService
}

func (e *ConsulTerminatingConfigEntry) Copy() *ConsulTerminatingConfigEntry {
	if e == nil {
		return nil
	}

	var services []*ConsulLinkedService = nil
	if n := len(e.Services); n > 0 {
		services = make([]*ConsulLinkedService, n)
		for i := 0; i < n; i++ {
			services[i] = e.Services[i].Copy()
		}
	}

	return &ConsulTerminatingConfigEntry{
		Services: services,
	}
}

func (e *ConsulTerminatingConfigEntry) Equal(o *ConsulTerminatingConfigEntry) bool {
	if e == nil || o == nil {
		return e == o
	}

	return linkedServicesEqual(e.Services, o.Services)
}

func (e *ConsulTerminatingConfigEntry) Validate() error {
	if e == nil {
		return nil
	}

	if len(e.Services) == 0 {
		return fmt.Errorf("Consul Terminating Gateway requires at least one service")
	}

	for _, service := range e.Services {
		if err := service.Validate(); err != nil {
			return err
		}
	}

	return nil
}

// ConsulMeshConfigEntry is a stub used to represent that the gateway service
// type should be for a Mesh Gateway. Unlike Ingress and Terminating, there is no
// dedicated Consul Config Entry type for "mesh-gateway", for now. We still
// create a type for future proofing, and to keep underlying job-spec marshaling
// consistent with the other types.
type ConsulMeshConfigEntry struct {
	// nothing in here
}

func (e *ConsulMeshConfigEntry) Copy() *ConsulMeshConfigEntry {
	if e == nil {
		return nil
	}
	return new(ConsulMeshConfigEntry)
}

func (e *ConsulMeshConfigEntry) Equal(o *ConsulMeshConfigEntry) bool {
	if e == nil || o == nil {
		return e == o
	}
	return true
}

func (e *ConsulMeshConfigEntry) Validate() error {
	return nil
}
