package permissions

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

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/formula"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// 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
	GetSharingRules(ctx context.Context, objectAPIName string) []*models.SharingRule
}

// ObjectFieldPermCache is the interface for the in-memory permission cache.
// PermissionService uses this (when set) to avoid DB queries for object/field
// permission checks that have already been bulk-loaded.
type ObjectFieldPermCache interface {
	GetObjectPermission(userID, profileID, objectAPIName string) (*models.ObjectPermissions, bool)
	GetFieldPermission(userID, profileID, objectAPIName, fieldAPIName string) (*models.FieldPermissions, bool)
	HasObjectPermsLoaded(userID, profileID string) bool
	HasFieldPermsLoaded(userID, profileID string) bool
	// 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.
//
// When an ObjectFieldPermCache is attached (via SetCache), object-level and
// field-level permission checks are served from the in-memory cache if available,
// falling back to the database on cache miss. System/custom permissions continue
// to be cached by the middleware PermissionLoader.
//
// Permission evaluation order:
//  1. SuperUser check (system_admin profile bypasses all checks)
//  2. Object-level permissions from _System_ObjectPerms
//  3. Field-level permissions from _System_FieldPerms
//  4. 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 in-memory cache for object/field permissions (set via SetCache)
	cache ObjectFieldPermCache

	// 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
}

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

// 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)
	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, even by SuperUser (H1 fix)
	for _, p := range permissions {
		if p == blockedPermission {
			return false
		}
	}

	if user.IsSystemAdmin || constants.IsSuperUser(user.ProfileID) {
		return true
	}

	effectivePerms, err := ps.GetEffectiveSystemPermissions(ctx, user.ID, user.ProfileID)
	if err != nil {
		return false
	}

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

	return false
}

// NewPermissionService creates a new PermissionService
func NewPermissionService(
	repo ports.PermissionRepository,
	metadata metadataProvider,
	userRepo ports.UserRepository,
) *PermissionService {
	ps := &PermissionService{
		metadata:           metadata,
		repo:               repo,
		userRepo:           userRepo,
		formula:            formula.NewEngine(),
		roleHierarchyCache: make(map[string]*string),
		sharingRuleCache:   make(map[string]*cachedSharingRules),
	}
	// Pre-load role hierarchy
	ps.refreshRoleHierarchy(context.Background())
	return ps
}

// SetCache attaches an in-memory permission cache. When set, object-level and
// field-level permission checks are served from cache when available.
func (ps *PermissionService) SetCache(cache ObjectFieldPermCache) {
	ps.cache = cache
}

// 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.cache != nil {
		ps.cache.InvalidateUser(userID)
	}
}

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

// --- Object Permission Queries ---

// loadEffectiveObjectPermission loads permissions considering Profile AND Permission Sets.
// Checks the in-memory cache first if available.
func (ps *PermissionService) loadEffectiveObjectPermission(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
	if ps.cache != nil && ps.cache.HasObjectPermsLoaded(user.ID, user.ProfileID) {
		perm, _ := ps.cache.GetObjectPermission(user.ID, user.ProfileID, objectAPIName)
		return perm, nil
	}
	return ps.repo.LoadEffectiveObjectPermission(ctx, user, objectAPIName)
}

// loadEffectiveFieldPermission loads field permissions considering Profile AND Permission Sets.
// Checks the in-memory cache first if available.
func (ps *PermissionService) loadEffectiveFieldPermission(ctx context.Context, user *models.UserSession, objectAPIName, fieldAPIName string) (*models.FieldPermissions, error) {
	if ps.cache != nil && ps.cache.HasFieldPermsLoaded(user.ID, user.ProfileID) {
		perm, _ := ps.cache.GetFieldPermission(user.ID, user.ProfileID, objectAPIName, fieldAPIName)
		return perm, nil
	}
	return ps.repo.LoadEffectiveFieldPermission(ctx, user, objectAPIName, fieldAPIName)
}

// --- 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) CheckObjectPermissionOnRecord(ctx context.Context, schema *models.ObjectMetadata, record models.SObject, operation string, user *models.UserSession) bool {
	if schema == nil {
		return false
	}
	if user == nil {
		return false
	}
	if !schema.UsesDerivedAccessPolicy() || !ps.accessPolicyRequiresRecordContext(schema, operation) {
		return ps.CheckObjectPermissionWithUser(ctx, schema.APIName, operation, user)
	}
	allowed, err := ps.evaluateObjectAccessPolicy(ctx, schema, record, operation, user, newAccessPolicyEvalState())
	if err != nil {
		return false
	}
	return allowed
}

func (ps *PermissionService) checkObjectPermissionWithState(ctx context.Context, objectAPIName string, 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)
	}

	// BLOCKED tables: security infrastructure objects (Session, OAuthCode, etc.)
	// are never accessible via generic CRUD, even by SuperUser.
	// This check MUST run before the SuperUser bypass.
	requiredSystemPerms := ps.requiredSystemPermissionsForSetupObject(ctx, objectAPIName, operation)
	for _, p := range requiredSystemPerms {
		if p == blockedPermission {
			return false
		}
	}

	// BLOCKED read: RequiredReadPermission=BLOCKED also blocks SuperUser from
	// generic query paths. Admins access these via dedicated management APIs.
	var schema *models.ObjectMetadata
	if ps.metadata != nil {
		schema = ps.metadata.GetSchema(ctx, objectAPIName)
	}
	if operation == constants.PermRead && schema != nil {
		if schema.RequiredReadPermission == "BLOCKED" {
			return false
		}
	}

	// SuperUser bypass - system_admin or IsSystemAdmin flag has full access
	if user.IsSystemAdmin || constants.IsSuperUser(user.ProfileID) {
		return true
	}

	// Defense in depth for setup/security objects: direct CRUD and query access
	// must also satisfy the matching system permission gate.
	if len(requiredSystemPerms) > 0 && !ps.hasAnySystemPermission(ctx, user, requiredSystemPerms) {
		return false
	}

	// 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).
	if operation == constants.PermRead && schema != nil {
		if strings.HasPrefix(string(schema.TableType), "system") {
			if schema.RequiredReadPermission != "" {
				if schema.RequiredReadPermission == "BLOCKED" {
					return false
				}
				return ps.hasAnySystemPermission(ctx, user, []string{schema.RequiredReadPermission})
			}
			return true
		}
	}

	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 "ViewAllRecords":
		return perm.PermissionsViewAllRecords
	case "ModifyAllRecords":
		return perm.PermissionsModifyAllRecords
	default:
		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).
func (ps *PermissionService) CheckSystemPermission(ctx context.Context, user *models.UserSession, permission string) bool {
	if ps == nil || user == nil {
		return false
	}
	if user.IsSystemAdmin || constants.IsSuperUser(user.ProfileID) {
		return true
	}
	if ps.repo == nil {
		return false
	}

	effectivePerms, err := ps.GetEffectiveSystemPermissions(ctx, user.ID, user.ProfileID)
	if err != nil {
		return false
	}
	return effectivePerms[permission]
}

// 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
}

// IsSecuritySensitiveField returns true for fields containing secrets/credentials
// that must never be exposed via generic query paths regardless of SecurityContext.
func IsSecuritySensitiveField(objectAPIName, fieldAPIName string) bool {
	return isSecuritySensitiveField(objectAPIName, fieldAPIName)
}

// 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 {
	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 {
	// System fields are never editable
	if IsFieldSystemReadOnly(ctx, ps.metadata, objectAPIName, fieldAPIName) {
		return false
	}

	// No user = no access
	if user == nil {
		return false
	}

	// SuperUser bypass
	if user.IsSystemAdmin || constants.IsSuperUser(user.ProfileID) {
		return true
	}

	if ps.metadata != nil {
		if schema := ps.metadata.GetSchema(ctx, objectAPIName); schema != nil && schema.AccessPolicy != nil && schema.UsesDerivedAccessPolicy() {
			allowed, err := ps.evaluateObjectAccessPolicy(ctx, schema, record, "fieldEdit", user, newAccessPolicyEvalState())
			if err != nil {
				return false
			}
			return allowed
		}
	}

	// Check field-level permission (Effective)
	perm, err := ps.loadEffectiveFieldPermission(ctx, user, objectAPIName, fieldAPIName)
	if err != nil {
		return false
	}

	if perm != nil {
		return perm.PermissionsEdit
	}

	// Fallback to object permission
	return ps.CheckObjectPermissionWithUser(ctx, objectAPIName, constants.PermEdit, user)
}

// 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 {
	if user == nil {
		return false
	}

	// SuperUser bypass
	if user.IsSystemAdmin || constants.IsSuperUser(user.ProfileID) {
		return true
	}

	// Sensitive fields are admin-only — MUST be checked before explicit FLS.
	// Bootstrap seeds PermissionsRead=true on all system table fields, so explicit
	// FLS would allow these. This hard deny overrides any grant.
	if isSecuritySensitiveField(objectAPIName, fieldAPIName) {
		return false
	}

	if ps.metadata != nil {
		if schema := ps.metadata.GetSchema(ctx, objectAPIName); schema != nil && schema.AccessPolicy != nil && schema.UsesDerivedAccessPolicy() {
			allowed, err := ps.evaluateObjectAccessPolicy(ctx, schema, nil, "fieldRead", user, newAccessPolicyEvalState())
			if err != nil {
				return false
			}
			return allowed
		}
	}

	// Check field-level permission (Effective)
	perm, err := ps.loadEffectiveFieldPermission(ctx, user, objectAPIName, fieldAPIName)
	if err != nil {
		return false
	}

	// If explicit field permission exists, use it
	if perm != nil {
		return perm.PermissionsRead
	}

	// Fallback to object permission
	return ps.CheckObjectPermissionWithUser(ctx, objectAPIName, constants.PermRead, user)
}

// isSecuritySensitiveField returns true for fields that contain secrets/credentials
// and must only be visible to SuperUser/admin (checked before this function is called).
func isSecuritySensitiveField(objectAPIName, fieldAPIName string) bool {
	// Sensitive fields by object — these contain authentication credentials,
	// encryption keys, or configuration that would enable privilege escalation.
	switch objectAPIName {
	case "_System_ConnectedApp":
		return fieldAPIName == "ConsumerSecret"
	case "_System_Session":
		return fieldAPIName == "Token"
	case "_System_OAuthCode":
		return fieldAPIName == "Code"
	case "_System_Webhook":
		return fieldAPIName == "AuthConfig" || fieldAPIName == "SecretKey"
	case "_System_Config":
		return fieldAPIName == "Value"
	case "User":
		return fieldAPIName == "Password"
	}
	return false
}

// 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 {
	if schema == nil {
		return 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))

	// Super users see all fields
	if user != nil && (user.IsSystemAdmin || constants.IsSuperUser(user.ProfileID)) {
		return schema
	}

	for _, field := range schema.Fields {
		if ps.CheckFieldVisibilityWithUser(ctx, schema.APIName, field.APIName, user) {
			effectiveSchema.Fields = append(effectiveSchema.Fields, field)
		}
	}

	return &effectiveSchema
}

// 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)
}

// 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 := validatePermissionDependencies(perm); err != nil {
		return errors.NewValidationError("", err.Error())
	}

	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 := validatePermissionDependencies(perm); err != nil {
		return errors.NewValidationError("", err.Error())
	}

	// 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 := 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)
}

// BatchUpsertObjectPermissions batch-upserts object permissions via multi-value INSERT
func (ps *PermissionService) BatchUpsertObjectPermissions(ctx context.Context, perms []models.ObjectPermissions) error {
	if err := ps.repo.BatchUpsertObjectPermissions(ctx, perms); err != nil {
		return err
	}
	ps.InvalidateCache()
	if ps.setupAuditor != nil && len(perms) > 0 {
		ps.setupAuditor.Record(ctx, "Changed object permissions", "Permissions", "Batch updated object permissions", "")
	}
	return nil
}

// BatchUpsertFieldPermissions batch-upserts field permissions via multi-value INSERT
func (ps *PermissionService) BatchUpsertFieldPermissions(ctx context.Context, perms []models.FieldPermissions) error {
	if err := ps.repo.BatchUpsertFieldPermissions(ctx, perms); err != nil {
		return err
	}
	ps.InvalidateCache()
	if ps.setupAuditor != nil && len(perms) > 0 {
		ps.setupAuditor.Record(ctx, "Changed field permissions", "Permissions", "Batch updated 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
}

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

// CleanupDirectProfilePermissions removes old bootstrap data that used ProfileId directly as ParentId.
// This is a best-effort cleanup — errors are logged but not propagated.
func (ps *PermissionService) CleanupDirectProfilePermissions(ctx context.Context, profileIDs []string) {
	if err := ps.repo.CleanupDirectProfilePermissions(ctx, profileIDs); err != nil {
		log.Printf("⚠️  CleanupDirectProfilePermissions: %v", err)
	}
}

// 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 grants default permissions for a new object to all profiles
func (ps *PermissionService) GrantInitialPermissions(ctx context.Context, objectAPIName string) error {
	profiles, err := ps.repo.GetAllProfiles(ctx)
	if err != nil {
		return errors.NewInternalError("failed to fetch profiles", err)
	}

	var perms []models.ObjectPermissions
	for _, profile := range profiles {
		psId, err := ps.EnsureProfilePermissionSet(ctx, profile.ID, profile.Name)
		if err != nil {
			return errors.NewInternalError("failed to ensure profile permission set for "+profile.ID, err)
		}

		isSystemAdmin := profile.ID == constants.ProfileSystemAdmin
		perm := models.ObjectPermissions{
			ParentID:                    psId,
			SobjectType:                 objectAPIName,
			PermissionsRead:             true,
			PermissionsCreate:           true,
			PermissionsEdit:             true,
			PermissionsDelete:           true,
			PermissionsViewAllRecords:   isSystemAdmin,
			PermissionsModifyAllRecords: isSystemAdmin,
		}
		perms = append(perms, perm)
	}

	return ps.repo.BatchUpsertObjectPermissions(ctx, perms)
}

// 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) {
	return ps.repo.GetEffectiveSystemPermissions(ctx, userID, profileID)
}

// GetEffectiveCustomPermissions returns all custom permissions for a user
func (ps *PermissionService) GetEffectiveCustomPermissions(ctx context.Context, userID, profileID string) (map[string]bool, error) {
	return ps.repo.GetEffectiveCustomPermissions(ctx, userID, profileID)
}

// 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
}

// validatePermissionDependencies checks that an ObjectPermissions update satisfies
// Salesforce-style permission dependency requirements. Returns an error if dependencies
// are missing, otherwise returns nil.
//
// Salesforce requires:
//   - 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 validatePermissionDependencies(perm models.ObjectPermissions) error {
	var missingPerms []string

	// 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.PermissionsCreate {
			missingPerms = append(missingPerms, "Create "+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
}

// 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
	}
	// For other cases, return a generic descriptor
	return "Permission for " + perm.SobjectType
}

// ListEffectiveObjectPermissionsForUser retrieves all effective object permissions for a user (Profile + Permission Sets)
func (ps *PermissionService) ListEffectiveObjectPermissionsForUser(ctx context.Context, userID, profileID string) ([]models.ObjectPermissions, error) {
	return ps.repo.ListEffectiveObjectPermissionsForUser(ctx, userID, profileID)
}

// ListEffectiveFieldPermissionsForUser retrieves all effective field permissions for a user (Profile + Permission Sets)
func (ps *PermissionService) ListEffectiveFieldPermissionsForUser(ctx context.Context, userID, profileID string) ([]models.FieldPermissions, error) {
	return ps.repo.ListEffectiveFieldPermissionsForUser(ctx, userID, profileID)
}
