// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package structs

import (
	"bytes"
	_ "embed"
	"fmt"
	"hash"
	"hash/fnv"
	"html/template"

	"github.com/hashicorp/go-multierror"
	"github.com/xeipuuv/gojsonschema"
	"golang.org/x/exp/slices"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/lib/stringslice"
)

//go:embed acltemplatedpolicy/schemas/node.json
var ACLTemplatedPolicyNodeSchema string

//go:embed acltemplatedpolicy/schemas/service.json
var ACLTemplatedPolicyServiceSchema string

//go:embed acltemplatedpolicy/schemas/workload-identity.json
var ACLTemplatedPolicyWorkloadIdentitySchema string

//go:embed acltemplatedpolicy/schemas/api-gateway.json
var ACLTemplatedPolicyAPIGatewaySchema string

type ACLTemplatedPolicies []*ACLTemplatedPolicy

const (
	ACLTemplatedPolicyServiceID          = "00000000-0000-0000-0000-000000000003"
	ACLTemplatedPolicyNodeID             = "00000000-0000-0000-0000-000000000004"
	ACLTemplatedPolicyDNSID              = "00000000-0000-0000-0000-000000000005"
	ACLTemplatedPolicyNomadServerID      = "00000000-0000-0000-0000-000000000006"
	ACLTemplatedPolicyWorkloadIdentityID = "00000000-0000-0000-0000-000000000007"
	ACLTemplatedPolicyAPIGatewayID       = "00000000-0000-0000-0000-000000000008"
	ACLTemplatedPolicyNomadClientID      = "00000000-0000-0000-0000-000000000009"

	ACLTemplatedPolicyServiceDescription          = "Gives the token or role permissions to register a service and discover services in the Consul catalog. It also gives the specified service's sidecar proxy the permission to discover and route traffic to other services."
	ACLTemplatedPolicyNodeDescription             = "Gives the token or role permissions for a register an agent/node into the catalog. A node is typically a consul agent but can also be a physical server, cloud instance or a container."
	ACLTemplatedPolicyDNSDescription              = "Gives the token or role permissions for the Consul DNS to query services in the network."
	ACLTemplatedPolicyNomadServerDescription      = "Gives the token or role permissions required for integration with a nomad server."
	ACLTemplatedPolicyWorkloadIdentityDescription = "Gives the token or role permissions for a specific workload identity."
	ACLTemplatedPolicyAPIGatewayDescription       = "Gives the token or role permissions for a Consul api gateway"
	ACLTemplatedPolicyNomadClientDescription      = "Gives the token or role permissions required for integration with a nomad client."

	ACLTemplatedPolicyNoRequiredVariablesSchema = "" // catch-all schema for all templated policy that don't require a schema
)

// ACLTemplatedPolicyBase contains basic information about builtin templated policies
// template name, id, template code and schema
type ACLTemplatedPolicyBase struct {
	TemplateName string
	TemplateID   string
	Schema       string
	Template     string
	Description  string
}

var (
	// Note: when adding a new builtin template, ensure you update `command/acl/templatedpolicy/formatter.go`
	// to handle the new templates required variables and schema.
	aclTemplatedPoliciesList = map[string]*ACLTemplatedPolicyBase{
		api.ACLTemplatedPolicyServiceName: {
			TemplateID:   ACLTemplatedPolicyServiceID,
			TemplateName: api.ACLTemplatedPolicyServiceName,
			Schema:       ACLTemplatedPolicyServiceSchema,
			Template:     ACLTemplatedPolicyService,
			Description:  ACLTemplatedPolicyServiceDescription,
		},
		api.ACLTemplatedPolicyNodeName: {
			TemplateID:   ACLTemplatedPolicyNodeID,
			TemplateName: api.ACLTemplatedPolicyNodeName,
			Schema:       ACLTemplatedPolicyNodeSchema,
			Template:     ACLTemplatedPolicyNode,
			Description:  ACLTemplatedPolicyNodeDescription,
		},
		api.ACLTemplatedPolicyDNSName: {
			TemplateID:   ACLTemplatedPolicyDNSID,
			TemplateName: api.ACLTemplatedPolicyDNSName,
			Schema:       ACLTemplatedPolicyNoRequiredVariablesSchema,
			Template:     ACLTemplatedPolicyDNS,
			Description:  ACLTemplatedPolicyDNSDescription,
		},
		api.ACLTemplatedPolicyNomadServerName: {
			TemplateID:   ACLTemplatedPolicyNomadServerID,
			TemplateName: api.ACLTemplatedPolicyNomadServerName,
			Schema:       ACLTemplatedPolicyNoRequiredVariablesSchema,
			Template:     ACLTemplatedPolicyNomadServer,
			Description:  ACLTemplatedPolicyNomadServerDescription,
		},
		api.ACLTemplatedPolicyWorkloadIdentityName: {
			TemplateID:   ACLTemplatedPolicyWorkloadIdentityID,
			TemplateName: api.ACLTemplatedPolicyWorkloadIdentityName,
			Schema:       ACLTemplatedPolicyWorkloadIdentitySchema,
			Template:     ACLTemplatedPolicyWorkloadIdentity,
			Description:  ACLTemplatedPolicyWorkloadIdentityDescription,
		},
		api.ACLTemplatedPolicyAPIGatewayName: {
			TemplateID:   ACLTemplatedPolicyAPIGatewayID,
			TemplateName: api.ACLTemplatedPolicyAPIGatewayName,
			Schema:       ACLTemplatedPolicyAPIGatewaySchema,
			Template:     ACLTemplatedPolicyAPIGateway,
			Description:  ACLTemplatedPolicyAPIGatewayDescription,
		},
		api.ACLTemplatedPolicyNomadClientName: {
			TemplateID:   ACLTemplatedPolicyNomadClientID,
			TemplateName: api.ACLTemplatedPolicyNomadClientName,
			Schema:       ACLTemplatedPolicyNoRequiredVariablesSchema,
			Template:     ACLTemplatedPolicyNomadClient,
			Description:  ACLTemplatedPolicyNomadClientDescription,
		},
	}
)

// ACLTemplatedPolicy represents a template used to generate a `synthetic` policy
// given some input variables.
type ACLTemplatedPolicy struct {
	// TemplateID are hidden from all displays and should not be exposed to the users.
	TemplateID string `json:",omitempty"`

	// TemplateName is used for display purposes mostly and should not be used for policy rendering.
	TemplateName string `json:",omitempty"`

	// TemplateVariables are input variables required to render templated policies.
	TemplateVariables *ACLTemplatedPolicyVariables `json:",omitempty"`

	// Datacenters that the synthetic policy will be valid within.
	//   - No wildcards allowed
	//   - If empty then the synthetic policy is valid within all datacenters
	//
	// This is kept for legacy reasons to enable us to replace Node/Service Identities by templated policies.
	//
	// Only valid for global tokens. It is an error to specify this for local tokens.
	Datacenters []string `json:",omitempty"`
}

// ACLTemplatedPolicyVariables are input variables required to render templated policies.
type ACLTemplatedPolicyVariables struct {
	Name string `json:"name,omitempty"`
}

func (tp *ACLTemplatedPolicy) Clone() *ACLTemplatedPolicy {
	tp2 := *tp

	tp2.TemplateVariables = nil
	if tp.TemplateVariables != nil {
		tp2.TemplateVariables = tp.TemplateVariables.Clone()
	}
	tp2.Datacenters = stringslice.CloneStringSlice(tp.Datacenters)

	return &tp2
}

func (tp *ACLTemplatedPolicy) AddToHash(h hash.Hash) {
	h.Write([]byte(tp.TemplateID))
	h.Write([]byte(tp.TemplateName))

	if tp.TemplateVariables != nil {
		tp.TemplateVariables.AddToHash(h)
	}
	for _, dc := range tp.Datacenters {
		h.Write([]byte(dc))
	}
}

func (tv *ACLTemplatedPolicyVariables) AddToHash(h hash.Hash) {
	h.Write([]byte(tv.Name))
}

func (tv *ACLTemplatedPolicyVariables) Clone() *ACLTemplatedPolicyVariables {
	tv2 := *tv
	return &tv2
}

// validates templated policy variables against schema.
func (tp *ACLTemplatedPolicy) ValidateTemplatedPolicy(schema string) error {
	if schema == "" {
		return nil
	}

	loader := gojsonschema.NewStringLoader(schema)
	dataloader := gojsonschema.NewGoLoader(tp.TemplateVariables)
	res, err := gojsonschema.Validate(loader, dataloader)
	if err != nil {
		return fmt.Errorf("failed to load json schema for validation %w", err)
	}

	// validate service and node identity names
	if tp.TemplateVariables != nil {
		if tp.TemplateName == api.ACLTemplatedPolicyServiceName && !acl.IsValidServiceIdentityName(tp.TemplateVariables.Name) {
			return fmt.Errorf("service identity %q has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed", tp.TemplateVariables.Name)
		}

		if tp.TemplateName == api.ACLTemplatedPolicyNodeName && !acl.IsValidNodeIdentityName(tp.TemplateVariables.Name) {
			return fmt.Errorf("node identity %q  has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed", tp.TemplateVariables.Name)
		}
	}

	if res.Valid() {
		return nil
	}

	var merr *multierror.Error

	for _, resultError := range res.Errors() {
		merr = multierror.Append(merr, fmt.Errorf(resultError.Description()))
	}
	return merr.ErrorOrNil()
}

func (tp *ACLTemplatedPolicy) EstimateSize() int {
	size := len(tp.TemplateName) + len(tp.TemplateID) + tp.TemplateVariables.EstimateSize()
	for _, dc := range tp.Datacenters {
		size += len(dc)
	}

	return size
}

func (tv *ACLTemplatedPolicyVariables) EstimateSize() int {
	return len(tv.Name)
}

// SyntheticPolicy generates a policy based on templated policies' ID and variables
//
// Given that we validate this string name before persisting, we do not
// have to escape it before doing the following interpolation.
func (tp *ACLTemplatedPolicy) SyntheticPolicy(entMeta *acl.EnterpriseMeta) (*ACLPolicy, error) {
	rules, err := tp.aclTemplatedPolicyRules(entMeta)
	if err != nil {
		return nil, err
	}
	hasher := fnv.New128a()
	hashID := fmt.Sprintf("%x", hasher.Sum([]byte(rules)))

	policy := &ACLPolicy{
		Rules:       rules,
		ID:          hashID,
		Name:        fmt.Sprintf("synthetic-policy-%s", hashID),
		Datacenters: tp.Datacenters,
		Description: fmt.Sprintf("synthetic policy generated from templated policy: %s", tp.TemplateName),
	}
	policy.EnterpriseMeta.Merge(entMeta)
	policy.SetHash(true)

	return policy, nil
}

func (tp *ACLTemplatedPolicy) aclTemplatedPolicyRules(entMeta *acl.EnterpriseMeta) (string, error) {
	if entMeta == nil {
		entMeta = DefaultEnterpriseMetaInDefaultPartition()
	}
	entMeta.Normalize()

	tpl := template.New(tp.TemplateName)
	tmplCode, ok := aclTemplatedPoliciesList[tp.TemplateName]
	if !ok {
		return "", fmt.Errorf("acl templated policy does not exist: %s", tp.TemplateName)
	}

	parsedTpl, err := tpl.Parse(tmplCode.Template)
	if err != nil {
		return "", fmt.Errorf("an error occured when parsing template structs: %w", err)
	}
	var buf bytes.Buffer
	err = parsedTpl.Execute(&buf, struct {
		*ACLTemplatedPolicyVariables
		Namespace string
		Partition string
	}{
		Namespace:                   entMeta.NamespaceOrDefault(),
		Partition:                   entMeta.PartitionOrDefault(),
		ACLTemplatedPolicyVariables: tp.TemplateVariables,
	})
	if err != nil {
		return "", fmt.Errorf("an error occured when executing on templated policy variables: %w", err)
	}

	return buf.String(), nil
}

// Deduplicate returns a new list of templated policies without duplicates.
// compares values of template variables to ensure no duplicates
func (tps ACLTemplatedPolicies) Deduplicate() ACLTemplatedPolicies {
	list := make(map[string][]ACLTemplatedPolicyVariables)
	var out ACLTemplatedPolicies

	for _, tp := range tps {
		// checks if template name already in the unique list
		_, found := list[tp.TemplateName]
		if !found {
			list[tp.TemplateName] = make([]ACLTemplatedPolicyVariables, 0)
		}
		templateSchema := aclTemplatedPoliciesList[tp.TemplateName].Schema

		// if schema is empty, template does not require variables
		if templateSchema == "" {
			if !found {
				out = append(out, tp)
			}
			continue
		}

		if !slices.Contains(list[tp.TemplateName], *tp.TemplateVariables) {
			list[tp.TemplateName] = append(list[tp.TemplateName], *tp.TemplateVariables)
			out = append(out, tp)
		}
	}

	return out
}

func GetACLTemplatedPolicyBase(templateName string) (*ACLTemplatedPolicyBase, bool) {
	if orig, found := aclTemplatedPoliciesList[templateName]; found {
		copy := *orig
		return &copy, found
	}

	return nil, false
}

// GetACLTemplatedPolicyList returns a copy of the list of templated policies
func GetACLTemplatedPolicyList() map[string]*ACLTemplatedPolicyBase {
	m := make(map[string]*ACLTemplatedPolicyBase, len(aclTemplatedPoliciesList))
	for k, v := range aclTemplatedPoliciesList {
		m[k] = v
	}

	return m
}
