package cache

import (
	"context"
	"encoding/hex"

	"go.opentelemetry.io/otel/metric"

	"github.com/cespare/xxhash/v2"

	"github.com/Permify/permify/internal"
	"github.com/Permify/permify/internal/engines"
	"github.com/Permify/permify/internal/invoke"
	"github.com/Permify/permify/internal/storage"
	"github.com/Permify/permify/pkg/cache"
	base "github.com/Permify/permify/pkg/pb/base/v1"
	"github.com/Permify/permify/pkg/telemetry"
)

// CheckEngineWithCache is a struct that holds an instance of a cache.Cache for managing engine cache.
type CheckEngineWithCache struct {
	// schemaReader is responsible for reading schema information
	schemaReader storage.SchemaReader
	checker      invoke.Check
	cache        cache.Cache

	// Metrics
	cacheHitHistogram metric.Int64Histogram
}

// NewCheckEngineWithCache creates a new instance of EngineKeyManager by initializing an EngineKeys
// struct with the provided cache.Cache instance.
func NewCheckEngineWithCache(
	checker invoke.Check,
	schemaReader storage.SchemaReader,
	cache cache.Cache,
) invoke.Check {
	return &CheckEngineWithCache{
		schemaReader:      schemaReader,
		checker:           checker,
		cache:             cache,
		cacheHitHistogram: telemetry.NewHistogram(internal.Meter, "cache_hit", "amount", "Number of cache hits"),
	}
}

// Check performs a permission check for a given request, using the cached results if available.
func (c *CheckEngineWithCache) Check(ctx context.Context, request *base.PermissionCheckRequest) (response *base.PermissionCheckResponse, err error) {
	// Retrieve entity definition
	var en *base.EntityDefinition
	en, _, err = c.schemaReader.ReadEntityDefinition(ctx, request.GetTenantId(), request.GetEntity().GetType(), request.GetMetadata().GetSchemaVersion())
	if err != nil {
		return &base.PermissionCheckResponse{
			Can: base.CheckResult_CHECK_RESULT_DENIED,
			Metadata: &base.PermissionCheckResponseMetadata{
				CheckCount: 0,
			},
		}, err
	}

	isRelational := engines.IsRelational(en, request.GetPermission())

	// Try to get the cached result for the given request.
	res, found := c.getCheckKey(request, isRelational)

	// If a cached result is found, handle exclusion and return the result.
	if found {
		// Increase the hit count in the metrics.
		c.cacheHitHistogram.Record(ctx, 1)

		// If the request doesn't have the exclusion flag set, return the cached result.
		return &base.PermissionCheckResponse{
			Can:      res.GetCan(),
			Metadata: &base.PermissionCheckResponseMetadata{},
		}, nil
	}

	// Perform the actual permission check using the provided request.
	cres, err := c.checker.Check(ctx, request)
	// Check if there's an error or the response is nil, and return the result.
	if err != nil {
		return &base.PermissionCheckResponse{
			Can: base.CheckResult_CHECK_RESULT_DENIED,
			Metadata: &base.PermissionCheckResponseMetadata{
				CheckCount: 0,
			},
		}, err
	}

	// Add to histogram the response

	c.setCheckKey(request, &base.PermissionCheckResponse{
		Can:      cres.GetCan(),
		Metadata: &base.PermissionCheckResponseMetadata{},
	}, isRelational)
	// Return the result of the permission check.
	return cres, err
}

// GetCheckKey retrieves the value for the given key from the EngineKeys cache.
// It returns the PermissionCheckResponse if the key is found, and a boolean value
// indicating whether the key was found or not.
func (c *CheckEngineWithCache) getCheckKey(key *base.PermissionCheckRequest, isRelational bool) (*base.PermissionCheckResponse, bool) {
	if key == nil {
		// If either the key or value is nil, return false
		return nil, false
	}

	// Initialize a new xxhash object
	h := xxhash.New()

	// Write the checkKey string to the hash object
	_, err := h.Write([]byte(engines.GenerateKey(key, isRelational)))
	if err != nil {
		// If there's an error, return nil and false
		return nil, false
	}

	// Generate the final cache key by encoding the hash object's sum as a hexadecimal string
	k := hex.EncodeToString(h.Sum(nil))

	// Get the value from the cache using the generated cache key
	resp, found := c.cache.Get(k)

	// If the key is found, return the value and true
	if found {
		// If permission is granted, return allowed response
		return &base.PermissionCheckResponse{
			Can: resp.(base.CheckResult),
			Metadata: &base.PermissionCheckResponseMetadata{
				CheckCount: 0,
			},
		}, true
	}

	// If the key is not found, return nil and false
	return nil, false
}

// setCheckKey is a function to set a check key in the cache of the CheckEngineWithKeys.
// It takes a permission check request as a key, a permission check response as a value,
// and returns a boolean value indicating if the operation was successful.
func (c *CheckEngineWithCache) setCheckKey(key *base.PermissionCheckRequest, value *base.PermissionCheckResponse, isRelational bool) bool {
	// If either the key or the value is nil, return false.
	if key == nil || value == nil {
		return false
	}

	// Create a new xxhash object for hashing.
	h := xxhash.New()

	// Generate a key string from the permission check request and write it to the hash.
	// If there's an error while writing to the hash, return false.
	size, err := h.Write([]byte(engines.GenerateKey(key, isRelational)))
	if err != nil {
		return false
	}

	// Compute the hash sum and encode it as a hexadecimal string.
	k := hex.EncodeToString(h.Sum(nil))

	// Set the hashed key and the check result in the cache, using the size of the hashed key as an expiry.
	// The Set method should return true if the operation was successful, so return the result.
	return c.cache.Set(k, value.GetCan(), int64(size))
}
