package records

import (
	"context"
	"strings"
	"sync"

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

type recordReadCacheCtxKey struct{}

type recordReadMode uint8

const (
	recordReadModeLogical recordReadMode = iota
	recordReadModePhysical
)

type recordIdentityKey struct {
	table string
	id    string
}

type recordCacheKey struct {
	table string
	id    string
	mode  recordReadMode
}

type recordReadCache struct {
	mu sync.Mutex

	store map[recordCacheKey]*cacheEntry
	// gen tracks, per key, how many times the key has been invalidated. A read
	// captures the generation it started under via beginRead; the matching
	// setIfFresh only lands when the generation is unchanged. This rejects a
	// stale in-flight read whose set races a concurrent invalidate, while still
	// allowing a fresh post-invalidate read to re-populate the entry — the
	// behavior a permanent dirty flag wrongly prevented (every parent re-read
	// after the first rollup write fell through to the wide view).
	gen map[recordIdentityKey]uint64

	// ownerUserProofs is a narrow request-local proof that OwnerId validation can
	// use when Flow already looked up a live User. It is deliberately separate
	// from store: Flow RecordLookup rows are query projections, not authoritative
	// raw FindOne rows for generic records-layer consumers.
	ownerUserProofs map[string]struct{}
}

type cacheEntry struct {
	record models.SObject
	found  bool
}

func WithRecordReadCache(ctx context.Context) context.Context {
	return context.WithValue(ctx, recordReadCacheCtxKey{}, &recordReadCache{
		store:           make(map[recordCacheKey]*cacheEntry),
		gen:             make(map[recordIdentityKey]uint64),
		ownerUserProofs: make(map[string]struct{}),
	})
}

func EnsureRecordReadCache(ctx context.Context) context.Context {
	if recordReadCacheFromCtx(ctx) != nil {
		return ctx
	}
	return WithRecordReadCache(ctx)
}

func recordReadCacheFromCtx(ctx context.Context) *recordReadCache {
	v, _ := ctx.Value(recordReadCacheCtxKey{}).(*recordReadCache)
	return v
}

func normalizeRecordIdentityKey(table, id string) recordIdentityKey {
	return recordIdentityKey{
		table: strings.ToLower(strings.TrimSpace(table)),
		id:    strings.TrimSpace(id),
	}
}

func normalizeRecordCacheKey(table, id string, mode recordReadMode) recordCacheKey {
	return recordCacheKey{
		table: strings.ToLower(strings.TrimSpace(table)),
		id:    strings.TrimSpace(id),
		mode:  mode,
	}
}

func (c *recordReadCache) get(table, id string) (models.SObject, bool, bool) {
	return c.getWithMode(table, id, recordReadModeLogical)
}

func (c *recordReadCache) getWithMode(table, id string, mode recordReadMode) (models.SObject, bool, bool) {
	if c == nil || id == "" {
		return nil, false, false
	}
	key := normalizeRecordCacheKey(table, id, mode)
	c.mu.Lock()
	defer c.mu.Unlock()
	entry, ok := c.store[key]
	if !ok {
		return nil, false, false
	}
	if !entry.found {
		return nil, false, true
	}
	return cloneRecordCacheEntry(entry.record), true, true
}

// beginRead records the generation a read is starting under. The caller passes
// the returned token to setIfFresh after the underlying FindOne completes so a
// set that was outpaced by an intervening invalidate is dropped instead of
// re-poisoning the cache with a pre-write snapshot.
func (c *recordReadCache) beginRead(table, id string) uint64 {
	if c == nil || id == "" {
		return 0
	}
	key := normalizeRecordIdentityKey(table, id)
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.gen[key]
}

// setIfFresh stores the read result only when the key's generation has not
// advanced since the matching beginRead. An advanced generation means an
// invalidate landed mid-read, so the result is stale and must be dropped.
func (c *recordReadCache) setIfFresh(table, id string, record models.SObject, found bool, readGen uint64) {
	c.setIfFreshWithMode(table, id, recordReadModeLogical, record, found, readGen)
}

func (c *recordReadCache) setIfFreshWithMode(table, id string, mode recordReadMode, record models.SObject, found bool, readGen uint64) {
	if c == nil || id == "" {
		return
	}
	identityKey := normalizeRecordIdentityKey(table, id)
	cacheKey := normalizeRecordCacheKey(table, id, mode)
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.gen[identityKey] != readGen {
		return
	}
	var stored models.SObject
	if found && record != nil {
		stored = cloneRecordCacheEntry(record)
	}
	c.store[cacheKey] = &cacheEntry{record: stored, found: found}
}

func (c *recordReadCache) invalidate(table, id string) {
	if c == nil || id == "" {
		return
	}
	identityKey := normalizeRecordIdentityKey(table, id)
	c.mu.Lock()
	defer c.mu.Unlock()
	c.gen[identityKey]++
	delete(c.store, normalizeRecordCacheKey(table, id, recordReadModeLogical))
	delete(c.store, normalizeRecordCacheKey(table, id, recordReadModePhysical))
	if strings.EqualFold(table, constants.TableUser) {
		delete(c.ownerUserProofs, strings.TrimSpace(id))
	}
}

func InvalidateRecordReadCache(ctx context.Context, table, id string) {
	if c := recordReadCacheFromCtx(ctx); c != nil {
		c.invalidate(table, id)
	}
}

func SeedOwnerUserLookupProof(ctx context.Context, id string) {
	c := recordReadCacheFromCtx(ctx)
	if c == nil {
		return
	}
	id = strings.TrimSpace(id)
	if id == "" {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.ownerUserProofs == nil {
		c.ownerUserProofs = make(map[string]struct{})
	}
	c.ownerUserProofs[id] = struct{}{}
}

func (c *recordReadCache) hasOwnerUserProof(id string) bool {
	if c == nil {
		return false
	}
	id = strings.TrimSpace(id)
	if id == "" {
		return false
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	_, ok := c.ownerUserProofs[id]
	return ok
}

func cloneRecordCacheEntry(record models.SObject) models.SObject {
	if record == nil {
		return nil
	}
	clone := make(models.SObject, len(record))
	for k, v := range record {
		clone[k] = v
	}
	return clone
}
