package consul

import (
	"fmt"
	"time"

	"github.com/armon/go-metrics"
	"github.com/armon/go-metrics/prometheus"
	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/consul/state"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/lib"
	"github.com/hashicorp/go-memdb"
)

var ACLEndpointLegacySummaries = []prometheus.SummaryDefinition{
	{
		Name: []string{"acl", "apply"},
		Help: "Measures the time it takes to complete an update to the ACL store.",
	},
}

// Bootstrap is used to perform a one-time ACL bootstrap operation on
// a cluster to get the first management token.
func (a *ACL) Bootstrap(args *structs.DCSpecificRequest, reply *structs.ACL) error {
	if done, err := a.srv.ForwardRPC("ACL.Bootstrap", args, args, reply); done {
		return err
	}

	// Verify we are allowed to serve this request
	if !a.srv.InACLDatacenter() {
		return acl.ErrDisabled
	}

	if err := a.srv.aclBootstrapAllowed(); err != nil {
		return err
	}

	// By doing some pre-checks we can head off later bootstrap attempts
	// without having to run them through Raft, which should curb abuse.
	state := a.srv.fsm.State()
	allowed, _, err := state.CanBootstrapACLToken()
	if err != nil {
		return err
	}
	if !allowed {
		return structs.ACLBootstrapNotAllowedErr
	}

	// Propose a new token.
	token, err := lib.GenerateUUID(a.srv.checkTokenUUID)
	if err != nil {
		return fmt.Errorf("failed to make random token: %v", err)
	}

	// Attempt a bootstrap.
	req := structs.ACLRequest{
		Datacenter: a.srv.config.ACLDatacenter,
		Op:         structs.ACLBootstrapNow,
		ACL: structs.ACL{
			ID:   token,
			Name: "Bootstrap Token",
			Type: structs.ACLTokenTypeManagement,
		},
	}
	resp, err := a.srv.raftApply(structs.ACLRequestType, &req)
	if err != nil {
		return err
	}
	switch v := resp.(type) {
	case error:
		return v

	case *structs.ACL:
		*reply = *v

	default:
		// Just log this, since it looks like the bootstrap may have
		// completed.
		a.logger.Error("Unexpected response during bootstrap", "type", fmt.Sprintf("%T", v))
	}

	a.logger.Info("ACL bootstrap completed")
	return nil
}

// aclApplyInternal is used to apply an ACL request after it has been vetted that
// this is a valid operation. It is used when users are updating ACLs, in which
// case we check their token to make sure they have management privileges. It is
// also used for ACL replication. We want to run the replicated ACLs through the
// same checks on the change itself.
func aclApplyInternal(srv *Server, args *structs.ACLRequest, reply *string) error {
	// All ACLs must have an ID by this point.
	if args.ACL.ID == "" {
		return fmt.Errorf("Missing ACL ID")
	}

	switch args.Op {
	case structs.ACLSet:
		// Verify the ACL type
		switch args.ACL.Type {
		case structs.ACLTokenTypeClient:
		case structs.ACLTokenTypeManagement:
		default:
			return fmt.Errorf("Invalid ACL Type")
		}

		// No need to check expiration times as those did not exist in legacy tokens.
		_, existing, _ := srv.fsm.State().ACLTokenGetBySecret(nil, args.ACL.ID, nil)
		if existing != nil && existing.UsesNonLegacyFields() {
			return fmt.Errorf("Cannot use legacy endpoint to modify a non-legacy token")
		}

		// Verify this is not a root ACL
		if acl.RootAuthorizer(args.ACL.ID) != nil {
			return acl.PermissionDeniedError{Cause: "Cannot modify root ACL"}
		}

		// Ensure that we allow more permissive rule formats for legacy tokens,
		// but that we correct them on the way into the system.
		//
		// DEPRECATED (ACL-Legacy-Compat)
		correctedRules := structs.SanitizeLegacyACLTokenRules(args.ACL.Rules)
		if correctedRules != "" {
			args.ACL.Rules = correctedRules
		}

		// Validate the rules compile
		_, err := acl.NewPolicyFromSource("", 0, args.ACL.Rules, acl.SyntaxLegacy, srv.aclConfig, nil)
		if err != nil {
			return fmt.Errorf("ACL rule compilation failed: %v", err)
		}

	case structs.ACLDelete:
		if args.ACL.ID == anonymousToken {
			return acl.PermissionDeniedError{Cause: "Cannot delete anonymous token"}
		}

	default:
		return fmt.Errorf("Invalid ACL Operation")
	}

	// Apply the update
	resp, err := srv.raftApply(structs.ACLRequestType, args)
	if err != nil {
		srv.logger.Error("Raft apply failed", "acl_op", args.Op, "error", err)
		return err
	}
	if respErr, ok := resp.(error); ok {
		return respErr
	}

	// Check if the return type is a string
	if respString, ok := resp.(string); ok {
		*reply = respString
	}

	return nil
}

// Apply is used to apply a modifying request to the data store. This should
// only be used for operations that modify the data
func (a *ACL) Apply(args *structs.ACLRequest, reply *string) error {
	if done, err := a.srv.ForwardRPC("ACL.Apply", args, args, reply); done {
		return err
	}
	defer metrics.MeasureSince([]string{"acl", "apply"}, time.Now())

	// Verify we are allowed to serve this request
	if !a.srv.config.ACLsEnabled {
		return acl.ErrDisabled
	}

	// Verify token is permitted to modify ACLs
	// NOTE: We will not support enterprise authorizer contexts with legacy ACLs
	if rule, err := a.srv.ResolveToken(args.Token); err != nil {
		return err
	} else if rule == nil || rule.ACLWrite(nil) != acl.Allow {
		return acl.ErrPermissionDenied
	}

	// If no ID is provided, generate a new ID. This must be done prior to
	// appending to the Raft log, because the ID is not deterministic. Once
	// the entry is in the log, the state update MUST be deterministic or
	// the followers will not converge.
	if args.Op == structs.ACLSet && args.ACL.ID == "" {
		var err error
		args.ACL.ID, err = lib.GenerateUUID(a.srv.checkTokenUUID)
		if err != nil {
			return err
		}
	}

	// Do the apply now that this update is vetted.
	if err := aclApplyInternal(a.srv, args, reply); err != nil {
		return err
	}

	// Clear the cache if applicable
	if args.ACL.ID != "" {
		a.srv.acls.cache.RemoveIdentity(tokenSecretCacheID(args.ACL.ID))
	}

	return nil
}

// Get is used to retrieve a single ACL
func (a *ACL) Get(args *structs.ACLSpecificRequest,
	reply *structs.IndexedACLs) error {
	if done, err := a.srv.ForwardRPC("ACL.Get", args, args, reply); done {
		return err
	}

	// NOTE: This has no ACL check because legacy ACLs were managed with
	// the secrets and therefore the argument to the Get request is
	// authorization in and of itself.

	// Verify we are allowed to serve this request
	if !a.srv.config.ACLsEnabled {
		return acl.ErrDisabled
	}

	return a.srv.blockingQuery(&args.QueryOptions,
		&reply.QueryMeta,
		func(ws memdb.WatchSet, state *state.Store) error {
			index, token, err := state.ACLTokenGetBySecret(ws, args.ACL, nil)
			if err != nil {
				return err
			}

			// Converting an ACLToken to an ACL will return nil and an error
			// (which we ignore) when it is unconvertible.
			//
			// This also means we won't have to check expiration times since
			// any legacy tokens never had expiration times and no non-legacy
			// tokens can be converted.

			var acl *structs.ACL
			if token != nil {
				acl, _ = token.Convert()
			}

			reply.Index = index
			if acl != nil {
				reply.ACLs = structs.ACLs{acl}
			} else {
				reply.ACLs = nil
			}
			return nil
		})
}

// List is used to list all the ACLs
func (a *ACL) List(args *structs.DCSpecificRequest,
	reply *structs.IndexedACLs) error {
	if done, err := a.srv.ForwardRPC("ACL.List", args, args, reply); done {
		return err
	}

	// Verify we are allowed to serve this request
	if !a.srv.config.ACLsEnabled {
		return acl.ErrDisabled
	}

	// Verify token is permitted to list ACLs
	// NOTES: Previously with legacy ACL there was no read-only ACL permissions
	// and this check for ACLWrite is basically what it did before.
	if rule, err := a.srv.ResolveToken(args.Token); err != nil {
		return err
	} else if rule == nil || rule.ACLWrite(nil) != acl.Allow {
		return acl.ErrPermissionDenied
	}

	return a.srv.blockingQuery(&args.QueryOptions,
		&reply.QueryMeta,
		func(ws memdb.WatchSet, state *state.Store) error {
			index, tokens, err := state.ACLTokenList(ws, false, true, "", "", "", nil, nil)
			if err != nil {
				return err
			}

			now := time.Now()

			var acls structs.ACLs
			for _, token := range tokens {
				if token.IsExpired(now) {
					continue
				}
				if acl, err := token.Convert(); err == nil && acl != nil {
					acls = append(acls, acl)
				}
			}

			reply.Index, reply.ACLs = index, acls
			return nil
		})
}
