package metadata

import (
	"fmt"
	"sort"
	"strings"
	"sync"
)

// SearchScopeOrderItem is one pack-authored entry in the global-search scope
// dropdown order.
type SearchScopeOrderItem struct {
	Name  string `json:"name"`
	Label string `json:"label,omitempty"`
}

var (
	packSearchScopeOrdersMu sync.RWMutex
	packSearchScopeOrders   map[string][]SearchScopeOrderItem
)

// RegisterPackSearchScopeOrder replaces one installed pack's active search
// scope order. Pack payload is the SSoT; this process-global map is only a
// rebuildable runtime cache.
func RegisterPackSearchScopeOrder(owner string, items []SearchScopeOrderItem) error {
	owner = strings.TrimSpace(owner)
	if owner == "" {
		return errInvalidSearchScopeOrder("missing owner")
	}
	if err := validateSearchScopeOrder(items); err != nil {
		return err
	}

	copied := cloneSearchScopeOrderItems(items)
	packSearchScopeOrdersMu.Lock()
	if packSearchScopeOrders == nil {
		packSearchScopeOrders = map[string][]SearchScopeOrderItem{}
	}
	if err := validateSearchScopeOrderConflictsLocked(owner, copied); err != nil {
		packSearchScopeOrdersMu.Unlock()
		return err
	}
	if len(copied) == 0 {
		delete(packSearchScopeOrders, owner)
	} else {
		packSearchScopeOrders[owner] = copied
	}
	packSearchScopeOrdersMu.Unlock()
	return nil
}

// ValidatePackSearchScopeOrder validates pack-authored scope-order payloads
// without mutating the runtime cache.
func ValidatePackSearchScopeOrder(items []SearchScopeOrderItem) error {
	return validateSearchScopeOrder(items)
}

// ValidatePackSearchScopeOrderOwnership ensures a pack only declares objects it
// owns through its object_spec payloads.
func ValidatePackSearchScopeOrderOwnership(items []SearchScopeOrderItem, ownedObjectAPINames []string) error {
	if len(items) == 0 {
		return nil
	}

	owned := make(map[string]string, len(ownedObjectAPINames))
	for _, objectAPIName := range ownedObjectAPINames {
		objectAPIName = strings.TrimSpace(objectAPIName)
		if objectAPIName == "" {
			continue
		}
		owned[strings.ToLower(objectAPIName)] = objectAPIName
	}

	for _, item := range items {
		name := strings.TrimSpace(item.Name)
		if name == "" {
			continue
		}
		if _, ok := owned[strings.ToLower(name)]; !ok {
			return errInvalidSearchScopeOrder(fmt.Sprintf("object %s is not owned by the declaring pack", name))
		}
	}

	return nil
}

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

// ResetPackSearchScopeOrdersForTest clears pack registrations. Production code
// never calls this; tests use it to avoid package-global state leakage.
func ResetPackSearchScopeOrdersForTest() {
	packSearchScopeOrdersMu.Lock()
	packSearchScopeOrders = nil
	packSearchScopeOrdersMu.Unlock()
}

// PackSearchScopeObjectNames returns the merged installed-pack dropdown order.
// The order is stable by pack name, then by item position inside each pack.
func PackSearchScopeObjectNames() []string {
	items := snapshotPackSearchScopeOrders()
	out := make([]string, 0, len(items))
	for _, item := range items {
		if name := strings.TrimSpace(item.Name); name != "" {
			out = append(out, name)
		}
	}
	return out
}

func cloneSearchScopeOrderItems(items []SearchScopeOrderItem) []SearchScopeOrderItem {
	if len(items) == 0 {
		return nil
	}
	out := make([]SearchScopeOrderItem, 0, len(items))
	for _, item := range items {
		out = append(out, SearchScopeOrderItem{
			Name:  strings.TrimSpace(item.Name),
			Label: strings.TrimSpace(item.Label),
		})
	}
	return out
}

func validateSearchScopeOrder(items []SearchScopeOrderItem) error {
	seen := make(map[string]struct{}, len(items))
	for _, item := range items {
		name := strings.TrimSpace(item.Name)
		if name == "" {
			return errInvalidSearchScopeOrder("missing name")
		}
		key := strings.ToLower(name)
		if _, dup := seen[key]; dup {
			return errInvalidSearchScopeOrder("duplicate name " + name)
		}
		seen[key] = struct{}{}
	}
	return nil
}

type invalidSearchScopeOrderError struct {
	message string
}

func (e invalidSearchScopeOrderError) Error() string {
	return "invalid search-scope-order: " + e.message
}

func errInvalidSearchScopeOrder(message string) error {
	return invalidSearchScopeOrderError{message: message}
}

func snapshotPackSearchScopeOrders() []SearchScopeOrderItem {
	packSearchScopeOrdersMu.RLock()
	owners := make([]string, 0, len(packSearchScopeOrders))
	for owner := range packSearchScopeOrders {
		owners = append(owners, owner)
	}
	sort.Strings(owners)

	merged := make([]SearchScopeOrderItem, 0)
	for _, owner := range owners {
		for _, item := range packSearchScopeOrders[owner] {
			merged = append(merged, SearchScopeOrderItem{
				Name:  strings.TrimSpace(item.Name),
				Label: strings.TrimSpace(item.Label),
			})
		}
	}
	packSearchScopeOrdersMu.RUnlock()

	return merged
}

func validateSearchScopeOrderConflictsLocked(owner string, items []SearchScopeOrderItem) error {
	for existingOwner, existingItems := range packSearchScopeOrders {
		if strings.EqualFold(existingOwner, owner) {
			continue
		}
		existingByName := make(map[string]string, len(existingItems))
		for _, item := range existingItems {
			name := strings.TrimSpace(item.Name)
			if name == "" {
				continue
			}
			existingByName[strings.ToLower(name)] = name
		}
		for _, item := range items {
			name := strings.TrimSpace(item.Name)
			if name == "" {
				continue
			}
			if existingName, ok := existingByName[strings.ToLower(name)]; ok {
				return errInvalidSearchScopeOrder(
					fmt.Sprintf("object %s is already declared by pack %s", existingName, strings.TrimSpace(existingOwner)),
				)
			}
		}
	}
	return nil
}
