package permissions

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
	"golang.org/x/text/collate"
	"golang.org/x/text/language"
)

const (
	PermissionSetAssignmentPolicyComponentKind cp.ComponentKind = "permission_set_assignment_policy"
	PermissionSetAssignmentPolicySchema                         = "nexuscrm.permission-set-assignment-policy/v1"

	assignmentPolicySourceTargetDomain   = "permission_set_assignment_policy/source/utf8mb4_unicode_ci-v1"
	assignmentPolicySelectorTargetDomain = "permission_set_assignment_policy/selector/utf8mb4_unicode_ci-v1"
	assignmentPolicySerializationDomain  = "permission_set_assignment_policy/serialization/v1"
	assignmentPolicyMaximumSelectors     = 64
	assignmentPolicyMaximumIdentifier    = 255
)

// PermissionSetAssignmentPolicySerializationTarget is the stable, eagerly
// preparable target shared by policy declaration, operator, and product-source
// transitions. Product-source transactions cannot provision selector-specific
// markers after locking User/Profile rows, and their pre-lock view may race the
// first policy that introduces a selector. This target makes the locked replan
// authoritative without requiring a target discovered from that stale view.
func PermissionSetAssignmentPolicySerializationTarget() cp.ComponentManagedTarget {
	return cp.ComponentManagedTarget{
		TargetDomain:     assignmentPolicySerializationDomain,
		PhysicalKeyBytes: []byte("all"),
	}
}

// PermissionSetAssignmentPolicy is the strict v1 declaration consumed by the
// permissions-owned assignment reconciler. ProfilesAnyOf is an OR selector
// over Profile.Name; the DB-bound planner remains authoritative for collisions
// under the database's utf8mb4_unicode_ci collation.
type PermissionSetAssignmentPolicy struct {
	Schema        string   `json:"schema"`
	PermissionSet string   `json:"permissionSet"`
	ProfilesAnyOf []string `json:"profiles_any_of"`
}

// PermissionSetAssignmentPolicySource identifies one logical policy source.
// It is distinct from the physical Permission Set target claimed by that
// source, so two policies never serialize through a process-wide global key.
type PermissionSetAssignmentPolicySource struct {
	ownerPack string
	policyKey string
}

func NewPermissionSetAssignmentPolicySource(ownerPack, policyKey string) (PermissionSetAssignmentPolicySource, error) {
	if err := validateAssignmentPolicySourceIdentity("owner pack", ownerPack); err != nil {
		return PermissionSetAssignmentPolicySource{}, err
	}
	if err := validateAssignmentPolicySourceIdentity("policy key", policyKey); err != nil {
		return PermissionSetAssignmentPolicySource{}, err
	}
	return PermissionSetAssignmentPolicySource{ownerPack: ownerPack, policyKey: policyKey}, nil
}

func (s PermissionSetAssignmentPolicySource) OwnerPack() string { return s.ownerPack }

func (s PermissionSetAssignmentPolicySource) PolicyKey() string { return s.policyKey }

// ManagedTarget returns a delimiter-safe target identity for this logical
// source using the same Unicode primary-strength identity as its durable store.
func (s PermissionSetAssignmentPolicySource) ManagedTarget() cp.ComponentManagedTarget {
	return cp.ComponentManagedTarget{
		TargetDomain:     assignmentPolicySourceTargetDomain,
		PhysicalKeyBytes: assignmentPolicySourceIdentityBytes(s.ownerPack, s.policyKey),
	}
}

// ManagedTargets returns the two identities that every policy operation must
// serialize: its logical source and its physical Permission Set Definition.
func (s PermissionSetAssignmentPolicySource) ManagedTargets(policy PermissionSetAssignmentPolicy) ([]cp.ComponentManagedTarget, error) {
	definitionTarget, err := PermissionSetDefinitionManagedTarget(policy.PermissionSet)
	if err != nil {
		return nil, fmt.Errorf("assignment policy target: %w", err)
	}
	return []cp.ComponentManagedTarget{s.ManagedTarget(), definitionTarget}, nil
}

// assignmentPolicySelectorManagedTarget serializes every policy lifecycle
// writer and every eligibility transition that can change or consume the same
// selector-to-target mapping. The identity deliberately follows the durable
// selector store's collation rather than the caller's spelling.
func assignmentPolicySelectorManagedTarget(selector string) (cp.ComponentManagedTarget, error) {
	if err := validateAssignmentPolicyProfileName(selector); err != nil {
		return cp.ComponentManagedTarget{}, fmt.Errorf("assignment policy selector target: %w", err)
	}
	physicalKey := []byte(assignmentPolicyDatabaseIdentity(selector))
	if len(physicalKey) == 0 || len(physicalKey) > cp.ComponentManagedMaximumPhysicalKey {
		return cp.ComponentManagedTarget{}, fmt.Errorf(
			"assignment policy selector target exceeds %d bytes",
			cp.ComponentManagedMaximumPhysicalKey,
		)
	}
	return cp.ComponentManagedTarget{
		TargetDomain: assignmentPolicySelectorTargetDomain, PhysicalKeyBytes: physicalKey,
	}, nil
}

func assignmentPolicySelectorManagedTargets(selectorGroups ...[]string) ([]cp.ComponentManagedTarget, error) {
	targets := make([]cp.ComponentManagedTarget, 0)
	seen := make(map[string]struct{})
	for _, selectors := range selectorGroups {
		for _, selector := range selectors {
			target, err := assignmentPolicySelectorManagedTarget(selector)
			if err != nil {
				return nil, err
			}
			identity := target.TargetDomain + "\x00" + string(target.PhysicalKeyBytes)
			if _, duplicate := seen[identity]; duplicate {
				continue
			}
			seen[identity] = struct{}{}
			targets = append(targets, target)
		}
	}
	return targets, nil
}

// DecodePermissionSetAssignmentPolicy decodes and canonicalizes a declaration.
// The returned selector list owns its storage and has deterministic ordering.
func DecodePermissionSetAssignmentPolicy(data []byte) (PermissionSetAssignmentPolicy, error) {
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()
	var policy PermissionSetAssignmentPolicy
	if err := decoder.Decode(&policy); err != nil {
		return PermissionSetAssignmentPolicy{}, fmt.Errorf("assignment policy payload: %w", err)
	}
	if err := ensureAssignmentPolicyEOF(decoder); err != nil {
		return PermissionSetAssignmentPolicy{}, err
	}
	if policy.Schema != PermissionSetAssignmentPolicySchema {
		return PermissionSetAssignmentPolicy{}, fmt.Errorf(
			"assignment policy payload: schema = %q, want %q",
			policy.Schema,
			PermissionSetAssignmentPolicySchema,
		)
	}
	if _, err := PermissionSetDefinitionManagedTarget(policy.PermissionSet); err != nil {
		return PermissionSetAssignmentPolicy{}, fmt.Errorf("assignment policy payload: %w", err)
	}
	if len(policy.ProfilesAnyOf) == 0 || len(policy.ProfilesAnyOf) > assignmentPolicyMaximumSelectors {
		return PermissionSetAssignmentPolicy{}, fmt.Errorf(
			"assignment policy payload: profiles_any_of must contain 1..%d names",
			assignmentPolicyMaximumSelectors,
		)
	}
	seen := make(map[string]struct{}, len(policy.ProfilesAnyOf))
	for _, name := range policy.ProfilesAnyOf {
		if err := validateAssignmentPolicyProfileName(name); err != nil {
			return PermissionSetAssignmentPolicy{}, fmt.Errorf("assignment policy payload: %w", err)
		}
		key := assignmentPolicyDatabaseIdentity(name)
		if _, duplicate := seen[key]; duplicate {
			return PermissionSetAssignmentPolicy{}, fmt.Errorf(
				"assignment policy payload: duplicate profile %q under database collation",
				name,
			)
		}
		seen[key] = struct{}{}
	}
	sort.Slice(policy.ProfilesAnyOf, func(i, j int) bool {
		left := assignmentPolicyDatabaseIdentity(policy.ProfilesAnyOf[i])
		right := assignmentPolicyDatabaseIdentity(policy.ProfilesAnyOf[j])
		if left != right {
			return left < right
		}
		return policy.ProfilesAnyOf[i] < policy.ProfilesAnyOf[j]
	})
	return policy, nil
}

// assignmentPolicyDatabaseIdentity returns a stable primary-level Unicode
// collation key. The assignment-policy stores use utf8mb4_unicode_ci, whose
// logical identity is insensitive to case, accents, and character width. All
// in-memory set membership and managed-target serialization must use the same
// identity class or a value retained by the database can be released as stale.
func assignmentPolicyDatabaseIdentity(value string) string {
	collator := collate.New(
		language.Und,
		collate.IgnoreCase,
		collate.IgnoreDiacritics,
		collate.IgnoreWidth,
	)
	var buffer collate.Buffer
	return string(collator.KeyFromString(&buffer, value))
}

func assignmentPolicyDatabaseIdentityEqual(left, right string) bool {
	return assignmentPolicyDatabaseIdentity(left) == assignmentPolicyDatabaseIdentity(right)
}

func assignmentPolicySourceIdentityBytes(ownerPack, policyKey string) []byte {
	ownerIdentity := []byte(assignmentPolicyDatabaseIdentity(ownerPack))
	policyIdentity := []byte(assignmentPolicyDatabaseIdentity(policyKey))
	physicalKey := make([]byte, 4+len(ownerIdentity)+len(policyIdentity))
	binary.BigEndian.PutUint16(physicalKey[0:2], uint16(len(ownerIdentity)))
	copy(physicalKey[2:], ownerIdentity)
	offset := 2 + len(ownerIdentity)
	binary.BigEndian.PutUint16(physicalKey[offset:offset+2], uint16(len(policyIdentity)))
	copy(physicalKey[offset+2:], policyIdentity)
	return physicalKey
}

func validateAssignmentPolicySourceIdentity(field, value string) error {
	if len(value) == 0 || len(value) > assignmentPolicyMaximumIdentifier {
		return fmt.Errorf("assignment policy source: %s must contain 1..%d bytes", field, assignmentPolicyMaximumIdentifier)
	}
	if !utf8.ValidString(value) || strings.TrimSpace(value) != value {
		return fmt.Errorf("assignment policy source: %s must be valid UTF-8 without surrounding whitespace", field)
	}
	for _, character := range value {
		if unicode.IsControl(character) {
			return fmt.Errorf("assignment policy source: %s must not contain control characters", field)
		}
	}
	return nil
}

func validateAssignmentPolicyProfileName(name string) error {
	length := utf8.RuneCountInString(name)
	if !utf8.ValidString(name) || length == 0 || length > 255 {
		return fmt.Errorf("Profile name must contain 1..255 valid UTF-8 characters")
	}
	if strings.TrimSpace(name) != name {
		return fmt.Errorf("Profile name %q must not contain leading or trailing whitespace", name)
	}
	for _, character := range name {
		if unicode.IsControl(character) {
			return fmt.Errorf("Profile name %q must not contain control characters", name)
		}
	}
	return nil
}

func ensureAssignmentPolicyEOF(decoder *json.Decoder) error {
	var extra any
	if err := decoder.Decode(&extra); err == io.EOF {
		return nil
	} else if err != nil {
		return fmt.Errorf("assignment policy payload: %w", err)
	}
	return fmt.Errorf("assignment policy payload: trailing JSON value")
}
