package metadata

import (
	"sort"
	"strings"
	"sync"

	"github.com/nexuscrm/shared/pkg/models"
)

// Bucket classifies a related list candidate for editor display and seed logic.
type Bucket string

const (
	BucketRecommended Bucket = "recommended"
	BucketAdditional  Bucket = "additional"
	BucketSystem      Bucket = "system"
)

// RelatedListRule is the single metadata-side authority for related-list
// recommendation, ordering, synthetic default presentation, and optional
// runtime resolution overrides.
type RelatedListRule struct {
	Key               string                 `json:"key,omitempty"`
	ParentObjectAPI   string                 `json:"parentObjectApiName"`
	ChildObjectAPI    string                 `json:"childObjectApiName"`
	LookupField       string                 `json:"lookupField"`
	Bucket            Bucket                 `json:"bucket,omitempty"`
	SortOrder         int                    `json:"sortOrder,omitempty"`
	AppendIfMissing   bool                   `json:"appendIfMissing,omitempty"`
	Label             string                 `json:"label,omitempty"`
	SecondaryLabel    string                 `json:"secondaryLabel,omitempty"`
	LookupFieldLabel  string                 `json:"lookupFieldLabel,omitempty"`
	Fields            []string               `json:"fields,omitempty"`
	Resolution        *RelatedListResolution `json:"resolution,omitempty"`
	RuntimeAppendOnly bool                   `json:"-"`
}

var (
	packRelatedListRulesMu sync.RWMutex
	packRelatedListRules   map[string][]RelatedListRule

	tenantRelatedListRulesMu sync.RWMutex
	tenantRelatedListRules   []RelatedListRule
)

// RegisterPackRelatedListRules replaces one installed pack's active rule set.
// Pack payload is the SSoT; this process-global map is only a rebuildable
// runtime cache layered below tenant/admin overlays.
func RegisterPackRelatedListRules(owner string, rules []RelatedListRule) error {
	owner = strings.TrimSpace(owner)
	if owner == "" {
		return errInvalidRelatedListRule("missing owner")
	}
	if err := validateRelatedListRules(rules); err != nil {
		return err
	}

	copied := cloneRelatedListRules(rules)
	packRelatedListRulesMu.Lock()
	if packRelatedListRules == nil {
		packRelatedListRules = map[string][]RelatedListRule{}
	}
	if len(copied) == 0 {
		delete(packRelatedListRules, owner)
	} else {
		packRelatedListRules[owner] = copied
	}
	packRelatedListRulesMu.Unlock()
	return nil
}

// ValidatePackRelatedListRules validates app-pack-authored rules without
// mutating the runtime cache.
func ValidatePackRelatedListRules(rules []RelatedListRule) error {
	return validateRelatedListRules(rules)
}

// ClearPackRelatedListRules removes all installed-pack cache entries before a
// runtime rebuild from _System_InstalledPack.
func ClearPackRelatedListRules() {
	packRelatedListRulesMu.Lock()
	packRelatedListRules = nil
	packRelatedListRulesMu.Unlock()
}

// ClearTenantRelatedListRules removes the active tenant rule cache. L0-only
// control-plane paths use this to avoid carrying L2 runtime state across a
// process-local metadata rebuild.
func ClearTenantRelatedListRules() {
	tenantRelatedListRulesMu.Lock()
	tenantRelatedListRules = nil
	tenantRelatedListRulesMu.Unlock()
}

// RegisterTenantRelatedListRules replaces the active tenant rule set.
// The selected overlay directory is the only authoring surface for these rows,
// so replacement semantics are correct and keep the runtime registry single-owned.
func RegisterTenantRelatedListRules(rules []RelatedListRule) error {
	if err := validateRelatedListRules(rules); err != nil {
		return err
	}

	copied := cloneRelatedListRules(rules)
	tenantRelatedListRulesMu.Lock()
	tenantRelatedListRules = copied
	tenantRelatedListRulesMu.Unlock()
	return nil
}

// ValidateTenantRelatedListRules performs schema validation for tenant-authored
// rules without mutating the active runtime registry.
func ValidateTenantRelatedListRules(rules []RelatedListRule) error {
	return validateRelatedListRules(rules)
}

// ResetTenantRelatedListRulesForTest clears tenant registrations. Production
// code never calls this; tests use it to avoid package-global state leakage.
func ResetTenantRelatedListRulesForTest() {
	packRelatedListRulesMu.Lock()
	packRelatedListRules = nil
	packRelatedListRulesMu.Unlock()
	tenantRelatedListRulesMu.Lock()
	tenantRelatedListRules = nil
	tenantRelatedListRulesMu.Unlock()
}

func cloneRelatedListRules(rules []RelatedListRule) []RelatedListRule {
	if len(rules) == 0 {
		return nil
	}
	out := make([]RelatedListRule, 0, len(rules))
	for _, rule := range rules {
		cloned := rule
		if len(rule.Fields) > 0 {
			cloned.Fields = append([]string(nil), rule.Fields...)
		}
		if rule.Resolution != nil {
			resolution := *rule.Resolution
			if len(rule.Resolution.Create.PrefillMappings) > 0 {
				resolution.Create.PrefillMappings = make(map[string]string, len(rule.Resolution.Create.PrefillMappings))
				for k, v := range rule.Resolution.Create.PrefillMappings {
					resolution.Create.PrefillMappings[k] = v
				}
			}
			cloned.Resolution = &resolution
		}
		out = append(out, cloned)
	}
	return out
}

func validateRelatedListRules(rules []RelatedListRule) error {
	seenKeys := make(map[string]struct{}, len(rules))
	seenIdentities := make(map[string]struct{}, len(rules))
	for _, rule := range rules {
		if strings.TrimSpace(rule.ParentObjectAPI) == "" {
			return errInvalidRelatedListRule("missing parentObjectApiName")
		}
		if strings.TrimSpace(rule.ChildObjectAPI) == "" {
			return errInvalidRelatedListRule("missing childObjectApiName")
		}
		if strings.TrimSpace(rule.LookupField) == "" {
			return errInvalidRelatedListRule("missing lookupField")
		}
		bucket := normalizeRelatedListBucket(rule.Bucket)
		if bucket == "" {
			return errInvalidRelatedListRule("invalid bucket " + string(rule.Bucket))
		}
		if rule.Key != "" {
			key := strings.ToLower(rule.Key)
			if _, dup := seenKeys[key]; dup {
				return errInvalidRelatedListRule("duplicate key " + rule.Key)
			}
			seenKeys[key] = struct{}{}
		}
		identity := relatedListRuleIdentity(rule.ParentObjectAPI, rule.ChildObjectAPI, rule.LookupField)
		if _, dup := seenIdentities[identity]; dup {
			return errInvalidRelatedListRule("duplicate rule identity " + identity)
		}
		seenIdentities[identity] = struct{}{}
	}
	return nil
}

type invalidRelatedListRuleError struct {
	message string
}

func (e invalidRelatedListRuleError) Error() string {
	return "invalid related-list rule: " + e.message
}

func errInvalidRelatedListRule(message string) error {
	return invalidRelatedListRuleError{message: message}
}

func snapshotRelatedListRules() []RelatedListRule {
	packRelatedListRulesMu.RLock()
	packOwners := make([]string, 0, len(packRelatedListRules))
	for owner := range packRelatedListRules {
		packOwners = append(packOwners, owner)
	}
	sort.Strings(packOwners)
	var packRules []RelatedListRule
	for _, owner := range packOwners {
		packRules = append(packRules, cloneRelatedListRules(packRelatedListRules[owner])...)
	}
	packRelatedListRulesMu.RUnlock()

	tenantRelatedListRulesMu.RLock()
	tenant := cloneRelatedListRules(tenantRelatedListRules)
	tenantRelatedListRulesMu.RUnlock()

	merged := make([]RelatedListRule, 0, len(packRules)+len(tenant))
	identityIndex := make(map[string]int, len(packRules)+len(tenant))
	for _, rule := range packRules {
		identity := relatedListRuleIdentity(rule.ParentObjectAPI, rule.ChildObjectAPI, rule.LookupField)
		identityIndex[identity] = len(merged)
		merged = append(merged, rule)
	}
	for _, rule := range tenant {
		identity := relatedListRuleIdentity(rule.ParentObjectAPI, rule.ChildObjectAPI, rule.LookupField)
		if idx, ok := identityIndex[identity]; ok {
			merged[idx] = rule
			continue
		}
		identityIndex[identity] = len(merged)
		merged = append(merged, rule)
	}
	return merged
}

func relatedListRuleIdentity(parentObject, childObject, lookupField string) string {
	return strings.ToLower(parentObject) + "|" + strings.ToLower(childObject) + "|" + strings.ToLower(lookupField)
}

func relatedListDisplayIdentity(childObject, lookupField string) string {
	return strings.ToLower(childObject) + "|" + strings.ToLower(lookupField)
}

func normalizeRelatedListBucket(bucket Bucket) Bucket {
	switch bucket {
	case BucketRecommended, BucketAdditional, BucketSystem:
		return bucket
	case "":
		return BucketRecommended
	default:
		return ""
	}
}

func parentRuleSet(parentObject string) ([]RelatedListRule, map[string]RelatedListRule) {
	return parentRuleSetFromRules(parentObject, snapshotRelatedListRules())
}

func parentRuleSetFromRules(parentObject string, all []RelatedListRule) ([]RelatedListRule, map[string]RelatedListRule) {
	parentKey := strings.ToLower(parentObject)
	rules := make([]RelatedListRule, 0, len(all))
	ruleByIdentity := make(map[string]RelatedListRule)
	for _, rule := range all {
		if strings.ToLower(rule.ParentObjectAPI) != parentKey {
			continue
		}
		rule.Bucket = normalizeRelatedListBucket(rule.Bucket)
		rules = append(rules, rule)
		ruleByIdentity[relatedListDisplayIdentity(rule.ChildObjectAPI, rule.LookupField)] = rule
	}
	sort.SliceStable(rules, func(i, j int) bool {
		if rules[i].SortOrder != rules[j].SortOrder {
			return rules[i].SortOrder < rules[j].SortOrder
		}
		if !strings.EqualFold(rules[i].ChildObjectAPI, rules[j].ChildObjectAPI) {
			return strings.ToLower(rules[i].ChildObjectAPI) < strings.ToLower(rules[j].ChildObjectAPI)
		}
		return strings.ToLower(rules[i].LookupField) < strings.ToLower(rules[j].LookupField)
	})
	return rules, ruleByIdentity
}

func runtimeRuleSet(parentObject string) []RelatedListRule {
	rules, _ := parentRuleSet(parentObject)
	filtered := make([]RelatedListRule, 0, len(rules))
	for _, rule := range rules {
		if rule.AppendIfMissing && rule.RuntimeAppendOnly {
			filtered = append(filtered, rule)
		}
	}
	return filtered
}

// ClassifyCandidate determines the bucket for a related list candidate.
// Metadata-owned rules may curate buckets; otherwise generic relationship
// candidates stay additional until an installed pack or tenant overlay
// explicitly recommends them.
func ClassifyCandidate(parentObject, childObjectAPI, lookupField string) Bucket {
	rules, ruleByIdentity := parentRuleSet(parentObject)
	return classifyCandidateWithRules(childObjectAPI, lookupField, rules, ruleByIdentity)
}

func classifyCandidateWithRules(childObjectAPI, lookupField string, rules []RelatedListRule, ruleByIdentity map[string]RelatedListRule) Bucket {
	if rule, ok := ruleByIdentity[relatedListDisplayIdentity(childObjectAPI, lookupField)]; ok {
		return rule.Bucket
	}
	if IsSystemRelatedObject(childObjectAPI) {
		return BucketSystem
	}
	if len(rules) > 0 {
		return BucketAdditional
	}
	return BucketAdditional
}

// ResolveRelatedListCandidates normalizes bucket metadata, injects any missing
// synthetic candidates owned by the shared rule registry, and returns a stable
// ordered candidate slice.
func ResolveRelatedListCandidates(parentObject string, candidates []RelatedListCandidate) []RelatedListCandidate {
	rules, ruleByIdentity := parentRuleSet(parentObject)
	return resolveRelatedListCandidatesWithRules(candidates, rules, ruleByIdentity)
}

// ResolveRelatedListCandidatesWithRules resolves candidates against an explicit
// rules slice. Pack apply/verify paths use this to stay independent from the
// process-global runtime cache, which is rebuilt from installed packs at
// runtime startup.
func ResolveRelatedListCandidatesWithRules(parentObject string, candidates []RelatedListCandidate, rules []RelatedListRule) []RelatedListCandidate {
	parentRules, ruleByIdentity := parentRuleSetFromRules(parentObject, rules)
	return resolveRelatedListCandidatesWithRules(candidates, parentRules, ruleByIdentity)
}

func resolveRelatedListCandidatesWithRules(candidates []RelatedListCandidate, rules []RelatedListRule, ruleByIdentity map[string]RelatedListRule) []RelatedListCandidate {
	resolved := make([]RelatedListCandidate, 0, len(candidates)+len(rules))
	seen := make(map[string]bool, len(candidates)+len(rules))

	for _, candidate := range candidates {
		key := relatedListDisplayIdentity(candidate.ObjectAPIName, candidate.LookupField)
		if seen[key] {
			continue
		}
		seen[key] = true

		bucket := classifyCandidateWithRules(candidate.ObjectAPIName, candidate.LookupField, rules, ruleByIdentity)
		candidate.IsSystem = bucket == BucketSystem
		candidate.IsRecommended = bucket == BucketRecommended
		candidate.Bucket = string(bucket)

		if rule, ok := ruleByIdentity[key]; ok {
			if rule.Label != "" {
				candidate.Label = rule.Label
			}
			if rule.SecondaryLabel != "" {
				candidate.SecondaryLabel = rule.SecondaryLabel
			}
			if rule.LookupFieldLabel != "" {
				candidate.FieldLabel = rule.LookupFieldLabel
			}
			if len(rule.Fields) > 0 {
				candidate.Fields = append([]string(nil), rule.Fields...)
			}
		}

		resolved = append(resolved, candidate)
	}

	for _, rule := range rules {
		if !rule.AppendIfMissing {
			continue
		}
		key := relatedListDisplayIdentity(rule.ChildObjectAPI, rule.LookupField)
		if seen[key] {
			continue
		}
		seen[key] = true
		resolved = append(resolved, candidateFromRule(rule))
	}

	sort.SliceStable(resolved, func(i, j int) bool {
		bi, bj := bucketPriority(resolved[i].Bucket), bucketPriority(resolved[j].Bucket)
		if bi != bj {
			return bi < bj
		}

		ri, iHasRule := ruleByIdentity[relatedListDisplayIdentity(resolved[i].ObjectAPIName, resolved[i].LookupField)]
		rj, jHasRule := ruleByIdentity[relatedListDisplayIdentity(resolved[j].ObjectAPIName, resolved[j].LookupField)]
		if iHasRule && jHasRule && ri.SortOrder != rj.SortOrder {
			return ri.SortOrder < rj.SortOrder
		}
		if iHasRule != jHasRule {
			return iHasRule
		}
		if resolved[i].Label != resolved[j].Label {
			return resolved[i].Label < resolved[j].Label
		}
		return resolved[i].LookupField < resolved[j].LookupField
	})

	return resolved
}

func candidateFromRule(rule RelatedListRule) RelatedListCandidate {
	fieldLabel := rule.LookupFieldLabel
	if fieldLabel == "" && strings.HasPrefix(rule.SecondaryLabel, "via ") {
		fieldLabel = strings.TrimPrefix(rule.SecondaryLabel, "via ")
	}
	if fieldLabel == "" {
		fieldLabel = rule.LookupField
	}
	secondaryLabel := rule.SecondaryLabel
	if secondaryLabel == "" && fieldLabel != "" {
		secondaryLabel = "via " + fieldLabel
	}
	return RelatedListCandidate{
		ID:             rule.ChildObjectAPI + "-" + rule.LookupField,
		ObjectAPIName:  rule.ChildObjectAPI,
		LookupField:    rule.LookupField,
		Label:          rule.Label,
		SecondaryLabel: secondaryLabel,
		Fields:         append([]string(nil), rule.Fields...),
		FieldLabel:     fieldLabel,
		IsSystem:       rule.Bucket == BucketSystem,
		IsRecommended:  rule.Bucket == BucketRecommended,
		Bucket:         string(rule.Bucket),
	}
}

// ClassifyCandidatesWithFallback classifies a batch of candidates. Objects with
// curated rules get recommended/additional buckets; uncurated objects leave all
// non-system candidates as additional so empty layouts stay empty by default.
func ClassifyCandidatesWithFallback(parentObject string, candidates []RelatedListCandidate) {
	resolved := ResolveRelatedListCandidates(parentObject, candidates)
	copy(candidates, resolved[:len(candidates)])
}

// bucketPriority returns a sort key for candidate buckets: recommended < additional < system.
func bucketPriority(bucket string) int {
	switch Bucket(bucket) {
	case BucketRecommended:
		return 0
	case BucketAdditional:
		return 1
	case BucketSystem:
		return 2
	default:
		return 1
	}
}

// SortByCuratedOrder reorders related list configs to match the shared rule
// order. Items not in the rules keep their relative order after ordered items.
func SortByCuratedOrder(parentObject string, rls []models.RelatedListConfig) {
	rules, ruleByIdentity := parentRuleSet(parentObject)
	sortByCuratedOrderWithRules(rls, rules, ruleByIdentity)
}

// SortByCuratedOrderWithRules reorders related lists using an explicit rule
// slice instead of the process-global installed-pack cache.
func SortByCuratedOrderWithRules(parentObject string, rls []models.RelatedListConfig, rules []RelatedListRule) {
	parentRules, ruleByIdentity := parentRuleSetFromRules(parentObject, rules)
	sortByCuratedOrderWithRules(rls, parentRules, ruleByIdentity)
}

func sortByCuratedOrderWithRules(rls []models.RelatedListConfig, rules []RelatedListRule, ruleByIdentity map[string]RelatedListRule) {
	if len(rules) == 0 || len(rls) == 0 {
		return
	}
	sort.SliceStable(rls, func(i, j int) bool {
		ri, iHasRule := ruleByIdentity[relatedListDisplayIdentity(rls[i].ObjectAPIName, rls[i].LookupField)]
		rj, jHasRule := ruleByIdentity[relatedListDisplayIdentity(rls[j].ObjectAPIName, rls[j].LookupField)]
		switch {
		case iHasRule && jHasRule:
			if ri.SortOrder != rj.SortOrder {
				return ri.SortOrder < rj.SortOrder
			}
		case iHasRule != jHasRule:
			return iHasRule
		default:
			return false
		}
		return false
	})
}
