package analytics

import (
	"context"
	"fmt"
	"strings"

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

// StandardObjectResolver auto-discovers every deployed object at service
// startup and maps both canonical API names (e.g. "orderitem") and legacy
// "<PascalCase>List" aliases (e.g. "OrderItemList") to the canonical physical
// table name.
//
// It is built once at service wiring via BuildStandardObjectResolver and is
// consulted by resolveSourceObject BEFORE the existing string-trim heuristic.
//
// Why auto-scan instead of a hand-maintained whitelist: new deployed objects
// (custom or standard) immediately become routable without a code change and
// without missing legacy SF report-type aliases such as OrderItemList or
// ContractList that the prior hardcoded list kept forgetting.
type StandardObjectResolver struct {
	// canonical maps lowercased physical table names to themselves. Keyed
	// case-sensitively (always lowercase) for raw API-name lookups.
	canonical map[string]string
	// aliases maps uppercased legacy "<PascalCase>List" keys to the
	// lowercased physical table name. Keyed by uppercase for
	// case-insensitive alias lookups.
	aliases map[string]string
}

// Lookup returns the physical table name for a candidate SourceObject, or ""
// if not a known standard object. Canonical lookups are case-insensitive on
// the incoming candidate; alias lookups are case-insensitive.
func (r *StandardObjectResolver) Lookup(candidate string) string {
	if r == nil || candidate == "" {
		return ""
	}
	lc := strings.ToLower(candidate)
	if v, ok := r.canonical[lc]; ok {
		return v
	}
	if v, ok := r.aliases[strings.ToUpper(candidate)]; ok {
		return v
	}
	return ""
}

// schemaSourceFn is the unexported function shape used by the functional
// option below. Defined here (not inline) so test helpers can reference it.
type schemaSourceFn func(ctx context.Context, md MetadataSource) []*models.ObjectMetadata

// defaultSchemaSource delegates to MetadataService.GetSchemas — the
// production path.
func defaultSchemaSource(ctx context.Context, md MetadataSource) []*models.ObjectMetadata {
	if md == nil {
		return nil
	}
	return md.GetSchemas(ctx)
}

// StandardObjectResolverOption configures BuildStandardObjectResolver.
// HIGH-7: tests previously swapped a package-level var to inject a fake
// schema list; this functional option removes that global mutation so two
// tests running in parallel can never race on the same shared variable.
type StandardObjectResolverOption func(*standardObjectResolverConfig)

type standardObjectResolverConfig struct {
	source schemaSourceFn
}

// BuildStandardObjectResolver walks the metadata service's object list and
// registers each deployed object twice: canonical API name (lowercased) plus
// a "<PascalCase>List" legacy alias. Safe to call during service startup.
//
// Cold-start safety: if md is nil or returns an empty list, this returns a
// non-nil resolver whose Lookup always returns "". The caller's existing
// heuristic then takes over — no regression, no panic.
func BuildStandardObjectResolver(ctx context.Context, md MetadataSource, logger *logging.Logger, opts ...StandardObjectResolverOption) *StandardObjectResolver {
	cfg := standardObjectResolverConfig{source: defaultSchemaSource}
	for _, opt := range opts {
		opt(&cfg)
	}

	r := &StandardObjectResolver{
		canonical: make(map[string]string),
		aliases:   make(map[string]string),
	}

	schemas := cfg.source(ctx, md)
	if len(schemas) == 0 {
		if logger != nil {
			logger.Warn("standard object resolver built with empty metadata, Lookup will always return empty (cold start fallback active)")
		}
		return r
	}

	for _, s := range schemas {
		if s == nil || s.APIName == "" {
			continue
		}
		registerStandardObject(r, s.APIName)
	}

	if logger != nil {
		logger.Info(fmt.Sprintf("standard object resolver built, %d canonical + %d aliases registered", len(r.canonical), len(r.aliases)))
	}
	return r
}

// registerStandardObject adds canonical and alias entries for a single API
// name. Idempotent: repeated calls with the same name are safe.
func registerStandardObject(r *StandardObjectResolver, apiName string) {
	canonical := strings.ToLower(apiName)
	if canonical == "" {
		return
	}
	r.canonical[canonical] = canonical

	// Defensive: skip alias generation for API names that already end in
	// "List" — avoid producing "FooListList".
	if strings.HasSuffix(apiName, "List") {
		return
	}
	alias := strings.ToUpper(apiName + "List")
	r.aliases[alias] = canonical
}
