// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0

package cache

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"sort"
	"strings"
	"sync"

	corev1 "k8s.io/api/core/v1"

	"github.com/projectcapsule/capsule/pkg/api/rules"
	"github.com/projectcapsule/capsule/pkg/api/runtime"
)

type RuleSet struct {
	ID       string
	Compiled []CompiledRule
}

type CompiledRule struct {
	Match runtime.ExpressionMatch

	// RegexID is empty when Match.Expression is empty.
	RegexID string

	AllowedPolicy map[corev1.PullPolicy]struct{} // nil/empty => allow any
}

func (r *CompiledRule) AllowsPullPolicy(pullPolicy corev1.PullPolicy) bool {
	if len(r.AllowedPolicy) == 0 {
		return true
	}

	_, ok := r.AllowedPolicy[pullPolicy]

	return ok
}

type RegistryRuleSetCache struct {
	regexCache *RegexCache

	mu sync.RWMutex
	rs map[string]*RuleSet
}

func NewRegistryRuleSetCache(regexCache *RegexCache) *RegistryRuleSetCache {
	if regexCache == nil {
		regexCache = NewRegexCache()
	}

	return &RegistryRuleSetCache{
		regexCache: regexCache,
		rs:         make(map[string]*RuleSet),
	}
}

func (c *RegistryRuleSetCache) GetOrBuild(specRules []rules.OCIRegistry) (rs *RuleSet, fromCache bool, err error) {
	if len(specRules) == 0 {
		return nil, false, nil
	}

	if c == nil {
		return nil, false, fmt.Errorf("registry rule set cache is nil")
	}

	id := c.HashRules(specRules)

	c.mu.RLock()
	rs = c.rs[id]
	c.mu.RUnlock()

	if rs != nil {
		return rs, true, nil
	}

	built, err := c.buildRuleSet(id, specRules)
	if err != nil {
		return nil, false, err
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	if c.rs == nil {
		c.rs = make(map[string]*RuleSet)
	}

	if rs = c.rs[id]; rs != nil {
		return rs, true, nil
	}

	c.rs[id] = built

	return built, false, nil
}

// Match matches reference, expression and pullPolicy.
// Admission evaluation should usually use MatchReference instead.
func (c *RegistryRuleSetCache) Match(
	specRules []rules.OCIRegistry,
	reference string,
	pullPolicy corev1.PullPolicy,
) (*CompiledRule, error) {
	rs, _, err := c.GetOrBuild(specRules)
	if err != nil {
		return nil, err
	}

	if rs == nil {
		return nil, nil
	}

	return c.MatchRuleSet(rs, reference, pullPolicy)
}

func (c *RegistryRuleSetCache) MatchRuleSet(
	rs *RuleSet,
	reference string,
	pullPolicy corev1.PullPolicy,
) (*CompiledRule, error) {
	if c == nil {
		return nil, fmt.Errorf("registry rule set cache is nil")
	}

	if c.regexCache == nil {
		return nil, fmt.Errorf("regex cache is nil")
	}

	if rs == nil {
		return nil, nil
	}

	for i := range rs.Compiled {
		rule := &rs.Compiled[i]

		if !rule.AllowsPullPolicy(pullPolicy) {
			continue
		}

		matched, err := rule.Match.MatchesWithExpressionMatcher(c.regexCache, reference)
		if err != nil {
			return nil, err
		}

		if matched {
			return rule, nil
		}
	}

	return nil, nil
}

// MatchReference matches reference only.
// It intentionally does not check pullPolicy.
func (c *RegistryRuleSetCache) MatchReference(
	rs *RuleSet,
	reference string,
) (*CompiledRule, error) {
	if c == nil {
		return nil, fmt.Errorf("registry rule set cache is nil")
	}

	if c.regexCache == nil {
		return nil, fmt.Errorf("regex cache is nil")
	}

	if rs == nil {
		return nil, nil
	}

	for i := range rs.Compiled {
		rule := &rs.Compiled[i]

		matched, err := rule.Match.MatchesWithExpressionMatcher(c.regexCache, reference)
		if err != nil {
			return nil, err
		}

		if matched {
			return rule, nil
		}
	}

	return nil, nil
}

func (c *RegistryRuleSetCache) Stats() int {
	if c == nil {
		return 0
	}

	c.mu.RLock()
	defer c.mu.RUnlock()

	return len(c.rs)
}

func (c *RegistryRuleSetCache) PruneActive(activeIDs map[string]struct{}) int {
	if c == nil {
		return 0
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	removed := 0

	for id := range c.rs {
		if _, ok := activeIDs[id]; ok {
			continue
		}

		delete(c.rs, id)

		removed++
	}

	return removed
}

func (c *RegistryRuleSetCache) HashRules(specRules []rules.OCIRegistry) string {
	var b strings.Builder

	b.Grow(len(specRules) * 160)

	const (
		sepRule  = "\n"
		sepField = "\x1f"
		sepList  = "\x1e"
	)

	for _, r := range specRules {
		match := r.ExpressionMatch

		exact := append([]string(nil), match.Exact...)
		sort.Strings(exact)

		policies := make([]string, 0, len(r.Policy))
		for _, p := range r.Policy {
			policies = append(policies, strings.TrimSpace(string(p)))
		}

		sort.Strings(policies)

		b.WriteString("exact")
		b.WriteString(sepField)

		for i, v := range exact {
			if i > 0 {
				b.WriteString(sepList)
			}

			b.WriteString(v)
		}

		b.WriteString(sepField)
		b.WriteString("exp")
		b.WriteString(sepField)
		b.WriteString(strings.TrimSpace(match.Expression))

		b.WriteString(sepField)
		b.WriteString("negate")
		b.WriteString(sepField)

		if match.Negate {
			b.WriteString("1")
		} else {
			b.WriteString("0")
		}

		b.WriteString(sepField)
		b.WriteString("policy")
		b.WriteString(sepField)

		for i, p := range policies {
			if i > 0 {
				b.WriteString(sepList)
			}

			b.WriteString(p)
		}

		b.WriteString(sepRule)
	}

	sum := sha256.Sum256([]byte(b.String()))

	return hex.EncodeToString(sum[:])
}

func (c *RegistryRuleSetCache) Has(id string) bool {
	if c == nil {
		return false
	}

	c.mu.RLock()
	defer c.mu.RUnlock()

	_, ok := c.rs[id]

	return ok
}

func (c *RegistryRuleSetCache) Reset() {
	if c == nil {
		return
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	c.rs = make(map[string]*RuleSet)
}

//nolint:unused
func (c *RegistryRuleSetCache) insertForTest(id string) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if c.rs == nil {
		c.rs = make(map[string]*RuleSet)
	}

	c.rs[id] = &RuleSet{ID: id}
}

func (c *RegistryRuleSetCache) buildRuleSet(id string, specRules []rules.OCIRegistry) (*RuleSet, error) {
	if c.regexCache == nil {
		return nil, fmt.Errorf("regex cache is nil")
	}

	rs := &RuleSet{
		ID:       id,
		Compiled: make([]CompiledRule, 0, len(specRules)),
	}

	for _, r := range specRules {
		match := r.ExpressionMatch

		if len(match.Exact) == 0 && strings.TrimSpace(match.Expression) == "" {
			return nil, fmt.Errorf("registry rule must define at least one of exact or exp")
		}

		cr := CompiledRule{
			Match: match,
		}

		if strings.TrimSpace(match.Expression) != "" {
			compiled, _, err := c.regexCache.GetOrCompile(match.ExpressionRegex)
			if err != nil {
				return nil, err
			}

			cr.RegexID = compiled.ID
		}

		if len(r.Policy) > 0 {
			cr.AllowedPolicy = make(map[corev1.PullPolicy]struct{}, len(r.Policy))

			for _, p := range r.Policy {
				cr.AllowedPolicy[p] = struct{}{}
			}
		}

		rs.Compiled = append(rs.Compiled, cr)
	}

	return rs, nil
}
