package acl

import (
	"crypto/md5"
	"fmt"

	"github.com/hashicorp/consul/sentinel"
	"github.com/hashicorp/golang-lru"
)

// FaultFunc is a function used to fault in the parent,
// rules for an ACL given its ID
type FaultFunc func(id string) (string, string, error)

// aclEntry allows us to store the ACL with it's policy ID
type aclEntry struct {
	ACL    ACL
	Parent string
	RuleID string
}

// Cache is used to implement policy and ACL caching
type Cache struct {
	faultfn     FaultFunc
	aclCache    *lru.TwoQueueCache // Cache id -> acl
	policyCache *lru.TwoQueueCache // Cache policy -> acl
	ruleCache   *lru.TwoQueueCache // Cache rules -> policy
	sentinel    sentinel.Evaluator
}

// NewCache constructs a new policy and ACL cache of a given size
func NewCache(size int, faultfn FaultFunc, sentinel sentinel.Evaluator) (*Cache, error) {
	if size <= 0 {
		return nil, fmt.Errorf("Must provide positive cache size")
	}

	rc, err := lru.New2Q(size)
	if err != nil {
		return nil, err
	}

	pc, err := lru.New2Q(size)
	if err != nil {
		return nil, err
	}

	ac, err := lru.New2Q(size)
	if err != nil {
		return nil, err
	}

	c := &Cache{
		faultfn:     faultfn,
		aclCache:    ac,
		policyCache: pc,
		ruleCache:   rc,
		sentinel:    sentinel,
	}
	return c, nil
}

// GetPolicy is used to get a potentially cached policy set.
// If not cached, it will be parsed, and then cached.
func (c *Cache) GetPolicy(rules string) (*Policy, error) {
	return c.getPolicy(RuleID(rules), rules)
}

// getPolicy is an internal method to get a cached policy,
// but it assumes a pre-computed ID
func (c *Cache) getPolicy(id, rules string) (*Policy, error) {
	raw, ok := c.ruleCache.Get(id)
	if ok {
		return raw.(*Policy), nil
	}
	policy, err := Parse(rules, c.sentinel)
	if err != nil {
		return nil, err
	}
	policy.ID = id
	c.ruleCache.Add(id, policy)
	return policy, nil

}

// RuleID is used to generate an ID for a rule
func RuleID(rules string) string {
	return fmt.Sprintf("%x", md5.Sum([]byte(rules)))
}

// policyID returns the cache ID for a policy
func (c *Cache) policyID(parent, ruleID string) string {
	return parent + ":" + ruleID
}

// GetACLPolicy is used to get the potentially cached ACL
// policy. If not cached, it will be generated and then cached.
func (c *Cache) GetACLPolicy(id string) (string, *Policy, error) {
	// Check for a cached acl
	if raw, ok := c.aclCache.Get(id); ok {
		cached := raw.(aclEntry)
		if raw, ok := c.ruleCache.Get(cached.RuleID); ok {
			return cached.Parent, raw.(*Policy), nil
		}
	}

	// Fault in the rules
	parent, rules, err := c.faultfn(id)
	if err != nil {
		return "", nil, err
	}

	// Get cached
	policy, err := c.GetPolicy(rules)
	return parent, policy, err
}

// GetACL is used to get a potentially cached ACL policy.
// If not cached, it will be generated and then cached.
func (c *Cache) GetACL(id string) (ACL, error) {
	// Look for the ACL directly
	raw, ok := c.aclCache.Get(id)
	if ok {
		return raw.(aclEntry).ACL, nil
	}

	// Get the rules
	parentID, rules, err := c.faultfn(id)
	if err != nil {
		return nil, err
	}
	ruleID := RuleID(rules)

	// Check for a compiled ACL
	policyID := c.policyID(parentID, ruleID)
	var compiled ACL
	if raw, ok := c.policyCache.Get(policyID); ok {
		compiled = raw.(ACL)
	} else {
		// Get the policy
		policy, err := c.getPolicy(ruleID, rules)
		if err != nil {
			return nil, err
		}

		// Get the parent ACL
		parent := RootACL(parentID)
		if parent == nil {
			parent, err = c.GetACL(parentID)
			if err != nil {
				return nil, err
			}
		}

		// Compile the ACL
		acl, err := New(parent, policy, c.sentinel)
		if err != nil {
			return nil, err
		}

		// Cache the compiled ACL
		c.policyCache.Add(policyID, acl)
		compiled = acl
	}

	// Cache and return the ACL
	c.aclCache.Add(id, aclEntry{compiled, parentID, ruleID})
	return compiled, nil
}

// ClearACL is used to clear the ACL cache if any
func (c *Cache) ClearACL(id string) {
	c.aclCache.Remove(id)
}

// Purge is used to clear all the ACL caches. The
// rule and policy caches are not purged, since they
// are content-hashed anyways.
func (c *Cache) Purge() {
	c.aclCache.Purge()
}
