package records

import (
	"context"
	"fmt"
	"sort"
	"strings"

	"github.com/nexuscrm/kernel/pkg/createsourceguard"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/recordsguard"
)

// createFromSourceGuardLoader is the test seam that returns the configured
// create-from-source guard entries for a target object. Production wiring
// delegates to bootstrap.GetCreateFromSourceGuardEntries against the injected
// ps.rawDB; unit tests inject a deterministic fake via SetGuardEntryLoader
// to avoid spinning up a SQL fixture.
type createFromSourceGuardLoader func(ctx context.Context, objectName string) ([]recordsguard.CreateFromSourceGuardEntry, error)

type createFromSourceGuardCandidateFieldLoader func(ctx context.Context, objectName string) ([]string, error)

type createFromSourceGuardMetadataProvider interface {
	CreateFromSourceGuardEntries(context.Context, string) ([]recordsguard.CreateFromSourceGuardEntry, error)
}

type createFromSourceGuardCacheEntry struct {
	entries []recordsguard.CreateFromSourceGuardEntry
}

type createFromSourceGuardCandidateFieldCacheEntry struct {
	fields map[string]struct{}
}

// SetGuardEntryLoader overrides the create-from-source guard's entry loader
// for testing. Production code never calls this. Passing nil restores the
// default rawDB-backed loader. The setter mirrors SetRawDB's "narrow test
// seam" pattern documented at persistence_service.go:99-127.
func (ps *PersistenceService) SetGuardEntryLoader(loader createFromSourceGuardLoader) {
	ps.createFromSourceGuardLoader = loader
	ps.clearCreateFromSourceGuardCache()
}

// SetGuardCandidateFieldLoader overrides the narrow candidate-FK precheck for
// tests that need to prove the full semantic loader stays lazy.
func (ps *PersistenceService) SetGuardCandidateFieldLoader(loader createFromSourceGuardCandidateFieldLoader) {
	ps.createFromSourceGuardCandidateFieldLoader = loader
	ps.clearCreateFromSourceGuardCache()
}

// defaultCreateFromSourceGuardLoader returns the production loader: delegates
// to bootstrap.GetCreateFromSourceGuardEntries against ps.rawDB. Nil rawDB
// returns (nil, nil) — same nil-safety contract as the insert-inheritance
// applier (insert_inheritance.go:71-73), so test harnesses that don't wire
// rawDB get a no-op guard.
func (ps *PersistenceService) defaultCreateFromSourceGuardLoader() createFromSourceGuardLoader {
	return func(_ context.Context, objectName string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		if ps.rawDB == nil {
			return nil, nil
		}
		return createsourceguard.GetCreateFromSourceGuardEntries(ps.rawDB, objectName)
	}
}

func (ps *PersistenceService) defaultCreateFromSourceGuardCandidateFieldLoader() createFromSourceGuardCandidateFieldLoader {
	return func(_ context.Context, objectName string) ([]string, error) {
		if ps.rawDB == nil {
			return nil, nil
		}
		return createsourceguard.GetCreateFromSourceGuardCandidateFields(ps.rawDB, objectName)
	}
}

// loadCreateFromSourceGuardEntries returns the configured guard entries for
// objectName, using the injected loader when set or falling back to the
// rawDB-backed production loader. Results are cached by target object and
// invalidated by metadata writes that can change the derived guard registry.
func (ps *PersistenceService) loadCreateFromSourceGuardEntries(ctx context.Context, objectName string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
	if ps != nil && ps.createFromSourceGuardMeta != nil {
		// The view is immutable and request-bound. A process-global cache could
		// only mix revisions, so resolve it for every request.
		return ps.createFromSourceGuardMeta.CreateFromSourceGuardEntries(ctx, objectName)
	}
	cacheKey := createFromSourceGuardCacheKey(objectName)
	if cacheKey == "" {
		return nil, nil
	}

	for {
		ps.createFromSourceGuardCacheMu.RLock()
		if ps.createFromSourceGuardCache != nil {
			if entry, ok := ps.createFromSourceGuardCache[cacheKey]; ok {
				ps.createFromSourceGuardCacheMu.RUnlock()
				return entry.entries, nil
			}
		}
		loadEpoch := ps.createFromSourceGuardEpoch
		ps.createFromSourceGuardCacheMu.RUnlock()

		loadKey := createFromSourceGuardLoadGroupKey("entries", loadEpoch, cacheKey)
		loaded, err, _ := ps.createFromSourceGuardLoadGroup.Do(loadKey, func() (interface{}, error) {
			loader := ps.createFromSourceGuardLoader
			if loader == nil {
				loader = ps.defaultCreateFromSourceGuardLoader()
			}
			return loader(ctx, objectName)
		})
		if err != nil {
			return nil, err
		}
		entries, _ := loaded.([]recordsguard.CreateFromSourceGuardEntry)

		ps.createFromSourceGuardCacheMu.Lock()
		if ps.createFromSourceGuardEpoch != loadEpoch {
			// Metadata changed while the loader was reading. Discard the
			// pre-invalidation result so it cannot repopulate the cache.
			ps.createFromSourceGuardCacheMu.Unlock()
			continue
		}
		if ps.createFromSourceGuardCache == nil {
			ps.createFromSourceGuardCache = make(map[string]createFromSourceGuardCacheEntry)
		}
		if entry, ok := ps.createFromSourceGuardCache[cacheKey]; ok {
			ps.createFromSourceGuardCacheMu.Unlock()
			return entry.entries, nil
		}
		ps.createFromSourceGuardCache[cacheKey] = createFromSourceGuardCacheEntry{entries: entries}
		ps.createFromSourceGuardCacheMu.Unlock()
		return entries, nil
	}
}

func (ps *PersistenceService) loadCreateFromSourceGuardCandidateFields(ctx context.Context, objectName string) (map[string]struct{}, error) {
	if ps != nil && ps.createFromSourceGuardMeta != nil {
		return ps.loadCreateFromSourceGuardCandidateFieldSet(ctx, objectName)
	}
	cacheKey := createFromSourceGuardCacheKey(objectName)
	if cacheKey == "" {
		return nil, nil
	}

	for {
		ps.createFromSourceGuardCacheMu.RLock()
		if ps.createFromSourceGuardCandidateFieldCache != nil {
			if entry, ok := ps.createFromSourceGuardCandidateFieldCache[cacheKey]; ok {
				ps.createFromSourceGuardCacheMu.RUnlock()
				return entry.fields, nil
			}
		}
		loadEpoch := ps.createFromSourceGuardEpoch
		ps.createFromSourceGuardCacheMu.RUnlock()

		loadKey := createFromSourceGuardLoadGroupKey("candidate", loadEpoch, cacheKey)
		loaded, err, _ := ps.createFromSourceGuardLoadGroup.Do(loadKey, func() (interface{}, error) {
			return ps.loadCreateFromSourceGuardCandidateFieldSet(ctx, objectName)
		})
		if err != nil {
			return nil, err
		}
		fields, _ := loaded.(map[string]struct{})

		ps.createFromSourceGuardCacheMu.Lock()
		if ps.createFromSourceGuardEpoch != loadEpoch {
			ps.createFromSourceGuardCacheMu.Unlock()
			continue
		}
		if ps.createFromSourceGuardCandidateFieldCache == nil {
			ps.createFromSourceGuardCandidateFieldCache = make(map[string]createFromSourceGuardCandidateFieldCacheEntry)
		}
		if entry, ok := ps.createFromSourceGuardCandidateFieldCache[cacheKey]; ok {
			ps.createFromSourceGuardCacheMu.Unlock()
			return entry.fields, nil
		}
		ps.createFromSourceGuardCandidateFieldCache[cacheKey] = createFromSourceGuardCandidateFieldCacheEntry{fields: fields}
		ps.createFromSourceGuardCacheMu.Unlock()
		return fields, nil
	}
}

func (ps *PersistenceService) loadCreateFromSourceGuardCandidateFieldSet(ctx context.Context, objectName string) (map[string]struct{}, error) {
	if ps != nil && ps.createFromSourceGuardMeta != nil {
		entries, err := ps.createFromSourceGuardMeta.CreateFromSourceGuardEntries(ctx, objectName)
		if err != nil {
			return nil, err
		}
		fields := make([]string, 0, len(entries))
		for _, entry := range entries {
			fields = append(fields, entry.SourceFKField)
		}
		return createFromSourceGuardCandidateFieldSet(fields), nil
	}
	if ps.createFromSourceGuardCandidateFieldLoader != nil {
		fields, err := ps.createFromSourceGuardCandidateFieldLoader(ctx, objectName)
		return createFromSourceGuardCandidateFieldSet(fields), err
	}
	if ps.createFromSourceGuardLoader != nil {
		entries, err := ps.loadCreateFromSourceGuardEntries(ctx, objectName)
		if err != nil {
			return nil, err
		}
		fields := make([]string, 0, len(entries))
		for _, entry := range entries {
			fields = append(fields, entry.SourceFKField)
		}
		return createFromSourceGuardCandidateFieldSet(fields), nil
	}
	loader := ps.defaultCreateFromSourceGuardCandidateFieldLoader()
	fields, err := loader(ctx, objectName)
	return createFromSourceGuardCandidateFieldSet(fields), err
}

func createFromSourceGuardCandidateFieldSet(fields []string) map[string]struct{} {
	if len(fields) == 0 {
		return nil
	}
	out := make(map[string]struct{}, len(fields))
	for _, field := range fields {
		field = strings.TrimSpace(field)
		if field == "" {
			continue
		}
		out[field] = struct{}{}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func dataSetsCreateFromSourceCandidateFK(data models.SObject, fields map[string]struct{}) bool {
	if len(data) == 0 || len(fields) == 0 {
		return false
	}
	for field := range fields {
		if guardSourceFKString(data[field]) != "" {
			return true
		}
	}
	return false
}

func createFromSourceGuardCacheKey(objectName string) string {
	return strings.ToLower(strings.TrimSpace(objectName))
}

func createFromSourceGuardLoadGroupKey(namespace string, epoch uint64, cacheKey string) string {
	return fmt.Sprintf("%s:%d:%s", namespace, epoch, cacheKey)
}

func (ps *PersistenceService) clearCreateFromSourceGuardCache() {
	ps.createFromSourceGuardCacheMu.Lock()
	ps.createFromSourceGuardCache = nil
	ps.createFromSourceGuardCandidateFieldCache = nil
	ps.createFromSourceGuardEpoch++
	ps.createFromSourceGuardCacheMu.Unlock()
}

// InvalidateCreateFromSourceGuardCache clears the derived create-from-source
// guard registry. MetadataService's cache-invalidation hook calls this after
// metadata refreshes and flow/tooling invalidations, while generic CRUD
// metadata writes clear it through invalidateSecurityCachesForChanges.
func (ps *PersistenceService) InvalidateCreateFromSourceGuardCache() {
	ps.clearCreateFromSourceGuardCache()
}

func isCreateFromSourceGuardMetadataObject(objectName string) bool {
	return strings.EqualFold(objectName, constants.TableObjectFieldMapping) ||
		strings.EqualFold(objectName, constants.TableStandardAction) ||
		strings.EqualFold(objectName, constants.TableFlowDefinition) ||
		strings.EqualFold(objectName, constants.TableFlow) ||
		strings.EqualFold(objectName, constants.TableObject) ||
		strings.EqualFold(objectName, constants.TableField)
}

// guardCreateFromSourceContextWithEntries rejects a direct sObject create
// that would silently bypass the fan-out owned by a configured create-from-source
// standard action, using preloaded entries (or a pre-captured load error).
// This variant is the BulkInsert hot-path entry point; the single-record Path A
// calls the thin wrapper guardCreateFromSourceContext below.
//
// When loadErr != nil, the error is surfaced using the same wrapped-error
// envelope that the original loader path produced at create_from_source_guard.go:106-108.
// When entries is empty, the guard is a no-op (plain create with no configured action).
//
// The body of this function (grouping, ctx matching, multi-owner, message) is
// shared with the original guard — both paths evaluate identically.
func guardCreateFromSourceContextWithEntries(
	ctx context.Context,
	objectName string,
	data models.SObject,
	entries []recordsguard.CreateFromSourceGuardEntry,
	loadErr error,
) error {
	if loadErr != nil {
		return fmt.Errorf("load create-from-source guard entries for %s: %w", objectName, loadErr)
	}
	if len(entries) == 0 {
		return nil
	}

	// Group entries by SourceFKField. Each unique FK position is evaluated
	// independently; a body that sets ParentRefA and not ParentRefB only
	// triggers the ParentRefA group's check.
	groups := make(map[string][]recordsguard.CreateFromSourceGuardEntry, len(entries))
	for _, entry := range entries {
		if entry.SourceFKField == "" || entry.OwningStandardActionId == "" {
			continue
		}
		groups[entry.SourceFKField] = append(groups[entry.SourceFKField], entry)
	}

	activeActionID := constants.StandardActionExecutionActionID(ctx)
	activeActionRef := constants.StandardActionExecutionRef(ctx)
	var violatedActionNames []string
	for sourceFKField, groupEntries := range groups {
		fkValue := guardSourceFKString(data[sourceFKField])
		if fkValue == "" {
			continue
		}
		if activeActionID != "" || activeActionRef != "" {
			matched := false
			for _, entry := range groupEntries {
				if entry.OwningStandardActionId == activeActionID ||
					(activeActionRef != "" && strings.EqualFold(entry.ActionName, activeActionRef)) {
					matched = true
					break
				}
			}
			if matched {
				continue
			}
		}
		for _, entry := range groupEntries {
			violatedActionNames = append(violatedActionNames, entry.ActionName)
		}
	}
	if len(violatedActionNames) == 0 {
		return nil
	}

	// Deterministic ordering + de-dup so the error message and any test
	// assertion on the action list are stable across runs (Go map
	// iteration order is randomized).
	violatedActionNames = dedupAndSortStrings(violatedActionNames)
	return appErrors.NewCreateFromSourceGuardError(objectName, violatedActionNames)
}

// guardCreateFromSourceContext rejects a direct sObject create that would
// silently bypass the fan-out owned by a configured create-from-source standard
// action.
//
// Issue #1234: a target object `T` is the target of one or more standard
// actions whose Flow performs a multi-record fan-out (parent + children) when
// invoked through the action endpoint. A client that calls the generic
// `POST /sobjects/T` (or its Bulk-API and composite variants) with `T`'s
// source-FK populated reaches `PersistenceService.Insert` / `BulkInsert` and
// produces a parent-only success — the configured children are silently never
// created. This guard fires iff:
//
//  1. The bootstrap loader returns ≥ 1 entry for `T` (a configured action owns
//     at least one `(T, SourceFKField)` position).
//  2. `data[SourceFKField]` is set to a non-blank string for at least one
//     configured entry.
//  3. For every entry on that FK, the active standard-action context's id
//     differs from `entry.OwningStandardActionId` — i.e. no owning action is
//     authentically driving this insert. The multi-owner branch (F11) accepts
//     bypass when ctx matches ANY entry on the same FK.
//
// Returns nil to allow the insert; returns a neutral
// `*appErrors.DomainWireError{Kind: WireInvalidOperation}` to reject — the API
// boundary translates that kind to the v63 INVALID_OPERATION code at HTTP 400
// and renders dialect-specific action URLs. The neutral details name every
// owning action so the client knows which action(s) to call instead.
//
// Placement (shared between Path A and Path B):
//   - persistence_insert.go: AFTER pruneNonCreateableFields (FLS denial) and
//     AFTER validateNonPolymorphicLookups (FK access check), BEFORE
//     applyInsertInheritance.
//   - persistence_bulk_insert.go: AFTER the FLS denial loop and AFTER a
//     caller-payload validateNonPolymorphicLookups pass for guarded source FKs
//     (matching Path A's lookup-error precedence), BEFORE RecordBeforeCreate;
//     any guarded source FK newly materialized by RecordBeforeCreate is checked
//     again immediately after that hook, before final-image lookup validation.
//
// This ordering preserves FLS-error precedence (F12) — an FLS-denied source-FK
// surfaces the existing FLS error envelope, not INVALID_OPERATION — and FK-
// access precedence — an inaccessible parent surfaces validateNonPolymorphicLookups's
// 400 referencing the field, not INVALID_OPERATION.
//
// Idempotent / read-only: no DB writes, no ctx / data mutation. Returns the
// data unchanged when allowed.
//
// Implementation: thin wrapper that loads cached entries and delegates to
// guardCreateFromSourceContextWithEntries. Path A (single Insert) calls this;
// Path B (BulkInsert) calls the variant directly with preloaded entries. The
// shared loader cache is keyed by target object and is invalidated when CRUD
// mutates _System_ObjectFieldMapping, StandardAction, FlowDefinition, Flow,
// _System_Object, or _System_Field metadata.
func (ps *PersistenceService) guardCreateFromSourceContext(
	ctx context.Context,
	objectName string,
	data models.SObject,
) error {
	if len(data) == 0 {
		return nil
	}
	candidateFields, err := ps.loadCreateFromSourceGuardCandidateFields(ctx, objectName)
	if err != nil {
		return fmt.Errorf("load create-from-source candidate fields for %s: %w", objectName, err)
	}
	if !dataSetsCreateFromSourceCandidateFK(data, candidateFields) {
		return nil
	}
	entries, err := ps.loadCreateFromSourceGuardEntries(ctx, objectName)
	return guardCreateFromSourceContextWithEntries(ctx, objectName, data, entries, err)
}

// guardSourceFKString extracts the trimmed string form of a candidate FK
// value. Numeric / nil / non-string types are treated as blank so the guard
// only fires when the body sets the FK to an actual id-shaped string. Matches
// the inheritLookupString semantics in insert_inheritance.go for consistency.
func guardSourceFKString(v interface{}) string {
	s, ok := v.(string)
	if !ok {
		return ""
	}
	return strings.TrimSpace(s)
}

func dedupAndSortStrings(in []string) []string {
	if len(in) <= 1 {
		return in
	}
	seen := make(map[string]struct{}, len(in))
	out := make([]string, 0, len(in))
	for _, s := range in {
		if _, dup := seen[s]; dup {
			continue
		}
		seen[s] = struct{}{}
		out = append(out, s)
	}
	sort.Strings(out)
	return out
}
