package permissions

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/fieldpermissions"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/recordtypes"
)

// metadataProvider is the subset of metadata.MetadataService used by PermissionService.
// Using an interface allows unit tests to inject lightweight mocks without a full DB.
type metadataProvider interface {
	GetSchema(ctx context.Context, apiName string) *models.ObjectMetadata
	GetSchemas(ctx context.Context) []*models.ObjectMetadata
	GetRelationships(ctx context.Context, objectAPIName string) []*models.Relationship
	GetSharingParentRelationship(ctx context.Context, childObjectAPIName string) *models.Relationship
	// GetParentControlledObject returns the parent object's APIName when the
	// schema is a ControlledByParent child with a configured sharing-parent
	// relationship; "" otherwise. Single SSoT for "is this object parent-
	// controlled?" — runtime, seeder, and SQL verifier consume it identically.
	GetParentControlledObject(ctx context.Context, schema *models.ObjectMetadata) string
	GetSharingRules(ctx context.Context, objectAPIName string) []*models.SharingRule
	GetRecordTypes(ctx context.Context, objectAPIName string) ([]*models.RecordType, error)
}

// PermissionCacheInvalidator propagates authoritative permission writes to the
// bounded system/custom middleware cache. It intentionally has no read API:
// object and field authorization results must remain request-scoped.
type PermissionCacheInvalidator interface {
	// InvalidateUser removes all cached entries for a specific user.
	InvalidateUser(userID string)
	// Invalidate flushes the entire permission cache.
	Invalidate()
}

// PermissionService handles permission checks for objects and fields.
//
// System/custom permissions may be cached by the middleware PermissionLoader;
// permission writes reach that cache through an invalidation-only port. Object
// and field checks use request-scoped projections and never a process TTL cache.
//
// Permission evaluation order:
//  1. Metadata-declared setup/security gates (RequiredRead/WritePermission)
//  2. Platform super-user capability (ModifyAllData) bypasses data-plane checks
//  3. Object-level permissions from _System_ObjectPerms
//  4. Field-level permissions from _System_FieldPerms
//  5. Record-level access (ownership, sharing rules)
type PermissionService struct {
	metadata metadataProvider
	repo     ports.PermissionRepository
	userRepo ports.UserRepository
	formula  *formula.Engine

	// Role hierarchy cache: maps role_id -> parent_role_id.
	// roleHierarchyLoadedAt tracks the last successful refresh for lazy TTL-based
	// reloads. Both fields are guarded by roleHierarchyMu.
	roleHierarchyCache    map[string]*string
	roleHierarchyLoadedAt time.Time
	roleHierarchyMu       sync.RWMutex

	// Optional invalidation-only connection to the middleware system/custom
	// cache. It is never a permission read path.
	cacheInvalidator PermissionCacheInvalidator

	// Bound both executing FLS projections and callers waiting to execute one.
	fieldProjectionAdmissionOnce sync.Once
	fieldProjectionAdmission     *boundedFieldProjectionAdmission

	// Optional setup auditor for recording permission changes
	setupAuditor setupChangeRecorder

	// Sharing rule cache: maps objectName -> cached rules with TTL
	sharingRuleCache map[string]*cachedSharingRules
	sharingRuleMu    sync.RWMutex

	// FolderAccessService for FolderSharing SharingModel resolution in BuildRLSResult.
	// Set via SetFolderAccess() after construction (circular dependency resolution).
	folderAccess *FolderAccessService
}

// cachedSharingRules holds a cached result of GetSharingRules with an expiry time.
type cachedSharingRules struct {
	rules     []*models.SharingRule
	expiresAt time.Time
}

// setupChangeRecorder is the subset of SetupAuditor needed by PermissionService.
type setupChangeRecorder interface {
	Record(ctx context.Context, action, section, display, userID string)
}

// SetSetupAuditor wires the SetupAuditor for recording permission changes.
func (ps *PermissionService) SetSetupAuditor(a setupChangeRecorder) {
	ps.setupAuditor = a
}

// SetFolderAccess wires the FolderAccessService for FolderSharing SharingModel
// resolution in BuildRLSResult. Called after both services are constructed to
// resolve the circular dependency (FolderAccessService holds *PermissionService).
func (ps *PermissionService) SetFolderAccess(fa *FolderAccessService) {
	ps.folderAccess = fa
}

// FolderAccessService returns the folder-sharing resolver wired into this
// permission service, if one is available.
func (ps *PermissionService) FolderAccessService() *FolderAccessService {
	if ps == nil {
		return nil
	}
	return ps.folderAccess
}

// canBypassApprovalLock returns true for trusted contexts that must be able to
// mutate records while approval actions execute on behalf of the user.
func (ps *PermissionService) canBypassApprovalLock(ctx context.Context, user *models.UserSession) bool {
	if constants.GetSecurityContext(ctx) >= constants.ServiceContext {
		return true
	}
	return ps.HasSuperUserCapability(ctx, user)
}

// canBypassLockRule reports whether the caller bypasses ObjectMetadata.LockRule
// evaluation. Narrower than canBypassApprovalLock: only trusted ServiceContext+
// execution bypasses (system operations). System Administrators and SuperUsers
// do NOT bypass LockRule.
//
// Rationale: LockRule is an admin-authored metadata declaration (e.g., an app
// pack declaring "Lead with IsConverted=true is locked"). The pack has
// explicitly authored this business invariant; silently bypassing it for
// System Administrators would gut the metadata declaration — admins are still
// subject to admin-authored invariants by design. Approval-lock (a
// platform-internal "awaiting approval" state) keeps allowing admin bypass so
// administrators can force-unlock records — that Salesforce ModifyAllData
// convention is unchanged.
func canBypassLockRule(ctx context.Context, _ *models.UserSession) bool {
	return constants.GetSecurityContext(ctx) >= constants.ServiceContext
}

// blockedPermission is a sentinel value that can never be satisfied by any user permission.
// It is returned for objects that should never be writable via generic CRUD.
const blockedPermission = "__BLOCKED__"

func (ps *PermissionService) requiredSystemPermissionsForSetupObject(ctx context.Context, objectAPIName string, operation string) []string {
	if ps.metadata == nil {
		return nil
	}
	schema := ps.metadata.GetSchema(ctx, objectAPIName)
	return requiredSystemPermissionsForSetupSchema(schema, operation)
}

func requiredSystemPermissionsForSetupSchema(schema *models.ObjectMetadata, operation string) []string {
	if schema == nil {
		return nil // unknown object, let CRUD check handle it
	}

	// Read-exempt: setup objects are readable by all authenticated users (SF model)
	if strings.EqualFold(operation, constants.PermRead) {
		return nil
	}

	// Check metadata-driven permission
	if schema.RequiredWritePermission != "" {
		if schema.RequiredWritePermission == "BLOCKED" {
			return []string{blockedPermission}
		}
		return []string{schema.RequiredWritePermission}
	}

	return nil
}

func (ps *PermissionService) hasAnySystemPermission(ctx context.Context, user *models.UserSession, permissions []string) bool {
	if len(permissions) == 0 || user == nil {
		return len(permissions) == 0
	}

	// BLOCKED sentinel is never satisfiable.
	for _, p := range permissions {
		if p == blockedPermission {
			return false
		}
	}

	effectivePerms, ok := ps.effectiveSystemPermissionsForDecision(ctx, user)
	if !ok {
		return false
	}

	for _, permission := range permissions {
		if effectivePerms[permission] {
			return true
		}
	}

	return false
}

// NewPermissionService creates a new PermissionService
type PermissionServiceOption func(*PermissionService)

type FieldProjectionAdmissionLimits struct {
	Active  int
	Waiters int
}

const (
	FieldProjectionActiveLimitEnv = "NEXUSCRM_FLS_PROJECTION_ACTIVE_LIMIT"
	FieldProjectionWaiterLimitEnv = "NEXUSCRM_FLS_PROJECTION_WAITER_LIMIT"
)

func DefaultFieldProjectionAdmissionLimits() FieldProjectionAdmissionLimits {
	return FieldProjectionAdmissionLimits{
		Active:  defaultBoundedFieldPermissionProjectionLimit,
		Waiters: defaultBoundedFieldPermissionProjectionWaiterLimit,
	}
}

func FieldProjectionAdmissionLimitsFromEnv(getenv func(string) string) (FieldProjectionAdmissionLimits, error) {
	limits := DefaultFieldProjectionAdmissionLimits()
	if getenv == nil {
		return limits, nil
	}
	parse := func(name string, fallback, maximum int, allowZero bool) (int, error) {
		raw := strings.TrimSpace(getenv(name))
		if raw == "" {
			return fallback, nil
		}
		value, err := strconv.Atoi(raw)
		if err != nil || value < 0 || value > maximum || (!allowZero && value == 0) {
			return fallback, fmt.Errorf("%s=%q is invalid; expected %s%d, using default %d", name, raw, map[bool]string{true: "0..", false: "1.."}[allowZero], maximum, fallback)
		}
		return value, nil
	}
	var activeErr, waiterErr error
	limits.Active, activeErr = parse(FieldProjectionActiveLimitEnv, limits.Active, maxBoundedFieldPermissionProjectionLimit, false)
	limits.Waiters, waiterErr = parse(FieldProjectionWaiterLimitEnv, limits.Waiters, maxBoundedFieldPermissionProjectionWaiterLimit, true)
	if activeErr != nil && waiterErr != nil {
		return limits, fmt.Errorf("%v; %v", activeErr, waiterErr)
	}
	if activeErr != nil {
		return limits, activeErr
	}
	return limits, waiterErr
}

// WithFieldProjectionAdmissionLimits configures the finite process-local
// admission boundary for cold, object-bounded FLS projections. Non-positive
// active limits and negative waiter limits retain the package defaults; zero
// waiters is valid and sheds immediately once all active slots are occupied.
func WithFieldProjectionAdmissionLimits(activeLimit, waiterLimit int) PermissionServiceOption {
	return func(ps *PermissionService) {
		if activeLimit <= 0 || activeLimit > maxBoundedFieldPermissionProjectionLimit {
			activeLimit = defaultBoundedFieldPermissionProjectionLimit
		}
		if waiterLimit < 0 || waiterLimit > maxBoundedFieldPermissionProjectionWaiterLimit {
			waiterLimit = defaultBoundedFieldPermissionProjectionWaiterLimit
		}
		ps.fieldProjectionAdmission = newBoundedFieldProjectionAdmission(activeLimit, waiterLimit)
	}
}

func NewPermissionService(
	repo ports.PermissionRepository,
	metadata metadataProvider,
	userRepo ports.UserRepository,
	opts ...PermissionServiceOption,
) *PermissionService {
	ps := &PermissionService{
		metadata:           metadata,
		repo:               repo,
		userRepo:           userRepo,
		formula:            formula.NewEngine(),
		roleHierarchyCache: make(map[string]*string),
		sharingRuleCache:   make(map[string]*cachedSharingRules),
	}
	for _, opt := range opts {
		if opt != nil {
			opt(ps)
		}
	}
	// Pre-load role hierarchy
	ps.refreshRoleHierarchy(context.Background())
	return ps
}

// SetCacheInvalidator attaches the write-through invalidation port for the
// middleware system/custom permission cache.
func (ps *PermissionService) SetCacheInvalidator(invalidator PermissionCacheInvalidator) {
	ps.cacheInvalidator = invalidator
}

// InvalidateUserCache removes all cached permission entries for a specific user.
// Call this after changing a user's ProfileId or RoleId.
func (ps *PermissionService) InvalidateUserCache(userID string) {
	if ps.cacheInvalidator != nil {
		ps.cacheInvalidator.InvalidateUser(userID)
	}
}

// InvalidateCache flushes the entire permission cache.
// Call this after modifying profile/permission-set permissions.
func (ps *PermissionService) InvalidateCache() {
	if ps.cacheInvalidator != nil {
		ps.cacheInvalidator.Invalidate()
	}
}

// --- Object Permission Queries ---

// loadEffectiveObjectPermission loads permissions considering Profile AND Permission Sets.
func (ps *PermissionService) loadEffectiveObjectPermission(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
	if ps == nil || ps.repo == nil || user == nil {
		return nil, nil
	}
	if perm, handled, err := ps.cachedEffectiveObjectPermission(ctx, user, objectAPIName); handled {
		if err != nil {
			return nil, err
		}
		recordObjectPermissionCacheHit(ctx)
		return perm, nil
	}
	start := time.Now()
	perm, err := ps.repo.LoadEffectiveObjectPermission(ctx, user, objectAPIName)
	recordObjectPermissionDBFallback(ctx, time.Since(start))
	return perm, err
}

// loadEffectiveFieldPermission loads field permissions considering Profile AND Permission Sets.
func (ps *PermissionService) loadEffectiveFieldPermission(ctx context.Context, user *models.UserSession, objectAPIName, fieldAPIName string) (*models.FieldPermissions, error) {
	if ps == nil || ps.repo == nil || user == nil {
		return nil, nil
	}
	if perm, handled, err := ps.cachedEffectiveFieldPermission(ctx, user, objectAPIName, fieldAPIName); handled {
		if err != nil {
			return nil, err
		}
		recordFieldPermissionCacheHit(ctx)
		return perm, nil
	}
	start := time.Now()
	perm, err := ps.repo.LoadEffectiveFieldPermission(ctx, user, objectAPIName, fieldAPIName)
	recordFieldPermissionDBFallback(ctx, time.Since(start))
	return perm, err
}

// --- Core Permission Checks ---

// CheckObjectPermissionWithUser checks if a user has permission for an operation on an object.
// Operations: "Read", "Create", "Edit", "Delete" (case-insensitive)
// Returns true if the operation is permitted.
func (ps *PermissionService) CheckObjectPermissionWithUser(ctx context.Context, objectAPIName string, operation string, user *models.UserSession) bool {
	return ps.checkObjectPermissionWithState(ctx, objectAPIName, operation, user, newAccessPolicyEvalState())
}

func (ps *PermissionService) EffectiveAccess(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.EffectiveObjectAccess, error) {
	start := time.Now()
	fieldCount := 0
	defer func() {
		recordEffectiveAccessLoad(ctx, fieldCount, time.Since(start))
	}()

	access := &models.EffectiveObjectAccess{
		Object: models.ObjectAccess{
			Create:     ps.CheckObjectPermissionWithUser(ctx, objectAPIName, constants.PermCreate, user),
			Read:       ps.CheckObjectPermissionWithUser(ctx, objectAPIName, constants.PermRead, user),
			Update:     ps.CheckObjectPermissionWithUser(ctx, objectAPIName, constants.PermEdit, user),
			Delete:     ps.CheckObjectPermissionWithUser(ctx, objectAPIName, constants.PermDelete, user),
			Deactivate: ps.CheckObjectDeactivationPermissionWithUser(ctx, objectAPIName, user),
		},
		Fields: map[string]models.FieldAccess{},
	}

	var schema *models.ObjectMetadata
	if ps != nil && ps.metadata != nil {
		schema = ps.metadata.GetSchema(ctx, objectAPIName)
	}
	if schema != nil {
		fieldCount = len(schema.Fields)
		resolver := NewFieldAccessResolver(ps)
		for _, field := range schema.Fields {
			createable, err := resolver.EffectiveCreateable(ctx, &field, objectAPIName, user)
			if err != nil {
				return nil, err
			}
			updateable, err := resolver.EffectiveUpdateable(ctx, &field, objectAPIName, user)
			if err != nil {
				return nil, err
			}
			readable, err := ps.CheckFieldVisibilityWithSchema(ctx, schema, &field, user)
			if err != nil {
				return nil, err
			}
			fieldAccess := models.FieldAccess{
				Createable: createable,
				Updateable: updateable,
				Readable:   readable,
			}
			if field.IsVirtual {
				switch field.VirtualKind {
				case constants.VirtualFieldSystemPermission:
					fieldAccess.Createable = access.Object.Create
					fieldAccess.Updateable = access.Object.Update
					fieldAccess.Readable = access.Object.Read
				case constants.VirtualFieldCompound:
					fieldAccess.Createable = false
					fieldAccess.Updateable = false
					fieldAccess.Readable = access.Object.Read
				}
			}
			access.Fields[field.APIName] = fieldAccess
		}
	}

	var recordTypeRows []*models.RecordType
	if ps != nil && ps.metadata != nil {
		var err error
		recordTypeRows, err = ps.metadata.GetRecordTypes(ctx, objectAPIName)
		if err != nil {
			return nil, fmt.Errorf("load record types for effective access: %w", err)
		}
	}
	recordTypeGrants, hasExplicitRecordTypeGrants, err := ps.effectiveRecordTypeGrantsForObject(ctx, recordTypeRows, user)
	if err != nil {
		return nil, err
	}
	recordTypeSurface := recordtypes.BuildRawAccessSurface(recordTypeRows, recordTypeGrants, hasExplicitRecordTypeGrants)
	access.RecordTypes = make([]models.RecordTypeAvailability, 0, len(recordTypeSurface.Entries))
	for _, entry := range recordTypeSurface.Entries {
		access.RecordTypes = append(access.RecordTypes, models.RecordTypeAvailability{
			ID:            entry.ID,
			DeveloperName: entry.DeveloperName,
			Name:          entry.Name,
			Available:     entry.Available,
			Master:        entry.Master,
			Default:       entry.IsDefault,
		})
	}
	access.DefaultRecordTypeID = recordTypeSurface.DefaultRecordTypeID
	access.HasConcreteRecordTypes = recordTypeSurface.HasConcreteRecordTypes
	access.AvailableConcreteCount = recordTypeSurface.AvailableConcreteCount
	return access, nil
}

func (ps *PermissionService) effectiveRecordTypeGrantsForObject(
	ctx context.Context,
	recordTypeRows []*models.RecordType,
	user *models.UserSession,
) (map[string]models.PermissionSetRecordTypeVisibility, bool, error) {
	if ps == nil || user == nil || ps.HasSuperUserCapability(ctx, user) {
		return nil, false, nil
	}
	effective, err := ps.GetEffectiveRecordTypeVisibilities(ctx, user.ID, user.ProfileID)
	if err != nil {
		return nil, false, err
	}
	if len(effective) == 0 {
		return nil, false, nil
	}
	grants, ok := recordtypes.FilterGrantsForObject(recordTypeRows, effective)
	return grants, ok, nil
}

func (ps *PermissionService) CheckObjectPermissionOnRecord(ctx context.Context, schema *models.ObjectMetadata, record models.SObject, operation string, user *models.UserSession) bool {
	return ps.checkObjectPermissionOnRecordWithState(ctx, schema, record, operation, user, newAccessPolicyEvalState(), make(map[string]bool))
}

// CheckRecordReadAccessWithUser verifies that a concrete source row is readable
// through the same object + record/RLS authority used by user-facing record
// reads. It intentionally does not apply FLS; callers that serialize fields
// must run their field projection separately.
func (ps *PermissionService) CheckRecordReadAccessWithUser(ctx context.Context, objectAPIName string, record models.SObject, user *models.UserSession) bool {
	if ps == nil || user == nil || record == nil || strings.TrimSpace(objectAPIName) == "" {
		return false
	}
	if id, _ := record[constants.FieldID].(string); strings.TrimSpace(id) == "" {
		return false
	}
	if ps.metadata == nil {
		return false
	}
	schema := ps.metadata.GetSchema(ctx, objectAPIName)
	if schema == nil {
		return false
	}
	if !ps.CheckObjectPermissionOnRecord(ctx, schema, record, constants.PermRead, user) {
		return false
	}
	return ps.CheckRecordAccess(ctx, schema, record, constants.PermRead, user)
}

// checkObjectPermissionOnRecordWithState is the private record-aware entry
// point. It carries an evaluation state (in-evaluation cycle guard for AP
// expression / object-op marks) AND a separate `visited` delegation-chain
// tracker for ControlledByParent recursion. Two trackers are mandatory: the
// record-context branch evaluates parent AP directly via
// evaluateObjectAccessPolicy, which does NOT mark state.objectOps for the
// schema — so state.objectOps alone cannot terminate an A→B→A delegation
// chain when both parents have record-context AP. See parent_controlled.go.
func (ps *PermissionService) checkObjectPermissionOnRecordWithState(
	ctx context.Context,
	schema *models.ObjectMetadata,
	record models.SObject,
	operation string,
	user *models.UserSession,
	state *accessPolicyEvalState,
	visited map[string]bool,
) bool {
	if schema == nil {
		return false
	}
	if user == nil {
		return false
	}
	if state == nil {
		state = newAccessPolicyEvalState()
	}
	if visited == nil {
		visited = make(map[string]bool)
	}

	// ControlledByParent record-aware delegation. Even if the child itself has
	// no AccessPolicy, the PARENT may carry a record-context AP that needs the
	// parent record loaded — calling the non-record path here would evaluate
	// the parent AP with nil record and falsely deny (access_policy.go:144).
	if schema.SharingModel == constants.SharingModelControlledByParent {
		if ps.metadata == nil {
			return false
		}
		if parent := ps.metadata.GetParentControlledObject(ctx, schema); parent != "" {
			return ps.checkParentControlledRecordAwareCRUD(ctx, schema, record, parent, operation, user, state, visited)
		}
		return false
	}

	if !schema.UsesDerivedAccessPolicy() || !ps.accessPolicyRequiresRecordContext(schema, operation) {
		return ps.checkObjectPermissionWithState(ctx, schema.APIName, operation, user, state)
	}
	allowed, err := ps.evaluateObjectAccessPolicy(ctx, schema, record, operation, user, state)
	if err != nil {
		return false
	}
	return allowed
}

func (ps *PermissionService) checkObjectPermissionWithState(ctx context.Context, objectAPIName string, operation string, user *models.UserSession, state *accessPolicyEvalState) bool {
	if user == nil {
		return false
	}
	var schema *models.ObjectMetadata
	if ps.metadata != nil {
		schema = ps.metadata.GetSchema(ctx, objectAPIName)
	}
	return ps.checkObjectPermissionWithSchemaState(ctx, objectAPIName, schema, operation, user, state)
}

// The field projection already owns an admitted schema. Reuse that same fact
// for object gates instead of reloading metadata while projecting its fields.
func (ps *PermissionService) checkObjectPermissionWithSchemaState(ctx context.Context, objectAPIName string, schema *models.ObjectMetadata, operation string, user *models.UserSession, state *accessPolicyEvalState) bool {
	// No user = no access
	if user == nil {
		return false
	}
	if state == nil {
		state = newAccessPolicyEvalState()
	}
	stackKey := strings.ToLower(strings.TrimSpace(objectAPIName)) + ":" + normalizePolicyOperation(operation)
	if stackKey != ":" {
		if state.objectOps[stackKey] {
			return false
		}
		state.objectOps[stackKey] = true
		defer delete(state.objectOps, stackKey)
	}

	requiredSystemPerms := requiredSystemPermissionsForSetupSchema(schema, operation)
	if normalizePolicyOperation(operation) == "objectDelete" && schemaBlocksGenericDelete(schema) {
		return false
	}

	// Metadata-declared setup/security gates must run before the data-plane
	// super-user bypass. ModifyAllData is allowed to bypass CRUD/FLS/RLS, but it
	// is not a wildcard for modeled setup/admin permissions such as ManageUsers.
	if len(requiredSystemPerms) > 0 && !ps.hasAnySystemPermission(ctx, user, requiredSystemPerms) {
		return false
	}

	requiredReadGranted := false
	if operation == constants.PermRead && schema != nil && schema.RequiredReadPermission != "" {
		if schema.RequiredReadPermission == "BLOCKED" {
			return false
		}
		perms := splitRequiredReadPermission(schema.RequiredReadPermission)
		// Fail-closed: a non-empty RequiredReadPermission that yields no valid
		// tokens after parsing (e.g. ",", "  ,  ", ",,,") is malformed — deny
		// rather than silently opening the gate via an empty permission list.
		// See #564 Codex BLOCK: empty-slice-after-filter fail-open regression.
		if len(perms) == 0 {
			return false
		}
		if !ps.hasAnySystemPermission(ctx, user, perms) {
			return false
		}
		requiredReadGranted = true
	}

	// Super-user capability bypass grants full data-plane access after
	// setup/security gates have been satisfied.
	if ps.HasSuperUserCapability(ctx, user) {
		return true
	}

	// system tables: Read is granted to all authenticated users, EXCEPT tables
	// with RequiredReadPermission — those require the specified system permission.
	// Write is governed by RequiredWritePermission (checked above via requiredSystemPerms).
	//
	// RequiredReadPermission supports comma-separated values (e.g. "ViewAllData,ModifyAllData"):
	// any one matching permission grants read access (OR semantics). This allows tables
	// whose audit rows may carry L3 record values to require a privileged-data tier
	// permission instead of the lighter ViewSetup. See #503 and docs/design/system-tables-permissions.md.
	if operation == constants.PermRead && schema != nil {
		if strings.HasPrefix(string(schema.TableType), "system") {
			if schema.RequiredReadPermission != "" {
				return requiredReadGranted
			}
			return true
		}
	}

	// ControlledByParent capability resolution. A child write permission declared
	// in metadata is authoritative for Gate-1 writes; otherwise inherited child
	// mutations derive from parent Edit, not the parent's same-named operation.
	// Recurse into the same entry point so the recursed frame does its own
	// state.objectOps mark at the top of checkObjectPermissionWithState; do
	// NOT pre-mark here, otherwise the recursed call's stackKey check would
	// self-deny on parent:op. Cycle A→B→A terminates naturally because
	// state.objectOps[a:op] is still set on the outer frame when the inner
	// B→A recursion hits the same stackKey check.
	if schema != nil && ps.metadata != nil {
		if schema.SharingModel == constants.SharingModelControlledByParent {
			parent := ps.metadata.GetParentControlledObject(ctx, schema)
			if parent == "" {
				return false
			}
			edge := ps.parentControlledSharingEdgeForSchema(ctx, schema, schema.APIName)
			if edge.malformed {
				return false
			}
			if parentControlledRequiredWritePermissionIsCapability(schema, operation) {
				return true
			}
			parentOperation := parentControlledParentCapabilityOperation(operation)
			if strings.EqualFold(operation, constants.PermCreate) {
				parentSchema := ps.metadata.GetSchema(ctx, parent)
				if parentSchema == nil {
					return false
				}
				// Insert's record-aware parent gate is validateNonPolymorphicLookups:
				// it evaluates concrete sharing-parent FK writes against the referenced
				// parent record. If parent Edit needs record context, the non-record
				// preflight must not evaluate that policy with record == nil. Insert
				// marks lookup validation with create intent so no-parent creates fail
				// closed when this deferral is needed, unless child RequiredWritePermission
				// is the authoritative write capability.
				if ps.requiresRecordAwareCRUDDeferral(ctx, parentSchema, parentOperation, make(map[string]bool)) {
					return true
				}
			}
			return ps.checkObjectPermissionWithState(ctx, parent, parentOperation, user, state)
		}
	}

	if schema != nil && schema.UsesDerivedAccessPolicy() {
		allowed, err := ps.evaluateObjectAccessPolicy(ctx, schema, nil, operation, user, state)
		if err != nil {
			return false
		}
		return allowed
	}

	// Query database for effective permission (Profile OR Permission Sets)
	perm, err := ps.loadEffectiveObjectPermission(ctx, user, objectAPIName)
	if err != nil {
		return false
	}

	// No permission record = no access
	if perm == nil {
		return false
	}

	// Check specific operation - Salesforce-native format only
	switch operation {
	case constants.PermRead:
		return perm.PermissionsRead
	case constants.PermCreate:
		return perm.PermissionsCreate
	case constants.PermEdit:
		return perm.PermissionsEdit
	case constants.PermDelete:
		return perm.PermissionsDelete
	case constants.PermViewAllRecords:
		return perm.PermissionsViewAllRecords
	case constants.PermModifyAllRecords:
		return perm.PermissionsModifyAllRecords
	default:
		return false
	}
}

func (ps *PermissionService) HasCapability(ctx context.Context, user *models.UserSession, capability constants.Permission) bool {
	if capability == "" {
		return false
	}
	return ps.hasDirectSystemPermission(ctx, user, string(capability))
}

func (ps *PermissionService) HasSuperUserCapability(ctx context.Context, user *models.UserSession) bool {
	return ps.HasCapability(ctx, user, constants.SuperUserCapability)
}

func (ps *PermissionService) hasDirectSystemPermission(ctx context.Context, user *models.UserSession, permission string) bool {
	if perms, ok := ps.effectiveSystemPermissionsForDecision(ctx, user); ok {
		return perms[permission]
	}
	return false
}

// CheckSystemPermission checks if a user has a specific system permission.
// Resolves effective permissions from the database via the repository layer
// (profile grants + permission set assignments + groups - muting). The
// super-user data capability is not a wildcard for setup/admin permissions.
func (ps *PermissionService) CheckSystemPermission(ctx context.Context, user *models.UserSession, permission string) bool {
	if ps == nil || user == nil || permission == "" {
		return false
	}
	if perms, ok := ps.effectiveSystemPermissionsForDecision(ctx, user); ok {
		return perms[permission]
	}
	return false
}

func (ps *PermissionService) effectiveSystemPermissionsForDecision(ctx context.Context, user *models.UserSession) (perms map[string]bool, ok bool) {
	if user == nil {
		return nil, false
	}
	if perms, ok := systemPermissionsFromContext(ctx, user.ID, user.ProfileID); ok {
		return perms, true
	}
	if ps == nil || ps.repo == nil {
		return nil, false
	}

	// Serve from the request-scoped cache when present (issue #1585): system
	// permissions are checked repeatedly during record/metadata access. Resolving
	// them once per request avoids an O(records) GetEffectiveSystemPermissions
	// blast during list/lookup hydration.
	if perms, ok := ps.cachedSystemPermissions(ctx, user.ID, user.ProfileID); ok {
		return perms, true
	}

	defer func() {
		if recover() != nil {
			perms = nil
			ok = false
		}
	}()
	perms, err := ps.GetEffectiveSystemPermissions(ctx, user.ID, user.ProfileID)
	if err != nil {
		return nil, false
	}
	return perms, true
}

type effectivePermissionsContextKey struct{}

type effectivePermissionsContextValue struct {
	userID      string
	profileID   string
	systemPerms map[string]bool
	customPerms map[string]bool
}

// WithEffectivePermissions installs request-authenticated effective permission
// inputs for the current session user. Consumers must still pass the matching
// user/profile before the values are reused.
func WithEffectivePermissions(ctx context.Context, userID, profileID string, systemPerms, customPerms map[string]bool) context.Context {
	if ctx == nil {
		ctx = context.Background()
	}
	return context.WithValue(ctx, effectivePermissionsContextKey{}, effectivePermissionsContextValue{
		userID:      strings.TrimSpace(userID),
		profileID:   strings.TrimSpace(profileID),
		systemPerms: systemPerms,
		customPerms: customPerms,
	})
}

func systemPermissionsFromContext(ctx context.Context, userID, profileID string) (map[string]bool, bool) {
	value, ok := effectivePermissionsFromContext(ctx, userID, profileID)
	if !ok || value.systemPerms == nil {
		return nil, false
	}
	return value.systemPerms, true
}

func customPermissionsFromContext(ctx context.Context, userID, profileID string) (map[string]bool, bool) {
	value, ok := effectivePermissionsFromContext(ctx, userID, profileID)
	if !ok || value.customPerms == nil {
		return nil, false
	}
	return value.customPerms, true
}

func effectivePermissionsFromContext(ctx context.Context, userID, profileID string) (effectivePermissionsContextValue, bool) {
	if ctx == nil {
		return effectivePermissionsContextValue{}, false
	}
	value, ok := ctx.Value(effectivePermissionsContextKey{}).(effectivePermissionsContextValue)
	if !ok {
		return effectivePermissionsContextValue{}, false
	}
	if value.userID != strings.TrimSpace(userID) || value.profileID != strings.TrimSpace(profileID) {
		return effectivePermissionsContextValue{}, false
	}
	return value, true
}

// CheckPermissionOrErrorWithUser checks permission and returns a specific PermissionError if false
func (ps *PermissionService) CheckPermissionOrErrorWithUser(ctx context.Context, objectAPIName string, operation string, user *models.UserSession) error {
	if !ps.CheckObjectPermissionWithUser(ctx, objectAPIName, operation, user) {
		return errors.NewPermissionError(operation, objectAPIName)
	}
	return nil
}

// IsBlockedObject returns true if the object is a hard-blocked security infrastructure
// table that must never be accessible via generic CRUD/query paths, regardless of
// SecurityContext. These tables (Session, OAuthCode, Config, etc.) are only accessible
// through dedicated management APIs.
func (ps *PermissionService) IsBlockedObject(ctx context.Context, objectAPIName string, operation string) bool {
	perms := ps.requiredSystemPermissionsForSetupObject(ctx, objectAPIName, operation)
	for _, p := range perms {
		if p == blockedPermission {
			return true
		}
	}
	// Also check RequiredReadPermission=BLOCKED for read operations
	if strings.EqualFold(operation, constants.PermRead) && ps.metadata != nil {
		if schema := ps.metadata.GetSchema(ctx, objectAPIName); schema != nil {
			if schema.RequiredReadPermission == "BLOCKED" {
				return true
			}
		}
	}
	return false
}

// FieldReadBlockedByMetadata reports whether field metadata declares a generic
// read block. Concrete object/field membership belongs in metadata rows.
func FieldReadBlockedByMetadata(field *models.FieldMetadata) bool {
	if field == nil {
		return false
	}
	return strings.EqualFold(strings.TrimSpace(field.ReadProtectionPolicy), constants.FieldReadProtectionBlocked)
}

// IsFieldReadBlocked returns true for fields that generic record/query paths
// must never expose. Metadata owns the contract through ReadProtectionPolicy.
func (ps *PermissionService) IsFieldReadBlocked(ctx context.Context, objectAPIName, fieldAPIName string) bool {
	if ps != nil && ps.metadata != nil {
		if schema := ps.metadata.GetSchema(ctx, objectAPIName); schema != nil {
			if FieldReadBlockedByMetadata(models.FindField(schema, fieldAPIName)) {
				return true
			}
		}
	}
	return false
}

// Record-level access functions are in permission_record_access.go:
// - CheckRecordAccess, checkManualShareAccess
// - checkTeamMemberAccess, accessLevelAllowsOperation

// CheckFieldEditabilityWithUser checks if a field can be edited by the current user
func (ps *PermissionService) CheckFieldEditabilityWithUser(ctx context.Context, objectAPIName, fieldAPIName string, user *models.UserSession) (bool, error) {
	return ps.CheckFieldEditabilityOnRecord(ctx, objectAPIName, fieldAPIName, nil, user)
}

func (ps *PermissionService) CheckFieldEditabilityOnRecord(ctx context.Context, objectAPIName, fieldAPIName string, record models.SObject, user *models.UserSession) (bool, error) {
	if ctx != nil {
		if err := ctx.Err(); err != nil {
			return false, err
		}
	}
	var schema *models.ObjectMetadata
	if ps != nil && ps.metadata != nil {
		schema = ps.metadata.GetSchema(ctx, objectAPIName)
	}
	evaluation := ps.newFieldAccessEvaluation(ctx, objectAPIName, schema, record, user)
	return evaluation.resolve(ctx, fieldAPIName, models.FindField(schema, fieldAPIName), fieldAccessEdit)
}

// CheckFieldVisibilityWithUser checks if a field is visible to the current user
func (ps *PermissionService) CheckFieldVisibilityWithUser(ctx context.Context, objectAPIName, fieldAPIName string, user *models.UserSession) (bool, error) {
	if ctx != nil {
		if err := ctx.Err(); err != nil {
			return false, err
		}
	}
	if user == nil {
		return false, nil
	}
	var schema *models.ObjectMetadata
	var field *models.FieldMetadata
	if ps != nil && ps.metadata != nil {
		schema = ps.metadata.GetSchema(ctx, objectAPIName)
		field = models.FindField(schema, fieldAPIName)
	}
	return ps.checkFieldVisibilityWithSchema(ctx, objectAPIName, fieldAPIName, schema, field, user)
}

// CheckFieldVisibilityWithSchema evaluates field visibility against an already
// admitted immutable schema. Bulk query/record projections must use this form:
// resolving the same request-bound Catalog object once per field used to deep
// copy the complete object hundreds of times in the independent Runtime.
func (ps *PermissionService) CheckFieldVisibilityWithSchema(ctx context.Context, schema *models.ObjectMetadata, field *models.FieldMetadata, user *models.UserSession) (bool, error) {
	if schema == nil || field == nil {
		return false, nil
	}
	return ps.checkFieldVisibilityWithSchema(ctx, schema.APIName, field.APIName, schema, field, user)
}

func (ps *PermissionService) checkFieldVisibilityWithSchema(ctx context.Context, objectAPIName, fieldAPIName string, schema *models.ObjectMetadata, field *models.FieldMetadata, user *models.UserSession) (bool, error) {
	evaluation := ps.newFieldAccessEvaluation(ctx, objectAPIName, schema, nil, user)
	return evaluation.resolve(ctx, fieldAPIName, field, fieldAccessRead)
}

// RefreshPermissions reloads permissions from the database
// This specifically refreshes the Role Hierarchy cache.
// Object/Field permissions are not cached (checked per-request), so they don't need refreshing.
func (ps *PermissionService) RefreshPermissions(ctx context.Context) error {
	ps.refreshRoleHierarchy(ctx)
	return nil
}

// GetEffectiveSchema returns the schema with field-level visibility applied
func (ps *PermissionService) GetEffectiveSchema(ctx context.Context, schema *models.ObjectMetadata, user *models.UserSession) (*models.ObjectMetadata, error) {
	if ctx != nil {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
	}
	if schema == nil {
		return nil, nil
	}

	// Create a copy of the schema to avoid mutating the original (which might be cached)
	effectiveSchema := *schema
	effectiveSchema.Fields = make([]models.FieldMetadata, 0, len(schema.Fields))
	// The source index points at unfiltered fields. A filtered view must derive
	// lookups from its own field slice, never retain that broader authority.
	effectiveSchema.FieldsMap = nil

	for _, field := range schema.Fields {
		visible, err := ps.CheckFieldVisibilityWithSchema(ctx, schema, &field, user)
		if err != nil {
			return nil, err
		}
		if visible {
			effectiveSchema.Fields = append(effectiveSchema.Fields, field)
		}
	}

	return &effectiveSchema, nil
}

// GetObjectPermissions retrieves all object permissions for a profile
func (ps *PermissionService) GetObjectPermissions(ctx context.Context, profileID string) ([]models.ObjectPermissions, error) {
	return ps.repo.ListObjectPermissions(ctx, profileID)
}

// RejectParentControlledChildWrite returns a *errors.ValidationError when the
// caller is attempting to author an ObjectPermissions row for a SobjectType
// that is parent-controlled. For well-formed children, runtime CRUD checks
// delegate to the parent's access policy (service.go:399-410). Malformed
// ControlledByParent schemas without a configured sharing-parent relationship
// fail closed. In both cases, seeding and authoring suppress the child's row
// (permission_seeder.go:85) so stale child OP rows cannot become a second read
// path.
//
// The helper preserves the existing malformed-payload error ordering: callers
// in the generic-CRUD path use a comma-ok type assertion and pass "" for
// missing or non-string payload entries; the helper short-circuits so
// downstream validation emits the canonical required-field / type-mismatch
// error. A non-empty SobjectType that metadata cannot resolve fails closed:
// persisting an ObjectPermissions row for an unknown object would create the
// same non-authoritative admin-visible state as a parent-controlled child row.
func (ps *PermissionService) RejectParentControlledChildWrite(ctx context.Context, sobjectType string) error {
	if ps == nil || ps.metadata == nil {
		return nil
	}
	if sobjectType == "" {
		return nil
	}
	schema := ps.metadata.GetSchema(ctx, sobjectType)
	if schema == nil {
		return errors.NewValidationError(
			constants.FieldObjectPermissions_SobjectType,
			"ObjectPermissions row references unknown SobjectType "+sobjectType,
		)
	}
	if schema.SharingModel == constants.SharingModelControlledByParent {
		parent := ps.metadata.GetParentControlledObject(ctx, schema)
		if parent == "" {
			return errors.NewValidationError(
				constants.FieldObjectPermissions_SobjectType,
				"Access to "+sobjectType+" is parent-controlled, but no sharing-parent relationship is configured. Fix object metadata before authoring object permissions.",
			)
		}
		return errors.NewValidationError(
			constants.FieldObjectPermissions_SobjectType,
			"Access to "+sobjectType+" is inherited from "+parent+
				". To change read, create, edit, or delete access, update the "+parent+" object permissions.",
		)
	}
	return nil
}

// UpdateObjectPermission creates or updates an object permission
func (ps *PermissionService) UpdateObjectPermission(ctx context.Context, perm models.ObjectPermissions) error {
	// Validate permission dependencies before persisting
	if err := ValidateObjectPermissionDependencies(perm); err != nil {
		return errors.NewValidationError("", err.Error())
	}

	// Parent-controlled authoring guard (F1): refuse to persist a row that
	// runtime CRUD will ignore. Mirrors the validatePermissionDependencies
	// idiom on the same authoring surface.
	if err := ps.RejectParentControlledChildWrite(ctx, perm.SobjectType); err != nil {
		return err
	}

	if err := ps.repo.UpsertObjectPermission(ctx, perm); err != nil {
		return err
	}
	ps.InvalidateCache()
	if ps.setupAuditor != nil {
		ps.setupAuditor.Record(ctx, "Changed object permission", "Permissions", "Updated "+perm.SobjectType+" permissions", "")
	}
	return nil
}

// UpdateObjectPermissionTx creates or updates an object permission within a transaction
// Note: This method implicitly uses the transaction in the context.
func (ps *PermissionService) UpdateObjectPermissionTx(ctx context.Context, perm models.ObjectPermissions) error {
	// Validate permission dependencies before persisting
	if err := ValidateObjectPermissionDependencies(perm); err != nil {
		return errors.NewValidationError("", err.Error())
	}

	// Parent-controlled authoring guard (F2): TX variant of F1.
	if err := ps.RejectParentControlledChildWrite(ctx, perm.SobjectType); err != nil {
		return err
	}

	// We delegate to the standard Upsert because Repo now extracts TX from context
	return ps.repo.UpsertObjectPermission(ctx, perm)
}

// updateObjectPermission creates or updates an object permission using the provided executor

// GetFieldPermissions retrieves all field permissions for a profile
func (ps *PermissionService) GetFieldPermissions(ctx context.Context, profileID string) ([]models.FieldPermissions, error) {
	return ps.repo.ListFieldPermissions(ctx, profileID)
}

// GrantFieldPermissions grants permissions for a field (Public wrapper for update)
func (ps *PermissionService) GrantFieldPermissions(ctx context.Context, perms models.FieldPermissions) error {
	return ps.UpdateFieldPermission(ctx, perms)
}

// UpdateFieldPermission creates or updates a field permission
func (ps *PermissionService) UpdateFieldPermission(ctx context.Context, perms models.FieldPermissions) error {
	if err := ValidateFieldPermissionDependencies(perms); err != nil {
		return errors.NewValidationError(constants.FieldFieldPermissions_PermissionsEdit, err.Error())
	}
	if err := ps.repo.UpsertFieldPermission(ctx, perms); err != nil {
		return err
	}
	ps.InvalidateCache()
	return nil
}

// CountObjectPermissionsByProfile returns count of object permissions for a profile
func (ps *PermissionService) CountObjectPermissionsByProfile(ctx context.Context, profileID string) (int, error) {
	return ps.repo.CountObjectPermissionsByProfile(ctx, profileID)
}

// BulkEnsureProfilePermissionSets creates or retrieves PermissionSets for multiple profiles in bulk.
func (ps *PermissionService) BulkEnsureProfilePermissionSets(ctx context.Context, profiles []struct{ ID, Name string }) (map[string]string, error) {
	return ps.repo.BulkEnsureProfilePermissionSets(ctx, profiles)
}

// BulkCountObjectPermissionsByProfiles returns object permission counts for multiple profiles.
func (ps *PermissionService) BulkCountObjectPermissionsByProfiles(ctx context.Context, profileIDs []string) (map[string]int, error) {
	return ps.repo.BulkCountObjectPermissionsByProfiles(ctx, profileIDs)
}

// BulkCountFieldPermissionsByProfiles returns field permission counts for multiple profiles.
func (ps *PermissionService) BulkCountFieldPermissionsByProfiles(ctx context.Context, profileIDs []string) (map[string]int, error) {
	return ps.repo.BulkCountFieldPermissionsByProfiles(ctx, profileIDs)
}

// BulkCountObjectPermissionsByProfilesAndTypes returns object permission counts for multiple
// profiles restricted to specific SObject API names.
func (ps *PermissionService) BulkCountObjectPermissionsByProfilesAndTypes(ctx context.Context, profileIDs []string, objectAPINames []string) (map[string]int, error) {
	return ps.repo.BulkCountObjectPermissionsByProfilesAndTypes(ctx, profileIDs, objectAPINames)
}

// BulkCountFieldPermissionsByProfilesAndTypes returns field permission counts for multiple
// profiles restricted to specific SObject API names.
func (ps *PermissionService) BulkCountFieldPermissionsByProfilesAndTypes(ctx context.Context, profileIDs []string, objectAPINames []string) (map[string]int, error) {
	return ps.repo.BulkCountFieldPermissionsByProfilesAndTypes(ctx, profileIDs, objectAPINames)
}

// EnsureProfilePermissionSet creates or retrieves the PermissionSet that backs a Profile.
// In Salesforce, every Profile has an implicit PermissionSet with IsOwnedByProfile=true.
// Uses INSERT IGNORE + SELECT to handle concurrent creation safely.
func (ps *PermissionService) EnsureProfilePermissionSet(ctx context.Context, profileID, profileName string) (string, error) {
	return ps.repo.EnsureProfilePermissionSet(ctx, profileID, profileName)
}

// CleanupProfilePermissionSet soft-deletes the PermissionSet owned by a Profile.
func (ps *PermissionService) CleanupProfilePermissionSet(ctx context.Context, profileID string) error {
	return ps.repo.CleanupProfilePermissionSet(ctx, profileID)
}

// InsertMissingObjectPermissions seeds only absent object permissions and never
// overwrites existing rows. This is the only seed/reconcile write path, so a
// default pass cannot clobber or widen import-/admin-owned access. (Explicit
// single-row admin edits go through UpsertObjectPermission, not here.)
func (ps *PermissionService) InsertMissingObjectPermissions(ctx context.Context, perms []models.ObjectPermissions) error {
	for _, perm := range perms {
		if err := ValidateObjectPermissionDependencies(perm); err != nil {
			return errors.NewValidationError("", err.Error())
		}
	}
	if err := ps.repo.InsertMissingObjectPermissions(ctx, perms); err != nil {
		return err
	}
	ps.InvalidateCache()
	if ps.setupAuditor != nil && len(perms) > 0 {
		ps.setupAuditor.Record(ctx, "Seeded object permissions", "Permissions", "Inserted missing object permissions", "")
	}
	return nil
}

// InsertMissingFieldPermissions seeds only absent field permissions and never
// overwrites existing rows. Additive-only field counterpart of
// InsertMissingObjectPermissions.
func (ps *PermissionService) InsertMissingFieldPermissions(ctx context.Context, perms []models.FieldPermissions) error {
	if err := ps.repo.InsertMissingFieldPermissions(ctx, perms); err != nil {
		return err
	}
	ps.InvalidateCache()
	if ps.setupAuditor != nil && len(perms) > 0 {
		ps.setupAuditor.Record(ctx, "Seeded field permissions", "Permissions", "Inserted missing field permissions", "")
	}
	return nil
}

func (ps *PermissionService) DeleteFieldPermissionsForFields(ctx context.Context, objectAPIName string, fieldAPINames []string) error {
	if err := ps.repo.DeleteFieldPermissionsForFields(ctx, objectAPIName, fieldAPINames); err != nil {
		return err
	}
	return nil
}

type roleLifecycleLocker interface {
	LockRoleLifecycle(context.Context, string) (*models.UserRole, error)
}

// LockRoleLifecycle holds the live selector authority through the owning transaction.
func (ps *PermissionService) LockRoleLifecycle(ctx context.Context, roleID string) (*models.UserRole, error) {
	locker, ok := ps.repo.(roleLifecycleLocker)
	if !ok {
		return nil, errors.NewInternalError("Role lifecycle lock authority is unavailable", nil)
	}
	return locker.LockRoleLifecycle(ctx, roleID)
}

type profileLifecyclesLocker interface {
	LockProfileLifecycles(context.Context, []string) ([]*models.Profile, error)
}

// LockProfileLifecycles returns exact transaction-current selector facts.
func (ps *PermissionService) LockProfileLifecycles(ctx context.Context, profileIDs []string) ([]*models.Profile, error) {
	locker, ok := ps.repo.(profileLifecyclesLocker)
	if !ok {
		return nil, errors.NewInternalError("Profile lifecycle lock authority is unavailable", nil)
	}
	return locker.LockProfileLifecycles(ctx, profileIDs)
}

// GetAllProfiles returns all profiles from the database.
func (ps *PermissionService) GetAllProfiles(ctx context.Context) ([]*models.Profile, error) {
	return ps.repo.GetAllProfiles(ctx)
}

// isFieldSystemReadOnlyByName checks if a field is a system read-only field based on its name
func isFieldSystemReadOnlyByName(fieldAPIName string) bool {
	switch fieldAPIName {
	case constants.FieldID,
		constants.FieldCreatedDate,
		constants.FieldCreatedByID,
		constants.FieldLastModifiedDate,
		constants.FieldLastModifiedByID,
		constants.FieldIsDeleted:
		return true
	default:
		return false
	}
}

// IsFieldSystemReadOnly checks if a field is a system audit field (always read-only).
// The context, metadata, and objectName params are reserved for future richer checking
// but currently only the field name is evaluated.
func IsFieldSystemReadOnly(_ context.Context, _ interface{}, _ string, fieldAPIName string) bool {
	return isFieldSystemReadOnlyByName(fieldAPIName)
}

// GrantInitialPermissions was a legacy bootstrap helper that built ObjectPermissions
// rows directly from an objectAPIName, bypassing BuildPermissionsForProfileAndSchema's
// isParentControlled gate (permission_seeder.go:76-115). It had zero in-tree callers
// at HEAD (verified by `rg -n 'GrantInitialPermissions'`) and was superseded by
// SeedPermissionsForObject (permission_seeder.go:179-218) which routes through the
// build helper. Removed in PR #1242 to eliminate a contract-violating constructor
// that could re-introduce the parent-controlled write-bypass if someone wired it
// back in.

// Role hierarchy functions are in permission_role_hierarchy.go:
// - refreshRoleHierarchy, getRoleAncestors, isUserAboveInHierarchy, getRecordOwnerRoleID, RefreshRoleHierarchy

// CreateRole creates a new role
func (ps *PermissionService) CreateRole(ctx context.Context, name, description string, parentRoleID *string) (*models.UserRole, error) {
	// Validate parent role if provided
	if parentRoleID != nil {
		parent, err := ps.repo.GetRole(ctx, *parentRoleID)
		if err != nil {
			return nil, errors.NewInternalError("failed to validate parent role", err)
		}
		if parent == nil {
			return nil, errors.NewValidationError("parent_role_id", "parent role does not exist")
		}
	}

	id, err := ps.repo.CreateRole(ctx, name, description, parentRoleID)
	if err != nil {
		return nil, err
	}

	// Refresh cache
	ps.refreshRoleHierarchy(ctx)

	return &models.UserRole{
		ID:                id,
		Name:              name,
		RollupDescription: &description,
		ParentRoleID:      parentRoleID,
	}, nil
}

// GetRole retrieves a role by ID
func (ps *PermissionService) GetRole(ctx context.Context, id string) (*models.UserRole, error) {
	return ps.repo.GetRole(ctx, id)
}

// GetAllRoles retrieves all roles
func (ps *PermissionService) GetAllRoles(ctx context.Context) ([]*models.UserRole, error) {
	return ps.repo.GetAllRoles(ctx)
}

// UpdateRole updates an existing role
func (ps *PermissionService) UpdateRole(ctx context.Context, id string, name, description string, parentRoleID *string) (*models.UserRole, error) {
	// Validate parent role if provided
	if parentRoleID != nil {
		// Prevent self-reference
		if *parentRoleID == id {
			return nil, errors.NewValidationError("parent_role_id", "cannot be self")
		}

		parent, err := ps.repo.GetRole(ctx, *parentRoleID)
		if err != nil {
			return nil, errors.NewInternalError("failed to validate parent role", err)
		}
		if parent == nil {
			return nil, errors.NewValidationError("parent_role_id", "parent role does not exist")
		}

		// H7 fix: detect indirect cycles (A→B→...→A) by walking ancestor chain
		ancestors := ps.GetRoleAncestors(*parentRoleID)
		for _, ancestorID := range ancestors {
			if ancestorID == id {
				return nil, errors.NewValidationError("parent_role_id", "would create a cycle in role hierarchy")
			}
		}
	}

	if err := ps.repo.UpdateRole(ctx, id, name, description, parentRoleID); err != nil {
		return nil, err
	}

	// Refresh cache
	ps.refreshRoleHierarchy(ctx)

	return ps.GetRole(ctx, id)
}

// DeleteRole deletes a role
func (ps *PermissionService) DeleteRole(ctx context.Context, id string) error {
	if err := ps.repo.DeleteRole(ctx, id); err != nil {
		return err
	}
	// Refresh cache
	ps.refreshRoleHierarchy(ctx)
	return nil
}

// Sharing rule functions are in permission_sharing_rules.go:

// - isUserInRoleOrBelow, checkSharingRuleAccess, evaluateSharingCriteria

// Permission Set functions are in permission_perm_sets.go:
// - GetPermissionSetObjectPermissions, GetPermissionSetFieldPermissions
// - UpdatePermissionSetObjectPermission, UpdatePermissionSetFieldPermission
// - GetEffectiveObjectPermissions, GetEffectiveFieldPermissions
// - CreatePermissionSet, UpdatePermissionSet, DeletePermissionSet

// --- Salesforce-Compatible System Permissions ---

// GetUserProfileID returns the user's current profile ID from the database.
// Use this instead of the JWT-embedded ProfileId when up-to-date data matters
// (e.g., /userinfo, PermissionLoader) — the JWT value is baked at session
// creation and becomes stale when a profile is reassigned.
func (ps *PermissionService) GetUserProfileID(ctx context.Context, userID string) (string, error) {
	return ps.repo.GetUserProfileID(ctx, userID)
}

// GetUserRoleID returns the user's current role ID from the database.
// Use this instead of the JWT-embedded RoleId when up-to-date data matters
// — the JWT value is baked at session creation and becomes stale when a role
// is reassigned.
func (ps *PermissionService) GetUserRoleID(ctx context.Context, userID string) (*string, error) {
	return ps.userRepo.GetUserRoleID(ctx, userID)
}

// GetEffectiveSystemPermissions returns all system permissions for a user
// following Salesforce's resolution order: Profile → PermSet → Groups → Muting
func (ps *PermissionService) GetEffectiveSystemPermissions(ctx context.Context, userID, profileID string) (map[string]bool, error) {
	if perms, ok := systemPermissionsFromContext(ctx, userID, profileID); ok {
		return perms, nil
	}
	if perms, ok := ps.cachedSystemPermissions(ctx, userID, profileID); ok {
		return perms, nil
	}
	start := time.Now()
	perms, err := ps.repo.GetEffectiveSystemPermissions(ctx, userID, profileID)
	recordSystemPermissionLoad(ctx, time.Since(start))
	return perms, err
}

// GetEffectiveRecordTypeVisibilities returns effective record type grants from
// the user's profile baseline plus additive permission sets. The repository may
// synthesize unavailable rows for missing active record types on profile-managed
// objects; objects with no profile baseline keep the historical all-active
// fallback.
func (ps *PermissionService) GetEffectiveRecordTypeVisibilities(ctx context.Context, userID, profileID string) (map[string]models.PermissionSetRecordTypeVisibility, error) {
	start := time.Now()
	visibilities, err := ps.repo.GetEffectiveRecordTypeVisibilities(ctx, userID, profileID)
	recordRecordTypePermissionLoad(ctx, len(visibilities), time.Since(start))
	return visibilities, err
}

// GetEffectiveAppVisibilities returns effective app grants from the user's
// profile baseline plus additive permission sets.
func (ps *PermissionService) GetEffectiveAppVisibilities(ctx context.Context, userID, profileID string) (map[string]models.ProfileAppVisibility, error) {
	return ps.repo.GetEffectiveAppVisibilities(ctx, userID, profileID)
}

// GetEffectiveTabSettings returns effective tab visibility settings using
// Salesforce's most-permissive profile plus permission-set ordering.
func (ps *PermissionService) GetEffectiveTabSettings(ctx context.Context, userID, profileID string) (map[string]models.ProfileTabSetting, error) {
	return ps.repo.GetEffectiveTabSettings(ctx, userID, profileID)
}

// EnsureNavigationVisibilityForProfile creates missing canonical app and tab
// visibility rows for a Profile. Callers use this on Profile lifecycle changes
// so fail-closed navigation reads never strand new or reactivated profiles.
func (ps *PermissionService) EnsureNavigationVisibilityForProfile(ctx context.Context, profileID string) error {
	return ps.repo.EnsureNavigationVisibilityForProfile(ctx, profileID)
}

// GetEffectiveCustomPermissions returns all custom permissions for a user
func (ps *PermissionService) GetEffectiveCustomPermissions(ctx context.Context, userID, profileID string) (map[string]bool, error) {
	if perms, ok := ps.cachedCustomPermissions(ctx, userID, profileID); ok {
		return perms, nil
	}
	start := time.Now()
	perms, err := ps.repo.GetEffectiveCustomPermissions(ctx, userID, profileID)
	recordCustomPermissionLoad(ctx, time.Since(start))
	return perms, err
}

// ValidatePermissionPrerequisites checks that a set of permission changes, when
// applied to the current grants, produces a final state where all prerequisite
// relationships are satisfied. This validates the FINAL intended state, not
// intermediate steps, so it is order-independent.
//
// The prerequisite chain is read from the row-driven _System_Permission
// catalog so admin edits to the Requires field on any permission row take
// effect without a backend release.
//
// Returns nil if valid, or an error describing the first violated prerequisite.
func (ps *PermissionService) ValidatePermissionPrerequisites(ctx context.Context, currentGrants map[string]bool, changes map[string]bool) error {
	finalState := make(map[string]bool, len(currentGrants))
	for k, v := range currentGrants {
		finalState[k] = v
	}
	for k, v := range changes {
		finalState[k] = v
	}

	catalog, err := ps.ListSystemPermissions(ctx)
	if err != nil {
		return fmt.Errorf("load permission catalog for prerequisite check: %w", err)
	}
	for _, p := range catalog {
		if p.RequiresPermission == nil || *p.RequiresPermission == "" {
			continue
		}
		if finalState[p.APIName] && !finalState[*p.RequiresPermission] {
			return fmt.Errorf("%s requires %s to be enabled", p.APIName, *p.RequiresPermission)
		}
	}
	return nil
}

// LockPermissionSet acquires a row-level lock and verifies the PermissionSet exists.
// Must be called inside a transaction.
func (ps *PermissionService) LockPermissionSet(ctx context.Context, permSetID string) (bool, error) {
	return ps.repo.LockPermissionSet(ctx, permSetID)
}

// GetPermissionSetGrants returns all system permission grants for a single PermissionSet from SSoT.
func (ps *PermissionService) GetPermissionSetGrants(ctx context.Context, permSetID string) (map[string]bool, error) {
	return ps.repo.GetPermissionSetGrants(ctx, permSetID)
}

// GetPermissionSetGrantsMany batches grant lookups for N PermissionSet IDs in a single query.
// Used by the read-path SObject projection to avoid N+1 when rendering Permissions<X> virtual
// fields on list/query results.
func (ps *PermissionService) GetPermissionSetGrantsMany(ctx context.Context, permSetIDs []string) (map[string]map[string]bool, error) {
	return ps.repo.GetPermissionSetGrantsMany(ctx, permSetIDs)
}

// ApplyPermissionChanges is the single write path for system-permission grant
// flips. It enforces three invariants in a fixed order:
//
//  1. Catalog membership — every apiName in `changes` must name an active
//     row of the _System_Permission catalog, regardless of whether the
//     requested value is a diff or a no-op. This preserves the
//     "unknown permission rejected" contract (PS-004) for direct service
//     callers even if the HTTP write path's schema-compliance layer
//     (which only knows names the decorator emitted) is bypassed.
//  2. Diff computation — the set of apiNames whose current value differs
//     from the requested value. When the diff is empty, the request is
//     a legitimate no-op PATCH and must succeed with no writes, no audit
//     entries, and no prerequisite check: prerequisite chains are a
//     property of the intended final state, and since a no-op leaves
//     the state unchanged, any prerequisite condition already satisfied
//     (or tolerated) by `currentGrants` stays satisfied / tolerated.
//  3. Prerequisite validation on the merged final state — only when
//     the diff is non-empty. This catches diffs that would produce an
//     invalid final state while allowing no-op PATCHes to succeed even
//     when the existing grant set already violates a freshly-added
//     prerequisite chain (which would otherwise make every future
//     no-op PATCH fail until an operator cleans up the violation
//     separately).
//
// Callers must already hold the PermissionSet row lock (SELECT FOR UPDATE)
// via LockPermissionSet or an SObject-level GetLock. `currentGrants` is
// the SSoT snapshot under that lock (used for both diff and audit
// old-value). Pass nil for `user` to attribute changes to the system
// context.
func (ps *PermissionService) ApplyPermissionChanges(
	ctx context.Context,
	permSetID string,
	currentGrants map[string]bool,
	changes map[string]bool,
	user *models.UserSession,
) error {
	if len(changes) == 0 {
		return nil
	}

	// Step 1 — catalog membership. Runs on every requested apiName,
	// including no-op keys, so a PATCH like {PermissionsFoobar: false}
	// that matches the current (default false) still rejects the unknown
	// name. This is the load-bearing guarantee direct callers rely on.
	catalog, err := ps.ListSystemPermissions(ctx)
	if err != nil {
		return err
	}
	valid := make(map[string]bool, len(catalog))
	for _, p := range catalog {
		if p.IsActive {
			valid[p.APIName] = true
		}
	}
	for apiName := range changes {
		if !valid[apiName] {
			return fmt.Errorf("unknown permission: %s", apiName)
		}
	}

	// Step 2 — diff. Keys whose value already equals the current grant
	// are no-ops and do not participate in prereq validation or writes.
	diff := make(map[string]bool, len(changes))
	for apiName, enabled := range changes {
		if currentGrants[apiName] != enabled {
			diff[apiName] = enabled
		}
	}
	if len(diff) == 0 {
		return nil
	}

	// Step 3 — prerequisite validation. Only applied when the diff is
	// non-empty; validates the merged final state (current + diff) so
	// chain violations introduced by the diff are caught while pre-
	// existing violations unrelated to the diff do not block no-op
	// PATCHes. ValidatePermissionPrerequisites is order-independent
	// and re-reads the catalog for the Requires chain.
	if err := ps.ValidatePermissionPrerequisites(ctx, currentGrants, diff); err != nil {
		return err
	}

	// Step 4 — writes + audit, one row per diff entry. SetSystemPermission
	// is the repository-level upsert; it does not validate and it does
	// not mutate the cache. ApplyPermissionChanges is the only path that
	// fans out to audit, ensuring every grant flip produces exactly one
	// Setup Audit Trail entry.
	for apiName, enabled := range diff {
		if err := ps.repo.SetSystemPermission(ctx, permSetID, apiName, enabled); err != nil {
			return fmt.Errorf("set permission %s: %w", apiName, err)
		}
		if ps.setupAuditor != nil {
			userID := ""
			if user != nil {
				userID = user.ID
			}
			action := "Revoked permission"
			if enabled {
				action = "Granted permission"
			}
			ps.setupAuditor.Record(
				ctx,
				action,
				"Permission Sets",
				fmt.Sprintf("%s on permission set %s", apiName, permSetID),
				userID,
			)
		}
	}
	return nil
}

// ValidateObjectPermissionDependencies checks that an ObjectPermissions update satisfies
// Salesforce-style permission dependency requirements. Returns an error if dependencies
// are missing, otherwise returns nil.
//
// Salesforce requires:
//   - When PermissionsCreate is true, PermissionsRead must also be true
//   - When PermissionsEdit is true, PermissionsRead must also be true
//   - When PermissionsDelete is true, PermissionsRead must also be true
//   - When PermissionsModifyAllRecords is true, all dependent permissions must also be true:
//     PermissionsDelete, PermissionsEdit, PermissionsRead, PermissionsViewAllRecords
//   - When PermissionsViewAllRecords is true, PermissionsRead must also be true
func ValidateObjectPermissionDependencies(perm models.ObjectPermissions) error {
	var missingPerms []string

	if perm.PermissionsCreate && !perm.PermissionsRead {
		missingPerms = append(missingPerms, "Read "+perm.SobjectType)
	}
	if perm.PermissionsEdit && !perm.PermissionsRead {
		missingPerms = append(missingPerms, "Read "+perm.SobjectType)
	}
	if perm.PermissionsDelete && !perm.PermissionsRead {
		missingPerms = append(missingPerms, "Read "+perm.SobjectType)
	}

	// Check ModifyAllRecords dependencies - all dependent permissions must be true if ModifyAllRecords is true
	if perm.PermissionsModifyAllRecords {
		if !perm.PermissionsRead {
			missingPerms = append(missingPerms, "Read "+perm.SobjectType)
		}
		if !perm.PermissionsEdit {
			missingPerms = append(missingPerms, "Edit "+perm.SobjectType)
		}
		if !perm.PermissionsDelete {
			missingPerms = append(missingPerms, "Delete "+perm.SobjectType)
		}
		if !perm.PermissionsViewAllRecords {
			missingPerms = append(missingPerms, "View All "+perm.SobjectType)
		}
	}

	// Check ViewAllRecords dependency - Read must be true if ViewAllRecords is true
	if perm.PermissionsViewAllRecords && !perm.PermissionsRead {
		missingPerms = append(missingPerms, "Read "+perm.SobjectType)
	}

	if len(missingPerms) > 0 {
		return fmt.Errorf("FIELD_INTEGRITY_EXCEPTION: Permission %s depends on permission(s): %s",
			buildPermissionDescription(perm),
			strings.Join(missingPerms, "; "))
	}

	return nil
}

// ValidateFieldPermissionDependencies checks the one Salesforce-style field
// permission dependency: edit access is never meaningful without read access.
// Callers must pass the final image that will be persisted, not the original
// request, because Records before-save lifecycle may rewrite either value.
func ValidateFieldPermissionDependencies(perm models.FieldPermissions) error {
	if !fieldpermissions.AccessCoherent(perm.PermissionsRead, perm.PermissionsEdit) {
		return fmt.Errorf("FIELD_INTEGRITY_EXCEPTION: Edit %s requires Read %s", perm.Field, perm.Field)
	}
	return nil
}

// buildPermissionDescription creates a descriptive name for the permission being set
func buildPermissionDescription(perm models.ObjectPermissions) string {
	if perm.PermissionsModifyAllRecords {
		return "Modify All " + perm.SobjectType
	}
	if perm.PermissionsViewAllRecords {
		return "View All " + perm.SobjectType
	}
	if perm.PermissionsDelete {
		return "Delete " + perm.SobjectType
	}
	if perm.PermissionsEdit {
		return "Edit " + perm.SobjectType
	}
	if perm.PermissionsCreate {
		return "Create " + perm.SobjectType
	}
	if perm.PermissionsRead {
		return "Read " + perm.SobjectType
	}
	return "Permission for " + perm.SobjectType
}

// splitRequiredReadPermission splits a comma-separated RequiredReadPermission value,
// trimming whitespace from each token and dropping empty tokens. This tolerates
// values such as "ViewAllData, ModifyAllData" (space after comma) and
// "ViewAllData," (trailing comma) that would otherwise produce non-matching tokens.
func splitRequiredReadPermission(raw string) []string {
	parts := strings.Split(raw, ",")
	result := make([]string, 0, len(parts))
	for _, p := range parts {
		if t := strings.TrimSpace(p); t != "" {
			result = append(result, t)
		}
	}
	return result
}

// --- Record Type Access Grants ---

// ListRecordTypeCatalog returns the active record types available for
// permission-set record type access authoring.
func (ps *PermissionService) ListRecordTypeCatalog(ctx context.Context) ([]ports.RecordTypeCatalogEntry, error) {
	return ps.repo.ListRecordTypeCatalog(ctx)
}

// GetPermissionSetRecordTypeGrants returns record type access grants for a
// PermissionSet.
func (ps *PermissionService) GetPermissionSetRecordTypeGrants(ctx context.Context, permSetID string) ([]models.PermissionSetRecordTypeVisibility, error) {
	return ps.repo.GetPermissionSetRecordTypeGrants(ctx, permSetID)
}

// SavePermissionSetRecordTypeGrants updates record type visibility grants for
// a PermissionSet. Permission sets do not own record type defaults. Must be
// called inside a transaction after LockPermissionSet.
func (ps *PermissionService) SavePermissionSetRecordTypeGrants(ctx context.Context, permSetID string, grants map[string]RecordTypeGrantChange) error {
	recordTypes, err := ps.repo.ListRecordTypeCatalog(ctx)
	if err != nil {
		return err
	}
	validRecordTypes := make(map[string]ports.RecordTypeCatalogEntry, len(recordTypes))
	for _, rt := range recordTypes {
		validRecordTypes[rt.RecordTypeID] = rt
	}

	for recordTypeID := range grants {
		if _, ok := validRecordTypes[recordTypeID]; !ok {
			return errors.NewValidationError("", fmt.Sprintf("unknown record type: %s", recordTypeID))
		}
	}

	for recordTypeID, grant := range grants {
		if err := ps.repo.SetPermissionSetRecordTypeGrant(ctx, permSetID, recordTypeID, grant.IsVisible); err != nil {
			return err
		}
	}
	if ps.setupAuditor != nil {
		ps.setupAuditor.Record(ctx, "Updated record type access", "Permission Sets",
			fmt.Sprintf("record type grants on permission set %s", permSetID), "")
	}
	return nil
}

// --- Custom Permission Grants ---

// GetPermissionSetCustomPermGrants returns custom permission grants for a PermissionSet.
func (ps *PermissionService) GetPermissionSetCustomPermGrants(ctx context.Context, permSetID string) ([]models.PermissionSetCustomPermission, error) {
	return ps.repo.GetPermissionSetCustomPermGrants(ctx, permSetID)
}

// SavePermissionSetCustomPermGrants updates custom permission grants for a PermissionSet.
// Must be called inside a transaction after LockPermissionSet.
// granted=true means insert the junction row; granted=false means delete it.
func (ps *PermissionService) SavePermissionSetCustomPermGrants(ctx context.Context, permSetID string, grants map[string]bool) error {
	// Validate custom permission IDs against catalog.
	perms, err := ps.repo.ListCustomPermissions(ctx)
	if err != nil {
		return err
	}
	validPerms := make(map[string]struct{}, len(perms))
	for _, p := range perms {
		validPerms[p.ID] = struct{}{}
	}
	for customPermID := range grants {
		if _, ok := validPerms[customPermID]; !ok {
			return errors.NewValidationError("", fmt.Sprintf("unknown custom permission: %s", customPermID))
		}
	}
	for customPermID, granted := range grants {
		var err error
		if granted {
			err = ps.repo.SetPermissionSetCustomPermGrant(ctx, permSetID, customPermID)
		} else {
			err = ps.repo.DeletePermissionSetCustomPermGrant(ctx, permSetID, customPermID)
		}
		if err != nil {
			return err
		}
	}
	if ps.setupAuditor != nil {
		ps.setupAuditor.Record(ctx, "Updated custom permissions", "Permission Sets",
			fmt.Sprintf("custom perm grants on permission set %s", permSetID), "")
	}
	return nil
}
