package acl

import (
	"fmt"
	"strings"
)

type EnforcementDecision int

const (
	// Deny returned from an Authorizer enforcement method indicates
	// that a corresponding rule was found and that access should be denied
	Deny EnforcementDecision = iota
	// Allow returned from an Authorizer enforcement method indicates
	// that a corresponding rule was found and that access should be allowed
	Allow
	// Default returned from an Authorizer enforcement method indicates
	// that a corresponding rule was not found and that whether access
	// should be granted or denied should be deferred to the default
	// access level
	Default
)

func (d EnforcementDecision) String() string {
	switch d {
	case Allow:
		return "Allow"
	case Deny:
		return "Deny"
	case Default:
		return "Default"
	default:
		return "Unknown"
	}
}

type Resource string

const (
	ResourceACL       Resource = "acl"
	ResourceAgent     Resource = "agent"
	ResourceEvent     Resource = "event"
	ResourceIntention Resource = "intention"
	ResourceKey       Resource = "key"
	ResourceKeyring   Resource = "keyring"
	ResourceNode      Resource = "node"
	ResourceOperator  Resource = "operator"
	ResourceQuery     Resource = "query"
	ResourceService   Resource = "service"
	ResourceSession   Resource = "session"
)

// Authorizer is the interface for policy enforcement.
type Authorizer interface {
	// ACLRead checks for permission to list all the ACLs
	ACLRead(*AuthorizerContext) EnforcementDecision

	// ACLWrite checks for permission to manipulate ACLs
	ACLWrite(*AuthorizerContext) EnforcementDecision

	// AgentRead checks for permission to read from agent endpoints for a
	// given node.
	AgentRead(string, *AuthorizerContext) EnforcementDecision

	// AgentWrite checks for permission to make changes via agent endpoints
	// for a given node.
	AgentWrite(string, *AuthorizerContext) EnforcementDecision

	// EventRead determines if a specific event can be queried.
	EventRead(string, *AuthorizerContext) EnforcementDecision

	// EventWrite determines if a specific event may be fired.
	EventWrite(string, *AuthorizerContext) EnforcementDecision

	// IntentionDefaultAllow determines the default authorized behavior
	// when no intentions match a Connect request.
	IntentionDefaultAllow(*AuthorizerContext) EnforcementDecision

	// IntentionRead determines if a specific intention can be read.
	IntentionRead(string, *AuthorizerContext) EnforcementDecision

	// IntentionWrite determines if a specific intention can be
	// created, modified, or deleted.
	IntentionWrite(string, *AuthorizerContext) EnforcementDecision

	// KeyList checks for permission to list keys under a prefix
	KeyList(string, *AuthorizerContext) EnforcementDecision

	// KeyRead checks for permission to read a given key
	KeyRead(string, *AuthorizerContext) EnforcementDecision

	// KeyWrite checks for permission to write a given key
	KeyWrite(string, *AuthorizerContext) EnforcementDecision

	// KeyWritePrefix checks for permission to write to an
	// entire key prefix. This means there must be no sub-policies
	// that deny a write.
	KeyWritePrefix(string, *AuthorizerContext) EnforcementDecision

	// KeyringRead determines if the encryption keyring used in
	// the gossip layer can be read.
	KeyringRead(*AuthorizerContext) EnforcementDecision

	// KeyringWrite determines if the keyring can be manipulated
	KeyringWrite(*AuthorizerContext) EnforcementDecision

	// NodeRead checks for permission to read (discover) a given node.
	NodeRead(string, *AuthorizerContext) EnforcementDecision

	// NodeReadAll checks for permission to read (discover) all nodes.
	NodeReadAll(*AuthorizerContext) EnforcementDecision

	// NodeWrite checks for permission to create or update (register) a
	// given node.
	NodeWrite(string, *AuthorizerContext) EnforcementDecision

	// OperatorRead determines if the read-only Consul operator functions
	// can be used.
	OperatorRead(*AuthorizerContext) EnforcementDecision

	// OperatorWrite determines if the state-changing Consul operator
	// functions can be used.
	OperatorWrite(*AuthorizerContext) EnforcementDecision

	// PreparedQueryRead determines if a specific prepared query can be read
	// to show its contents (this is not used for execution).
	PreparedQueryRead(string, *AuthorizerContext) EnforcementDecision

	// PreparedQueryWrite determines if a specific prepared query can be
	// created, modified, or deleted.
	PreparedQueryWrite(string, *AuthorizerContext) EnforcementDecision

	// ServiceRead checks for permission to read a given service
	ServiceRead(string, *AuthorizerContext) EnforcementDecision

	// ServiceReadAll checks for permission to read all services
	ServiceReadAll(*AuthorizerContext) EnforcementDecision

	// ServiceWrite checks for permission to create or update a given
	// service
	ServiceWrite(string, *AuthorizerContext) EnforcementDecision

	// SessionRead checks for permission to read sessions for a given node.
	SessionRead(string, *AuthorizerContext) EnforcementDecision

	// SessionWrite checks for permission to create sessions for a given
	// node.
	SessionWrite(string, *AuthorizerContext) EnforcementDecision

	// Snapshot checks for permission to take and restore snapshots.
	Snapshot(*AuthorizerContext) EnforcementDecision

	// Embedded Interface for Consul Enterprise specific ACL enforcement
	enterpriseAuthorizer
}

func Enforce(authz Authorizer, rsc Resource, segment string, access string, ctx *AuthorizerContext) (EnforcementDecision, error) {
	lowerAccess := strings.ToLower(access)

	switch rsc {
	case ResourceACL:
		switch lowerAccess {
		case "read":
			return authz.ACLRead(ctx), nil
		case "write":
			return authz.ACLWrite(ctx), nil
		}
	case ResourceAgent:
		switch lowerAccess {
		case "read":
			return authz.AgentRead(segment, ctx), nil
		case "write":
			return authz.AgentWrite(segment, ctx), nil
		}
	case ResourceEvent:
		switch lowerAccess {
		case "read":
			return authz.EventRead(segment, ctx), nil
		case "write":
			return authz.EventWrite(segment, ctx), nil
		}
	case ResourceIntention:
		switch lowerAccess {
		case "read":
			return authz.IntentionRead(segment, ctx), nil
		case "write":
			return authz.IntentionWrite(segment, ctx), nil
		}
	case ResourceKey:
		switch lowerAccess {
		case "read":
			return authz.KeyRead(segment, ctx), nil
		case "list":
			return authz.KeyList(segment, ctx), nil
		case "write":
			return authz.KeyWrite(segment, ctx), nil
		case "write-prefix":
			return authz.KeyWritePrefix(segment, ctx), nil
		}
	case ResourceKeyring:
		switch lowerAccess {
		case "read":
			return authz.KeyringRead(ctx), nil
		case "write":
			return authz.KeyringWrite(ctx), nil
		}
	case ResourceNode:
		switch lowerAccess {
		case "read":
			return authz.NodeRead(segment, ctx), nil
		case "write":
			return authz.NodeWrite(segment, ctx), nil
		}
	case ResourceOperator:
		switch lowerAccess {
		case "read":
			return authz.OperatorRead(ctx), nil
		case "write":
			return authz.OperatorWrite(ctx), nil
		}
	case ResourceQuery:
		switch lowerAccess {
		case "read":
			return authz.PreparedQueryRead(segment, ctx), nil
		case "write":
			return authz.PreparedQueryWrite(segment, ctx), nil
		}
	case ResourceService:
		switch lowerAccess {
		case "read":
			return authz.ServiceRead(segment, ctx), nil
		case "write":
			return authz.ServiceWrite(segment, ctx), nil
		}
	case ResourceSession:
		switch lowerAccess {
		case "read":
			return authz.SessionRead(segment, ctx), nil
		case "write":
			return authz.SessionWrite(segment, ctx), nil
		}
	default:
		if processed, decision, err := enforceEnterprise(authz, rsc, segment, lowerAccess, ctx); processed {
			return decision, err
		}
		return Deny, fmt.Errorf("Invalid ACL resource requested: %q", rsc)
	}

	return Deny, fmt.Errorf("Invalid access level for %s resource: %s", rsc, access)
}

// NewAuthorizerFromRules is a convenience function to invoke NewPolicyFromSource followed by NewPolicyAuthorizer with
// the parse policy.
func NewAuthorizerFromRules(id string, revision uint64, rules string, syntax SyntaxVersion, conf *Config, meta *EnterprisePolicyMeta) (Authorizer, error) {
	policy, err := NewPolicyFromSource(id, revision, rules, syntax, conf, meta)
	if err != nil {
		return nil, err
	}

	return NewPolicyAuthorizer([]*Policy{policy}, conf)
}
