package policy

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"sync"

	"git.mleku.dev/mleku/nostr/encoders/hex"
	"lol.mleku.dev/log"
	"next.orly.dev/pkg/utils"
)

// =============================================================================
// Policy Composition Types
// =============================================================================

// PolicyAdminContribution represents extensions/additions from a policy admin.
// Policy admins can extend the base owner policy but cannot modify protected fields
// (owners, policy_admins) or reduce owner-granted permissions.
type PolicyAdminContribution struct {
	// AdminPubkey is the hex-encoded pubkey of the policy admin who made this contribution
	AdminPubkey string `json:"admin_pubkey"`
	// CreatedAt is the Unix timestamp when this contribution was created
	CreatedAt int64 `json:"created_at"`
	// EventID is the Nostr event ID that created this contribution (for audit trail)
	EventID string `json:"event_id,omitempty"`

	// KindWhitelistAdd adds kinds to the whitelist (OR with owner's whitelist)
	KindWhitelistAdd []int `json:"kind_whitelist_add,omitempty"`
	// KindBlacklistAdd adds kinds to the blacklist (overrides whitelist)
	KindBlacklistAdd []int `json:"kind_blacklist_add,omitempty"`

	// RulesExtend extends existing rules defined by the owner
	RulesExtend map[int]RuleExtension `json:"rules_extend,omitempty"`
	// RulesAdd adds new rules for kinds not defined by the owner
	RulesAdd map[int]Rule `json:"rules_add,omitempty"`

	// GlobalExtend extends the global rule
	GlobalExtend *RuleExtension `json:"global_extend,omitempty"`
}

// RuleExtension defines how a policy admin can extend an existing owner rule.
// All fields are additive - they extend, not replace, the owner's configuration.
type RuleExtension struct {
	// WriteAllowAdd adds pubkeys to the write allow list
	WriteAllowAdd []string `json:"write_allow_add,omitempty"`
	// WriteDenyAdd adds pubkeys to the write deny list (overrides allow)
	WriteDenyAdd []string `json:"write_deny_add,omitempty"`
	// ReadAllowAdd adds pubkeys to the read allow list
	ReadAllowAdd []string `json:"read_allow_add,omitempty"`
	// ReadDenyAdd adds pubkeys to the read deny list (overrides allow)
	ReadDenyAdd []string `json:"read_deny_add,omitempty"`

	// SizeLimitOverride can only make the limit MORE permissive (larger)
	SizeLimitOverride *int64 `json:"size_limit_override,omitempty"`
	// ContentLimitOverride can only make the limit MORE permissive (larger)
	ContentLimitOverride *int64 `json:"content_limit_override,omitempty"`
	// MaxAgeOfEventOverride can only make the limit MORE permissive (older allowed)
	MaxAgeOfEventOverride *int64 `json:"max_age_of_event_override,omitempty"`
	// MaxAgeEventInFutureOverride can only make the limit MORE permissive (further future allowed)
	MaxAgeEventInFutureOverride *int64 `json:"max_age_event_in_future_override,omitempty"`

	// WriteAllowFollows extends the follow whitelist feature
	WriteAllowFollows *bool `json:"write_allow_follows,omitempty"`
	// FollowsWhitelistAdminsAdd adds admin pubkeys whose follows are whitelisted
	FollowsWhitelistAdminsAdd []string `json:"follows_whitelist_admins_add,omitempty"`
}

// ComposedPolicy manages the base owner policy and policy admin contributions.
// It computes an effective merged policy at runtime.
type ComposedPolicy struct {
	// OwnerPolicy is the base policy set by owners
	OwnerPolicy *P
	// Contributions is a map of event ID -> contribution for deduplication
	Contributions map[string]*PolicyAdminContribution
	// contributionsMx protects the contributions map
	contributionsMx sync.RWMutex
	// configDir is the directory where policy files are stored
	configDir string
}

// =============================================================================
// Protected Field Validation
// =============================================================================

// ProtectedFields are fields that only owners can modify
var ProtectedFields = []string{"owners", "policy_admins"}

// ValidateOwnerPolicy validates a policy update from an owner.
// Ensures owners list is non-empty.
func ValidateOwnerPolicy(policy *P) error {
	if len(policy.Owners) == 0 {
		return fmt.Errorf("owners list cannot be empty: at least one owner must be defined")
	}

	// Validate all owner pubkeys are valid hex
	for _, owner := range policy.Owners {
		if len(owner) != 64 {
			return fmt.Errorf("invalid owner pubkey length: %q (expected 64 hex characters)", owner)
		}
		if _, err := hex.Dec(owner); err != nil {
			return fmt.Errorf("invalid owner pubkey format: %q: %v", owner, err)
		}
	}

	// Validate all policy admin pubkeys are valid hex
	for _, admin := range policy.PolicyAdmins {
		if len(admin) != 64 {
			return fmt.Errorf("invalid policy_admin pubkey length: %q (expected 64 hex characters)", admin)
		}
		if _, err := hex.Dec(admin); err != nil {
			return fmt.Errorf("invalid policy_admin pubkey format: %q: %v", admin, err)
		}
	}

	return nil
}

// ValidatePolicyAdminContribution validates a contribution from a policy admin.
// Ensures no protected fields are modified and extensions are valid.
func ValidatePolicyAdminContribution(
	ownerPolicy *P,
	contribution *PolicyAdminContribution,
	existingContributions map[string]*PolicyAdminContribution,
) error {
	// Validate the admin pubkey is valid
	if len(contribution.AdminPubkey) != 64 {
		return fmt.Errorf("invalid admin pubkey length")
	}

	// Validate kind additions don't conflict with owner blacklist
	// (though PA can add to blacklist to override whitelist)

	// Validate rule extensions
	for kind, ext := range contribution.RulesExtend {
		ownerRule, exists := ownerPolicy.rules[kind]
		if !exists {
			return fmt.Errorf("cannot extend rule for kind %d: not defined in owner policy (use rules_add instead)", kind)
		}

		// Validate size limit overrides are more permissive
		if ext.SizeLimitOverride != nil && ownerRule.SizeLimit != nil {
			if *ext.SizeLimitOverride < *ownerRule.SizeLimit {
				return fmt.Errorf("size_limit_override for kind %d must be >= owner's limit (%d)", kind, *ownerRule.SizeLimit)
			}
		}

		if ext.ContentLimitOverride != nil && ownerRule.ContentLimit != nil {
			if *ext.ContentLimitOverride < *ownerRule.ContentLimit {
				return fmt.Errorf("content_limit_override for kind %d must be >= owner's limit (%d)", kind, *ownerRule.ContentLimit)
			}
		}

		if ext.MaxAgeOfEventOverride != nil && ownerRule.MaxAgeOfEvent != nil {
			if *ext.MaxAgeOfEventOverride < *ownerRule.MaxAgeOfEvent {
				return fmt.Errorf("max_age_of_event_override for kind %d must be >= owner's limit (%d)", kind, *ownerRule.MaxAgeOfEvent)
			}
		}

		// Validate pubkey formats in allow/deny lists
		for _, pk := range ext.WriteAllowAdd {
			if len(pk) != 64 {
				return fmt.Errorf("invalid pubkey in write_allow_add for kind %d: %q", kind, pk)
			}
		}
		for _, pk := range ext.WriteDenyAdd {
			if len(pk) != 64 {
				return fmt.Errorf("invalid pubkey in write_deny_add for kind %d: %q", kind, pk)
			}
		}
		for _, pk := range ext.ReadAllowAdd {
			if len(pk) != 64 {
				return fmt.Errorf("invalid pubkey in read_allow_add for kind %d: %q", kind, pk)
			}
		}
		for _, pk := range ext.ReadDenyAdd {
			if len(pk) != 64 {
				return fmt.Errorf("invalid pubkey in read_deny_add for kind %d: %q", kind, pk)
			}
		}
	}

	// Validate rules_add are for kinds not already defined by owner
	for kind := range contribution.RulesAdd {
		if _, exists := ownerPolicy.rules[kind]; exists {
			return fmt.Errorf("cannot add rule for kind %d: already defined in owner policy (use rules_extend instead)", kind)
		}
	}

	return nil
}

// =============================================================================
// Policy Composition Logic
// =============================================================================

// NewComposedPolicy creates a new composed policy from an owner policy.
func NewComposedPolicy(ownerPolicy *P, configDir string) *ComposedPolicy {
	return &ComposedPolicy{
		OwnerPolicy:   ownerPolicy,
		Contributions: make(map[string]*PolicyAdminContribution),
		configDir:     configDir,
	}
}

// AddContribution adds a policy admin contribution.
// Returns error if validation fails.
func (cp *ComposedPolicy) AddContribution(contribution *PolicyAdminContribution) error {
	cp.contributionsMx.Lock()
	defer cp.contributionsMx.Unlock()

	// Validate the contribution
	if err := ValidatePolicyAdminContribution(cp.OwnerPolicy, contribution, cp.Contributions); err != nil {
		return err
	}

	// Store the contribution
	cp.Contributions[contribution.EventID] = contribution

	// Persist to disk
	if err := cp.saveContribution(contribution); err != nil {
		log.W.F("failed to persist contribution: %v", err)
	}

	return nil
}

// RemoveContribution removes a policy admin contribution by event ID.
func (cp *ComposedPolicy) RemoveContribution(eventID string) {
	cp.contributionsMx.Lock()
	defer cp.contributionsMx.Unlock()

	delete(cp.Contributions, eventID)

	// Remove from disk
	if cp.configDir != "" {
		contribPath := filepath.Join(cp.configDir, "policy-contributions", eventID+".json")
		os.Remove(contribPath)
	}
}

// GetEffectivePolicy computes the merged effective policy.
// Composition rules:
// - Whitelists are unioned (OR)
// - Blacklists are unioned and override whitelists
// - Limits use the most permissive value
// - Conflicts between PAs: oldest created_at wins (except deny always wins)
func (cp *ComposedPolicy) GetEffectivePolicy() *P {
	cp.contributionsMx.RLock()
	defer cp.contributionsMx.RUnlock()

	// Clone the owner policy as base
	effective := cp.cloneOwnerPolicy()

	// Sort contributions by created_at (oldest first for conflict resolution)
	sorted := cp.getSortedContributions()

	// Apply each contribution
	for _, contrib := range sorted {
		cp.applyContribution(effective, contrib)
	}

	// Repopulate binary caches
	effective.Global.populateBinaryCache()
	for kind := range effective.rules {
		rule := effective.rules[kind]
		rule.populateBinaryCache()
		effective.rules[kind] = rule
	}

	return effective
}

// cloneOwnerPolicy creates a deep copy of the owner policy.
func (cp *ComposedPolicy) cloneOwnerPolicy() *P {
	// Marshal and unmarshal to create a deep copy
	data, _ := json.Marshal(cp.OwnerPolicy)
	var cloned P
	json.Unmarshal(data, &cloned)

	// Copy the manager reference (not cloned)
	cloned.manager = cp.OwnerPolicy.manager

	return &cloned
}

// getSortedContributions returns contributions sorted by created_at.
func (cp *ComposedPolicy) getSortedContributions() []*PolicyAdminContribution {
	sorted := make([]*PolicyAdminContribution, 0, len(cp.Contributions))
	for _, contrib := range cp.Contributions {
		sorted = append(sorted, contrib)
	}
	sort.Slice(sorted, func(i, j int) bool {
		return sorted[i].CreatedAt < sorted[j].CreatedAt
	})
	return sorted
}

// applyContribution applies a single contribution to the effective policy.
func (cp *ComposedPolicy) applyContribution(effective *P, contrib *PolicyAdminContribution) {
	// Apply kind whitelist additions (OR)
	for _, kind := range contrib.KindWhitelistAdd {
		if !containsInt(effective.Kind.Whitelist, kind) {
			effective.Kind.Whitelist = append(effective.Kind.Whitelist, kind)
		}
	}

	// Apply kind blacklist additions (OR, overrides whitelist)
	for _, kind := range contrib.KindBlacklistAdd {
		if !containsInt(effective.Kind.Blacklist, kind) {
			effective.Kind.Blacklist = append(effective.Kind.Blacklist, kind)
		}
	}

	// Apply rule extensions
	for kind, ext := range contrib.RulesExtend {
		if rule, exists := effective.rules[kind]; exists {
			cp.applyRuleExtension(&rule, &ext, contrib.CreatedAt)
			effective.rules[kind] = rule
		}
	}

	// Apply new rules
	for kind, rule := range contrib.RulesAdd {
		if _, exists := effective.rules[kind]; !exists {
			if effective.rules == nil {
				effective.rules = make(map[int]Rule)
			}
			effective.rules[kind] = rule
		}
	}

	// Apply global rule extension
	if contrib.GlobalExtend != nil {
		cp.applyRuleExtension(&effective.Global, contrib.GlobalExtend, contrib.CreatedAt)
	}
}

// applyRuleExtension applies a rule extension to an existing rule.
func (cp *ComposedPolicy) applyRuleExtension(rule *Rule, ext *RuleExtension, _ int64) {
	// Add to allow lists (OR)
	for _, pk := range ext.WriteAllowAdd {
		if !containsString(rule.WriteAllow, pk) {
			rule.WriteAllow = append(rule.WriteAllow, pk)
		}
	}
	for _, pk := range ext.ReadAllowAdd {
		if !containsString(rule.ReadAllow, pk) {
			rule.ReadAllow = append(rule.ReadAllow, pk)
		}
	}

	// Add to deny lists (OR, overrides allow) - deny always wins
	for _, pk := range ext.WriteDenyAdd {
		if !containsString(rule.WriteDeny, pk) {
			rule.WriteDeny = append(rule.WriteDeny, pk)
		}
	}
	for _, pk := range ext.ReadDenyAdd {
		if !containsString(rule.ReadDeny, pk) {
			rule.ReadDeny = append(rule.ReadDeny, pk)
		}
	}

	// Apply limit overrides (most permissive wins)
	if ext.SizeLimitOverride != nil {
		if rule.SizeLimit == nil || *ext.SizeLimitOverride > *rule.SizeLimit {
			rule.SizeLimit = ext.SizeLimitOverride
		}
	}
	if ext.ContentLimitOverride != nil {
		if rule.ContentLimit == nil || *ext.ContentLimitOverride > *rule.ContentLimit {
			rule.ContentLimit = ext.ContentLimitOverride
		}
	}
	if ext.MaxAgeOfEventOverride != nil {
		if rule.MaxAgeOfEvent == nil || *ext.MaxAgeOfEventOverride > *rule.MaxAgeOfEvent {
			rule.MaxAgeOfEvent = ext.MaxAgeOfEventOverride
		}
	}
	if ext.MaxAgeEventInFutureOverride != nil {
		if rule.MaxAgeEventInFuture == nil || *ext.MaxAgeEventInFutureOverride > *rule.MaxAgeEventInFuture {
			rule.MaxAgeEventInFuture = ext.MaxAgeEventInFutureOverride
		}
	}

	// Enable WriteAllowFollows if requested (OR logic)
	if ext.WriteAllowFollows != nil && *ext.WriteAllowFollows {
		rule.WriteAllowFollows = true
	}

	// Add to follows whitelist admins
	for _, pk := range ext.FollowsWhitelistAdminsAdd {
		if !containsString(rule.FollowsWhitelistAdmins, pk) {
			rule.FollowsWhitelistAdmins = append(rule.FollowsWhitelistAdmins, pk)
		}
	}
}

// =============================================================================
// Persistence
// =============================================================================

// saveContribution persists a contribution to disk.
func (cp *ComposedPolicy) saveContribution(contrib *PolicyAdminContribution) error {
	if cp.configDir == "" {
		return nil
	}

	contribDir := filepath.Join(cp.configDir, "policy-contributions")
	if err := os.MkdirAll(contribDir, 0755); err != nil {
		return err
	}

	contribPath := filepath.Join(contribDir, contrib.EventID+".json")
	data, err := json.MarshalIndent(contrib, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(contribPath, data, 0644)
}

// LoadContributions loads all contributions from disk.
func (cp *ComposedPolicy) LoadContributions() error {
	if cp.configDir == "" {
		return nil
	}

	contribDir := filepath.Join(cp.configDir, "policy-contributions")
	if _, err := os.Stat(contribDir); os.IsNotExist(err) {
		return nil // No contributions yet
	}

	entries, err := os.ReadDir(contribDir)
	if err != nil {
		return err
	}

	cp.contributionsMx.Lock()
	defer cp.contributionsMx.Unlock()

	for _, entry := range entries {
		if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
			continue
		}

		contribPath := filepath.Join(contribDir, entry.Name())
		data, err := os.ReadFile(contribPath)
		if err != nil {
			log.W.F("failed to read contribution %s: %v", entry.Name(), err)
			continue
		}

		var contrib PolicyAdminContribution
		if err := json.Unmarshal(data, &contrib); err != nil {
			log.W.F("failed to parse contribution %s: %v", entry.Name(), err)
			continue
		}

		// Validate against current owner policy
		if err := ValidatePolicyAdminContribution(cp.OwnerPolicy, &contrib, cp.Contributions); err != nil {
			log.W.F("contribution %s is no longer valid: %v (skipping)", entry.Name(), err)
			continue
		}

		cp.Contributions[contrib.EventID] = &contrib
	}

	log.I.F("loaded %d policy admin contributions", len(cp.Contributions))
	return nil
}

// =============================================================================
// Owner Detection
// =============================================================================

// IsOwner checks if the given pubkey is an owner.
// The pubkey parameter should be binary ([]byte), not hex-encoded.
func (p *P) IsOwner(pubkey []byte) bool {
	if len(pubkey) == 0 {
		return false
	}

	p.followsMx.RLock()
	defer p.followsMx.RUnlock()

	for _, owner := range p.ownersBin {
		if utils.FastEqual(owner, pubkey) {
			return true
		}
	}
	return false
}

// IsOwnerOrPolicyAdmin checks if the given pubkey is an owner or policy admin.
// The pubkey parameter should be binary ([]byte), not hex-encoded.
func (p *P) IsOwnerOrPolicyAdmin(pubkey []byte) bool {
	return p.IsOwner(pubkey) || p.IsPolicyAdmin(pubkey)
}

// =============================================================================
// Helper Functions
// =============================================================================

func containsInt(slice []int, val int) bool {
	for _, v := range slice {
		if v == val {
			return true
		}
	}
	return false
}

func containsString(slice []string, val string) bool {
	for _, v := range slice {
		if v == val {
			return true
		}
	}
	return false
}
