package permissions

import (
	"context"
	"strings"
	"sync"

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

// recordAccessEvaluationCache is deliberately narrower than the request-wide
// access-input cache. It exists only while one aggregate Read/Edit/Delete
// decision (or one explicit batch of those decisions) is being evaluated.
//
// Parent-controlled access asks the same immutable parent question from the
// object gate and from the Read/Edit/Delete record gates. Keeping that one
// parent snapshot local to the aggregate decision avoids serial database
// round-trips without allowing a later mutation in the same request to reuse a
// stale parent record or approval-lock result.
type recordAccessEvaluationCache struct {
	mu sync.Mutex

	parentRecords        map[recordAccessParentKey]models.SObject
	parentRecordsLoaded  map[recordAccessParentKey]bool
	parentRecordsLoading map[recordAccessParentKey]chan struct{}

	approvalLocks        map[recordAccessApprovalKey]bool
	approvalLocksLoaded  map[recordAccessApprovalKey]bool
	approvalLocksLoading map[recordAccessApprovalKey]chan struct{}

	manualShareLevels        map[recordAccessGrantKey][]string
	manualShareLevelsLoaded  map[recordAccessGrantKey]bool
	manualShareLevelsLoading map[recordAccessGrantKey]chan struct{}

	teamMemberLevels        map[recordAccessGrantKey]*string
	teamMemberLevelsLoaded  map[recordAccessGrantKey]bool
	teamMemberLevelsLoading map[recordAccessGrantKey]chan struct{}
}

type recordAccessEvaluationCacheKey struct{}

type recordAccessParentKey struct {
	objectAPIName string
	recordID      string
}

type recordAccessApprovalKey struct {
	recordID string
	userID   string
}

type recordAccessGrantKey struct {
	objectAPIName string
	recordID      string
	userID        string
}

func withRecordAccessEvaluationCache(ctx context.Context) context.Context {
	if ctx == nil {
		ctx = context.Background()
	}
	if _, ok := ctx.Value(recordAccessEvaluationCacheKey{}).(*recordAccessEvaluationCache); ok {
		return ctx
	}
	return context.WithValue(ctx, recordAccessEvaluationCacheKey{}, &recordAccessEvaluationCache{
		parentRecords:            map[recordAccessParentKey]models.SObject{},
		parentRecordsLoaded:      map[recordAccessParentKey]bool{},
		parentRecordsLoading:     map[recordAccessParentKey]chan struct{}{},
		approvalLocks:            map[recordAccessApprovalKey]bool{},
		approvalLocksLoaded:      map[recordAccessApprovalKey]bool{},
		approvalLocksLoading:     map[recordAccessApprovalKey]chan struct{}{},
		manualShareLevels:        map[recordAccessGrantKey][]string{},
		manualShareLevelsLoaded:  map[recordAccessGrantKey]bool{},
		manualShareLevelsLoading: map[recordAccessGrantKey]chan struct{}{},
		teamMemberLevels:         map[recordAccessGrantKey]*string{},
		teamMemberLevelsLoaded:   map[recordAccessGrantKey]bool{},
		teamMemberLevelsLoading:  map[recordAccessGrantKey]chan struct{}{},
	})
}

func recordAccessEvaluationCacheFromContext(ctx context.Context) *recordAccessEvaluationCache {
	if ctx == nil {
		return nil
	}
	cache, _ := ctx.Value(recordAccessEvaluationCacheKey{}).(*recordAccessEvaluationCache)
	return cache
}

func (ps *PermissionService) parentRecordForAccessEvaluation(
	ctx context.Context,
	objectAPIName string,
	recordID string,
) (models.SObject, error) {
	objectAPIName = strings.TrimSpace(objectAPIName)
	recordID = strings.TrimSpace(recordID)
	if ps == nil || ps.repo == nil || objectAPIName == "" || recordID == "" {
		return nil, nil
	}
	cache := recordAccessEvaluationCacheFromContext(ctx)
	if cache == nil {
		return ps.repo.GetRecordByID(ctx, objectAPIName, recordID)
	}

	key := recordAccessParentKey{objectAPIName: strings.ToLower(objectAPIName), recordID: recordID}
	for {
		cache.mu.Lock()
		if cache.parentRecordsLoaded[key] {
			record := cloneRecordAccessSObject(cache.parentRecords[key])
			cache.mu.Unlock()
			return record, nil
		}
		if ready := cache.parentRecordsLoading[key]; ready != nil {
			cache.mu.Unlock()
			select {
			case <-ready:
				continue
			case <-ctx.Done():
				return nil, ctx.Err()
			}
		}
		ready := make(chan struct{})
		cache.parentRecordsLoading[key] = ready
		cache.mu.Unlock()

		record, err := ps.repo.GetRecordByID(ctx, objectAPIName, recordID)

		cache.mu.Lock()
		if err == nil {
			cache.parentRecords[key] = cloneRecordAccessSObject(record)
			cache.parentRecordsLoaded[key] = true
		}
		delete(cache.parentRecordsLoading, key)
		close(ready)
		cache.mu.Unlock()
		return cloneRecordAccessSObject(record), err
	}
}

func (cache *recordAccessEvaluationCache) approvalLock(
	ctx context.Context,
	recordID string,
	userID string,
	load func() bool,
) bool {
	if cache == nil || strings.TrimSpace(recordID) == "" || strings.TrimSpace(userID) == "" {
		return load()
	}
	key := recordAccessApprovalKey{recordID: strings.TrimSpace(recordID), userID: strings.TrimSpace(userID)}
	for {
		cache.mu.Lock()
		if cache.approvalLocksLoaded[key] {
			locked := cache.approvalLocks[key]
			cache.mu.Unlock()
			return locked
		}
		if ready := cache.approvalLocksLoading[key]; ready != nil {
			cache.mu.Unlock()
			select {
			case <-ready:
				continue
			case <-ctx.Done():
				// Cancellation must fail closed for a mutation authority probe.
				return true
			}
		}
		ready := make(chan struct{})
		cache.approvalLocksLoading[key] = ready
		cache.mu.Unlock()

		locked := load()
		cache.mu.Lock()
		cache.approvalLocks[key] = locked
		cache.approvalLocksLoaded[key] = true
		delete(cache.approvalLocksLoading, key)
		close(ready)
		cache.mu.Unlock()
		return locked
	}
}

func (cache *recordAccessEvaluationCache) manualShareAccessLevels(
	ctx context.Context,
	objectAPIName string,
	recordID string,
	userID string,
	load func() ([]string, error),
) ([]string, error) {
	if cache == nil {
		return load()
	}
	key := newRecordAccessGrantKey(objectAPIName, recordID, userID)
	for {
		cache.mu.Lock()
		if cache.manualShareLevelsLoaded[key] {
			levels := append([]string(nil), cache.manualShareLevels[key]...)
			cache.mu.Unlock()
			return levels, nil
		}
		if ready := cache.manualShareLevelsLoading[key]; ready != nil {
			cache.mu.Unlock()
			select {
			case <-ready:
				continue
			case <-ctx.Done():
				return nil, ctx.Err()
			}
		}
		ready := make(chan struct{})
		cache.manualShareLevelsLoading[key] = ready
		cache.mu.Unlock()

		levels, err := load()
		cache.mu.Lock()
		if err == nil {
			cache.manualShareLevels[key] = append([]string(nil), levels...)
			cache.manualShareLevelsLoaded[key] = true
		}
		delete(cache.manualShareLevelsLoading, key)
		close(ready)
		cache.mu.Unlock()
		return append([]string(nil), levels...), err
	}
}

func (cache *recordAccessEvaluationCache) teamMemberAccessLevel(
	ctx context.Context,
	objectAPIName string,
	recordID string,
	userID string,
	load func() (*string, error),
) (*string, error) {
	if cache == nil {
		return load()
	}
	key := newRecordAccessGrantKey(objectAPIName, recordID, userID)
	for {
		cache.mu.Lock()
		if cache.teamMemberLevelsLoaded[key] {
			level := cloneRecordAccessString(cache.teamMemberLevels[key])
			cache.mu.Unlock()
			return level, nil
		}
		if ready := cache.teamMemberLevelsLoading[key]; ready != nil {
			cache.mu.Unlock()
			select {
			case <-ready:
				continue
			case <-ctx.Done():
				return nil, ctx.Err()
			}
		}
		ready := make(chan struct{})
		cache.teamMemberLevelsLoading[key] = ready
		cache.mu.Unlock()

		level, err := load()
		cache.mu.Lock()
		if err == nil {
			cache.teamMemberLevels[key] = cloneRecordAccessString(level)
			cache.teamMemberLevelsLoaded[key] = true
		}
		delete(cache.teamMemberLevelsLoading, key)
		close(ready)
		cache.mu.Unlock()
		return cloneRecordAccessString(level), err
	}
}

func newRecordAccessGrantKey(objectAPIName string, recordID string, userID string) recordAccessGrantKey {
	return recordAccessGrantKey{
		objectAPIName: strings.ToLower(strings.TrimSpace(objectAPIName)),
		recordID:      strings.TrimSpace(recordID),
		userID:        strings.TrimSpace(userID),
	}
}

func cloneRecordAccessString(value *string) *string {
	if value == nil {
		return nil
	}
	clone := *value
	return &clone
}

func cloneRecordAccessSObject(record models.SObject) models.SObject {
	if record == nil {
		return nil
	}
	clone := make(models.SObject, len(record))
	for key, value := range record {
		clone[key] = value
	}
	return clone
}
