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

package agent

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/hashicorp/consul/acl"
	cachetype "github.com/hashicorp/consul/agent/cache-types"
	"github.com/hashicorp/consul/agent/consul"
	"github.com/hashicorp/consul/agent/structs"
)

// /v1/connect/intentions
func (s *HTTPHandlers) IntentionEndpoint(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	switch req.Method {
	case "GET":
		return s.IntentionList(resp, req)

	case "POST":
		return s.IntentionCreate(resp, req)

	default:
		return nil, MethodNotAllowedError{req.Method, []string{"GET", "POST"}}
	}
}

// GET /v1/connect/intentions
func (s *HTTPHandlers) IntentionList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	// Method is tested in IntentionEndpoint

	var args structs.IntentionListRequest
	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
		return nil, nil
	}

	if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
		return nil, err
	}

	var reply structs.IndexedIntentions
	defer setMeta(resp, &reply.QueryMeta)
	if err := s.agent.RPC(req.Context(), "Intention.List", &args, &reply); err != nil {
		return nil, err
	}

	return reply.Intentions, nil
}

// IntentionCreate is used to create legacy intentions.
// Deprecated: use IntentionPutExact.
func (s *HTTPHandlers) IntentionCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	// Method is tested in IntentionEndpoint

	var entMeta acl.EnterpriseMeta
	if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
		return nil, err
	}

	if entMeta.PartitionOrDefault() != acl.PartitionOrDefault("") {
		return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Cannot use a partition with this endpoint"}
	}

	args := structs.IntentionRequest{
		Op: structs.IntentionOpCreate,
	}
	s.parseDC(req, &args.Datacenter)
	s.parseToken(req, &args.Token)
	if err := decodeBody(req.Body, &args.Intention); err != nil {
		return nil, fmt.Errorf("Failed to decode request body: %s", err)
	}

	if args.Intention.DestinationPartition != "" && args.Intention.DestinationPartition != "default" {
		return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Cannot specify a destination partition with this endpoint"}
	}
	if args.Intention.SourcePartition != "" && args.Intention.SourcePartition != "default" {
		return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Cannot specify a source partition with this endpoint"}
	}

	args.Intention.FillPartitionAndNamespace(&entMeta, false)

	if err := s.validateEnterpriseIntention(args.Intention); err != nil {
		return nil, err
	}

	var reply string
	if err := s.agent.RPC(req.Context(), "Intention.Apply", &args, &reply); err != nil {
		return nil, err
	}

	return intentionCreateResponse{reply}, nil
}

func (s *HTTPHandlers) validateEnterpriseIntention(ixn *structs.Intention) error {
	if err := s.validateEnterpriseIntentionPartition("SourcePartition", ixn.SourcePartition); err != nil {
		return err
	}
	if err := s.validateEnterpriseIntentionPartition("DestinationPartition", ixn.DestinationPartition); err != nil {
		return err
	}
	if err := s.validateEnterpriseIntentionNamespace("SourceNS", ixn.SourceNS, true); err != nil {
		return err
	}
	if err := s.validateEnterpriseIntentionNamespace("DestinationNS", ixn.DestinationNS, true); err != nil {
		return err
	}

	return nil
}

// GET /v1/connect/intentions/match
func (s *HTTPHandlers) IntentionMatch(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	// Prepare args
	args := &structs.IntentionQueryRequest{Match: &structs.IntentionQueryMatch{}}
	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
		return nil, nil
	}

	var entMeta acl.EnterpriseMeta
	if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
		return nil, err
	}

	q := req.URL.Query()

	// Extract the "by" query parameter
	if by, ok := q["by"]; !ok || len(by) != 1 {
		return nil, fmt.Errorf("required query parameter 'by' not set")
	} else {
		switch v := structs.IntentionMatchType(by[0]); v {
		case structs.IntentionMatchSource, structs.IntentionMatchDestination:
			args.Match.Type = v
		default:
			return nil, fmt.Errorf("'by' parameter must be one of 'source' or 'destination'")
		}
	}

	// Extract all the match names
	names, ok := q["name"]
	if !ok || len(names) == 0 {
		return nil, fmt.Errorf("required query parameter 'name' not set")
	}

	// Build the entries in order. The order matters since that is the
	// order of the returned responses.
	args.Match.Entries = make([]structs.IntentionMatchEntry, len(names))
	for i, n := range names {
		parsed, err := parseIntentionStringComponent(n, &entMeta, false)
		if err != nil {
			return nil, fmt.Errorf("name %q is invalid: %s", n, err)
		}

		args.Match.Entries[i] = structs.IntentionMatchEntry{
			Partition: parsed.ap,
			Namespace: parsed.ns,
			Name:      parsed.name,
		}
	}

	// Make the RPC request
	var out structs.IndexedIntentionMatches
	defer setMeta(resp, &out.QueryMeta)

	if s.agent.config.HTTPUseCache && args.QueryOptions.UseCache {
		raw, m, err := s.agent.cache.Get(req.Context(), cachetype.IntentionMatchName, args)
		if err != nil {
			return nil, err
		}
		defer setCacheMeta(resp, &m)

		reply, ok := raw.(*structs.IndexedIntentionMatches)
		if !ok {
			// This should never happen, but we want to protect against panics
			return nil, fmt.Errorf("internal error: response type not correct")
		}
		out = *reply
	} else {
	RETRY_ONCE:
		if err := s.agent.RPC(req.Context(), "Intention.Match", args, &out); err != nil {
			return nil, err
		}
		if args.QueryOptions.AllowStale && args.MaxStaleDuration > 0 && args.MaxStaleDuration < out.LastContact {
			args.AllowStale = false
			args.MaxStaleDuration = 0
			goto RETRY_ONCE
		}
	}
	out.ConsistencyLevel = args.QueryOptions.ConsistencyLevel()

	// We must have an identical count of matches
	if len(out.Matches) != len(names) {
		return nil, fmt.Errorf("internal error: match response count didn't match input count")
	}

	// Use empty list instead of nil.
	response := make(map[string]structs.Intentions)
	for i, ixns := range out.Matches {
		response[names[i]] = ixns
	}

	return response, nil
}

// GET /v1/connect/intentions/check
func (s *HTTPHandlers) IntentionCheck(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	// Prepare args
	args := &structs.IntentionQueryRequest{Check: &structs.IntentionQueryCheck{}}
	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
		return nil, nil
	}

	var entMeta acl.EnterpriseMeta
	if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
		return nil, err
	}

	q := req.URL.Query()

	// Set the source type if set
	args.Check.SourceType = structs.IntentionSourceConsul
	if sourceType, ok := q["source-type"]; ok && len(sourceType) > 0 {
		args.Check.SourceType = structs.IntentionSourceType(sourceType[0])
	}

	// Extract the source/destination
	source, ok := q["source"]
	if !ok || len(source) != 1 {
		return nil, fmt.Errorf("required query parameter 'source' not set")
	}
	destination, ok := q["destination"]
	if !ok || len(destination) != 1 {
		return nil, fmt.Errorf("required query parameter 'destination' not set")
	}

	// We parse them the same way as matches to extract partition/namespace/name
	args.Check.SourceName = source[0]
	if args.Check.SourceType == structs.IntentionSourceConsul {
		parsed, err := parseIntentionStringComponent(source[0], &entMeta, false)
		if err != nil {
			return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
		}
		args.Check.SourcePartition = parsed.ap
		args.Check.SourceNS = parsed.ns
		args.Check.SourceName = parsed.name
	}

	// The destination is always in the Consul format
	parsed, err := parseIntentionStringComponent(destination[0], &entMeta, false)
	if err != nil {
		return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
	}
	args.Check.DestinationPartition = parsed.ap
	args.Check.DestinationNS = parsed.ns
	args.Check.DestinationName = parsed.name

	var reply structs.IntentionQueryCheckResponse
	if err := s.agent.RPC(req.Context(), "Intention.Check", args, &reply); err != nil {
		return nil, err
	}

	return &reply, nil
}

// IntentionExact handles the endpoint for /v1/connect/intentions/exact
func (s *HTTPHandlers) IntentionExact(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	switch req.Method {
	case "GET":
		return s.IntentionGetExact(resp, req)
	case "PUT":
		return s.IntentionPutExact(resp, req)
	case "DELETE":
		return s.IntentionDeleteExact(resp, req)
	default:
		return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
	}
}

// GET /v1/connect/intentions/exact
func (s *HTTPHandlers) IntentionGetExact(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var entMeta acl.EnterpriseMeta
	if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
		return nil, err
	}

	args := structs.IntentionQueryRequest{
		Exact: &structs.IntentionQueryExact{},
	}
	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
		return nil, nil
	}

	q := req.URL.Query()

	// Extract the source/destination
	source, ok := q["source"]
	if !ok || len(source) != 1 {
		return nil, fmt.Errorf("required query parameter 'source' not set")
	}
	destination, ok := q["destination"]
	if !ok || len(destination) != 1 {
		return nil, fmt.Errorf("required query parameter 'destination' not set")
	}

	{
		parsed, err := parseIntentionStringComponent(source[0], &entMeta, true)
		if err != nil {
			return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
		}

		args.Exact.SourcePeer = parsed.peer
		args.Exact.SourcePartition = parsed.ap
		args.Exact.SourceNS = parsed.ns
		args.Exact.SourceName = parsed.name
	}

	{
		parsed, err := parseIntentionStringComponent(destination[0], &entMeta, false)
		if err != nil {
			return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
		}
		args.Exact.DestinationPartition = parsed.ap
		args.Exact.DestinationNS = parsed.ns
		args.Exact.DestinationName = parsed.name
	}

	var reply structs.IndexedIntentions
	if err := s.agent.RPC(req.Context(), "Intention.Get", &args, &reply); err != nil {
		// We have to check the string since the RPC sheds the error type
		if strings.Contains(err.Error(), consul.ErrIntentionNotFound.Error()) {
			return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: err.Error()}
		}

		// Not ideal, but there are a number of error scenarios that are not
		// user error (400). We look for a specific case of invalid UUID
		// to detect a parameter error and return a 400 response. The error
		// is not a constant type or message, so we have to use strings.Contains
		if strings.Contains(err.Error(), "UUID") {
			return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: err.Error()}
		}

		return nil, err
	}

	// This shouldn't happen since the called API documents it shouldn't,
	// but we check since the alternative if it happens is a panic.
	if len(reply.Intentions) == 0 {
		resp.WriteHeader(http.StatusNotFound)
		return nil, nil
	}

	return reply.Intentions[0], nil
}

// PUT /v1/connect/intentions/exact
func (s *HTTPHandlers) IntentionPutExact(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var entMeta acl.EnterpriseMeta
	if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
		return nil, err
	}

	exact, err := parseIntentionQueryExact(req, &entMeta)
	if err != nil {
		return nil, err
	}

	args := structs.IntentionRequest{
		Op: structs.IntentionOpUpsert,
	}
	s.parseDC(req, &args.Datacenter)
	s.parseToken(req, &args.Token)
	if err := decodeBody(req.Body, &args.Intention); err != nil {
		return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Request decode failed: %v", err)}
	}

	// Explicitly CLEAR the old legacy ID field
	args.Intention.ID = ""

	// Use the intention identity from the URL.
	args.Intention.SourcePartition = exact.SourcePartition
	args.Intention.SourceNS = exact.SourceNS
	args.Intention.SourceName = exact.SourceName
	args.Intention.DestinationPartition = exact.DestinationPartition
	args.Intention.DestinationNS = exact.DestinationNS
	args.Intention.DestinationName = exact.DestinationName

	args.Intention.FillPartitionAndNamespace(&entMeta, false)

	var ignored string
	if err := s.agent.RPC(req.Context(), "Intention.Apply", &args, &ignored); err != nil {
		return nil, err
	}

	return true, nil
}

// DELETE /v1/connect/intentions/exact
func (s *HTTPHandlers) IntentionDeleteExact(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var entMeta acl.EnterpriseMeta
	if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
		return nil, err
	}

	exact, err := parseIntentionQueryExact(req, &entMeta)
	if err != nil {
		return nil, err
	}

	args := structs.IntentionRequest{
		Op: structs.IntentionOpDelete,
		Intention: &structs.Intention{
			// NOTE: ID is explicitly empty here
			SourcePartition:      exact.SourcePartition,
			SourceNS:             exact.SourceNS,
			SourceName:           exact.SourceName,
			DestinationPartition: exact.DestinationPartition,
			DestinationNS:        exact.DestinationNS,
			DestinationName:      exact.DestinationName,
		},
	}
	s.parseDC(req, &args.Datacenter)
	s.parseToken(req, &args.Token)

	var ignored string
	if err := s.agent.RPC(req.Context(), "Intention.Apply", &args, &ignored); err != nil {
		return nil, err
	}

	return true, nil
}

// intentionCreateResponse is the response structure for creating an intention.
type intentionCreateResponse struct{ ID string }

func parseIntentionQueryExact(req *http.Request, entMeta *acl.EnterpriseMeta) (*structs.IntentionQueryExact, error) {
	q := req.URL.Query()

	// Extract the source/destination
	source, ok := q["source"]
	if !ok || len(source) != 1 || source[0] == "" {
		return nil, fmt.Errorf("required query parameter 'source' not set")
	}
	destination, ok := q["destination"]
	if !ok || len(destination) != 1 || destination[0] == "" {
		return nil, fmt.Errorf("required query parameter 'destination' not set")
	}

	var exact structs.IntentionQueryExact
	{
		parsed, err := parseIntentionStringComponent(source[0], entMeta, false)
		if err != nil {
			return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
		}
		exact.SourcePartition = parsed.ap
		exact.SourceNS = parsed.ns
		exact.SourceName = parsed.name
	}

	{
		parsed, err := parseIntentionStringComponent(destination[0], entMeta, false)
		if err != nil {
			return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
		}
		exact.DestinationPartition = parsed.ap
		exact.DestinationNS = parsed.ns
		exact.DestinationName = parsed.name
	}

	return &exact, nil
}

type parsedIntentionInput struct {
	peer, ap, ns, name string
}

func parseIntentionStringComponent(input string, entMeta *acl.EnterpriseMeta, allowPeerKeyword bool) (*parsedIntentionInput, error) {
	if strings.HasPrefix(input, "peer:") && !allowPeerKeyword {
		return nil, fmt.Errorf("cannot specify a peer here")
	}

	ss := strings.Split(input, "/")
	switch len(ss) {
	case 1: // Name only
		// need to specify at least the service name too
		if strings.HasPrefix(ss[0], "peer:") {
			return nil, fmt.Errorf("need to specify the service name as well")
		}

		ns := entMeta.NamespaceOrEmpty()
		ap := entMeta.PartitionOrEmpty()
		return &parsedIntentionInput{ap: ap, ns: ns, name: ss[0]}, nil
	case 2: // peer:peer/name OR namespace/name
		if strings.HasPrefix(ss[0], "peer:") {
			peerName := strings.TrimPrefix(ss[0], "peer:")
			ns := entMeta.NamespaceOrEmpty()

			return &parsedIntentionInput{peer: peerName, ns: ns, name: ss[1]}, nil
		}

		ap := entMeta.PartitionOrEmpty()
		return &parsedIntentionInput{ap: ap, ns: ss[0], name: ss[1]}, nil
	case 3: // peer:peer/namespace/name OR partition/namespace/name
		if strings.HasPrefix(ss[0], "peer:") {
			peerName := strings.TrimPrefix(ss[0], "peer:")
			return &parsedIntentionInput{peer: peerName, ns: ss[1], name: ss[2]}, nil
		} else {
			return &parsedIntentionInput{ap: ss[0], ns: ss[1], name: ss[2]}, nil
		}
	default:
		return nil, fmt.Errorf("input can contain at most two '/'")
	}
}

// IntentionSpecific handles the endpoint for /v1/connect/intentions/:id.
// Deprecated: use IntentionExact.
func (s *HTTPHandlers) IntentionSpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	id := strings.TrimPrefix(req.URL.Path, "/v1/connect/intentions/")

	switch req.Method {
	case "GET":
		return s.IntentionSpecificGet(id, resp, req)

	case "PUT":
		return s.IntentionSpecificUpdate(id, resp, req)

	case "DELETE":
		return s.IntentionSpecificDelete(id, resp, req)

	default:
		return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
	}
}

// Deprecated: use IntentionGetExact.
func (s *HTTPHandlers) IntentionSpecificGet(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	// Method is tested in IntentionEndpoint

	args := structs.IntentionQueryRequest{
		IntentionID: id,
	}
	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
		return nil, nil
	}

	var reply structs.IndexedIntentions
	if err := s.agent.RPC(req.Context(), "Intention.Get", &args, &reply); err != nil {
		// We have to check the string since the RPC sheds the error type
		if err.Error() == consul.ErrIntentionNotFound.Error() {
			return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: err.Error()}
		}

		// Not ideal, but there are a number of error scenarios that are not
		// user error (400). We look for a specific case of invalid UUID
		// to detect a parameter error and return a 400 response. The error
		// is not a constant type or message, so we have to use strings.Contains
		if strings.Contains(err.Error(), "UUID") {
			return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: err.Error()}
		}

		return nil, err
	}

	// This shouldn't happen since the called API documents it shouldn't,
	// but we check since the alternative if it happens is a panic.
	if len(reply.Intentions) == 0 {
		resp.WriteHeader(http.StatusNotFound)
		return nil, nil
	}

	return reply.Intentions[0], nil
}

// Deprecated: use IntentionPutExact.
func (s *HTTPHandlers) IntentionSpecificUpdate(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	// Method is tested in IntentionEndpoint

	var entMeta acl.EnterpriseMeta
	if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
		return nil, err
	}

	if entMeta.PartitionOrDefault() != acl.PartitionOrDefault("") {
		return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Cannot use a partition with this endpoint"}
	}

	args := structs.IntentionRequest{
		Op: structs.IntentionOpUpdate,
	}
	s.parseDC(req, &args.Datacenter)
	s.parseToken(req, &args.Token)
	if err := decodeBody(req.Body, &args.Intention); err != nil {
		return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Request decode failed: %v", err)}
	}

	if args.Intention.DestinationPartition != "" && args.Intention.DestinationPartition != "default" {
		return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Cannot specify a destination partition with this endpoint"}
	}
	if args.Intention.SourcePartition != "" && args.Intention.SourcePartition != "default" {
		return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Cannot specify a source partition with this endpoint"}
	}

	args.Intention.FillPartitionAndNamespace(&entMeta, false)

	// Use the ID from the URL
	args.Intention.ID = id

	var reply string
	if err := s.agent.RPC(req.Context(), "Intention.Apply", &args, &reply); err != nil {
		return nil, err
	}

	// Update uses the same create response
	return intentionCreateResponse{reply}, nil
}

// Deprecated: use IntentionDeleteExact.
func (s *HTTPHandlers) IntentionSpecificDelete(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	// Method is tested in IntentionEndpoint

	args := structs.IntentionRequest{
		Op:        structs.IntentionOpDelete,
		Intention: &structs.Intention{ID: id},
	}
	s.parseDC(req, &args.Datacenter)
	s.parseToken(req, &args.Token)

	var reply string
	if err := s.agent.RPC(req.Context(), "Intention.Apply", &args, &reply); err != nil {
		return nil, err
	}

	return true, nil
}