package records

import (
	"context"
	"log"
	"reflect"
	"strings"

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func normalizeObjectSecurityContract(objectName string, record models.SObject) {
	if !strings.EqualFold(objectName, constants.TableSharingRule) {
		return
	}

	normalizeSharingRuleCriteriaField(record)
	normalizeOptionalStringField(record, constants.FieldSharingRule_SharedFromRoleID)
	normalizeOptionalStringField(record, constants.FieldSharingRule_SharedFromGroupID)
	normalizeOptionalStringField(record, constants.FieldSharingRule_ShareWithRoleID)
	normalizeOptionalStringField(record, constants.FieldSharingRule_ShareWithGroupID)
}

func materializeObjectSecurityContractClears(objectName string, record, oldRecord, effectiveUpdates models.SObject) {
	if !strings.EqualFold(objectName, constants.TableSharingRule) {
		return
	}

	oldContract := cloneSObject(oldRecord)
	normalizeObjectSecurityContract(objectName, oldContract)
	for _, field := range sharingRuleOptionalContractFields() {
		if _, exists := record[field]; exists {
			continue
		}
		if !hasOptionalString(oldContract, field) {
			continue
		}
		record[field] = nil
		effectiveUpdates[field] = nil
	}
}

func sharingRuleOptionalContractFields() []string {
	return []string{
		constants.FieldSharingRule_Criteria,
		constants.FieldSharingRule_SharedFromRoleID,
		constants.FieldSharingRule_SharedFromGroupID,
		constants.FieldSharingRule_ShareWithRoleID,
		constants.FieldSharingRule_ShareWithGroupID,
	}
}

func validateObjectSecurityContract(objectName string, record models.SObject) error {
	if strings.EqualFold(objectName, constants.TableObjectPermissions) {
		perm := models.ObjectPermissions{
			ParentID:                  stringField(record, constants.FieldObjectPermissions_ParentID),
			SobjectType:               stringField(record, constants.FieldObjectPermissions_SobjectType),
			PermissionsRead:           boolField(record, constants.FieldObjectPermissions_PermissionsRead),
			PermissionsCreate:         boolField(record, constants.FieldObjectPermissions_PermissionsCreate),
			PermissionsEdit:           boolField(record, constants.FieldObjectPermissions_PermissionsEdit),
			PermissionsDelete:         boolField(record, constants.FieldObjectPermissions_PermissionsDelete),
			PermissionsViewAllRecords: boolField(record, constants.FieldObjectPermissions_PermissionsViewAllRecords),
			PermissionsModifyAllRecords: boolField(
				record,
				constants.FieldObjectPermissions_PermissionsModifyAllRecords,
			),
		}
		if err := permissions.ValidateObjectPermissionDependencies(perm); err != nil {
			return appErrors.NewValidationError("", err.Error())
		}
		return nil
	}
	if strings.EqualFold(objectName, constants.TableFieldPermissions) {
		perm := models.FieldPermissions{
			ParentID:        stringField(record, constants.FieldFieldPermissions_ParentID),
			SobjectType:     stringField(record, constants.FieldFieldPermissions_SobjectType),
			Field:           stringField(record, constants.FieldFieldPermissions_Field),
			PermissionsRead: boolField(record, constants.FieldFieldPermissions_PermissionsRead),
			PermissionsEdit: boolField(record, constants.FieldFieldPermissions_PermissionsEdit),
		}
		if err := permissions.ValidateFieldPermissionDependencies(perm); err != nil {
			return appErrors.NewValidationError(constants.FieldFieldPermissions_PermissionsEdit, err.Error())
		}
		return nil
	}

	return nil
}

func validateSharingRuleShapeContract(record models.SObject) error {
	hasCriteria := hasOptionalString(record, constants.FieldSharingRule_Criteria)
	hasSharedFromRole := hasOptionalString(record, constants.FieldSharingRule_SharedFromRoleID)
	hasSharedFromGroup := hasOptionalString(record, constants.FieldSharingRule_SharedFromGroupID)
	hasShareWithRole := hasOptionalString(record, constants.FieldSharingRule_ShareWithRoleID)
	hasShareWithGroup := hasOptionalString(record, constants.FieldSharingRule_ShareWithGroupID)

	// `==` here intentionally rejects both the both-empty (false == false)
	// and both-set (true == true) cases — exactly one ShareWith field must
	// be populated. A future refactor that changes this to `&&` would silently
	// flip the both-empty case to acceptance; security_contracts_test.go pins
	// both branches.
	if hasShareWithRole == hasShareWithGroup {
		return appErrors.NewValidationError("ShareWith", "sharing rule must target exactly one role or group")
	}

	if hasCriteria {
		if hasSharedFromRole || hasSharedFromGroup {
			return appErrors.NewValidationError("Criteria", "criteria-based sharing rules cannot define SharedFromRoleId or SharedFromGroupId")
		}
		return nil
	}

	// Same `==` symmetry as the ShareWith guard above: rejects both-empty
	// (no source specified) and both-set (ambiguous source) for owner-based
	// rules. security_contracts_test.go pins both branches.
	if hasSharedFromRole == hasSharedFromGroup {
		return appErrors.NewValidationError("SharedFrom", "owner-based sharing rules must define exactly one SharedFromRoleId or SharedFromGroupId")
	}

	return nil
}

type securityRecordChange struct {
	before models.SObject
	after  models.SObject
}

type securityInvalidationPlan struct {
	invalidatePermissions bool
	invalidateSharing     bool
	refreshRoleHierarchy  bool
	userIDs               map[string]struct{}
}

func newSecurityInvalidationPlan() *securityInvalidationPlan {
	return &securityInvalidationPlan{
		userIDs: make(map[string]struct{}),
	}
}

func (ps *PersistenceService) invalidateSecurityCaches(ctx context.Context, objectName string, before models.SObject, after models.SObject) {
	ps.invalidateSecurityCachesForChanges(ctx, objectName, []securityRecordChange{{
		before: before,
		after:  after,
	}})
}

func (ps *PersistenceService) invalidateSecurityCachesForRecords(ctx context.Context, objectName string, records []models.SObject) {
	if len(records) == 0 {
		return
	}

	changes := make([]securityRecordChange, 0, len(records))
	for _, record := range records {
		changes = append(changes, securityRecordChange{after: record})
	}
	ps.invalidateSecurityCachesForChanges(ctx, objectName, changes)
}

func (ps *PersistenceService) invalidateSecurityCachesForChanges(ctx context.Context, objectName string, changes []securityRecordChange) {
	if isCreateFromSourceGuardMetadataObject(objectName) {
		ps.clearCreateFromSourceGuardCache()
	}
	if strings.EqualFold(objectName, constants.TableObjectFieldMapping) {
		ps.clearInsertInheritanceCache()
	}
	if strings.EqualFold(objectName, constants.TableOAuthCustomScope) && ps.oauthScopePolicyInvalidator != nil {
		ps.invalidateOAuthScopePolicy(ctx)
	}

	if ps.permissions == nil {
		return
	}

	plan, err := ps.collectSecurityInvalidationPlan(ctx, objectName, changes)
	if err != nil {
		if !securityCacheInvalidationFallbackEligible(err) {
			log.Printf("Warning: failed to resolve record cache invalidation plan for %s after commit: %v", objectName, err)
			return
		}
		log.Printf("Warning: failed to resolve security invalidation plan for %s: %v; falling back to global security cache flush", objectName, err)
		ps.permissions.InvalidateCache()
		ps.permissions.InvalidateSharingRuleCache()
		ps.permissions.RefreshRoleHierarchy(ctx)
		return
	}

	ps.applySecurityInvalidationPlan(ctx, plan)
}

func (ps *PersistenceService) invalidateOAuthScopePolicy(ctx context.Context) {
	if ps == nil || ps.oauthScopePolicyInvalidator == nil {
		return
	}
	if ps.txManager != nil && ps.txManager.IsTransactionActive(ctx) {
		if ports.RegisterAfterCommitHook(ctx, ps.oauthScopePolicyInvalidator) {
			return
		}
		// Unknown transaction managers cannot provide a safe post-commit point.
		// Avoid firing before commit, where a reload could observe old rows and
		// incorrectly clear the registry dirty bit.
		return
	}
	ps.oauthScopePolicyInvalidator()
}

func (ps *PersistenceService) invalidateConnectedAppCache(ctx context.Context, appID string) {
	if ps == nil || ps.connectedAppHook == nil || strings.TrimSpace(appID) == "" {
		return
	}
	invalidate := func() {
		ps.connectedAppHook.OnConnectedAppChanged(appID)
	}
	if ps.txManager != nil && ps.txManager.IsTransactionActive(ctx) {
		if ports.RegisterAfterCommitHook(ctx, invalidate) {
			return
		}
	}
	invalidate()
}

func (ps *PersistenceService) collectSecurityInvalidationPlan(ctx context.Context, objectName string, changes []securityRecordChange) (*securityInvalidationPlan, error) {
	plan := newSecurityInvalidationPlan()
	if err := ps.runRecordCacheInvalidations(ctx, recordCacheInvalidationEvent{
		ObjectName: objectName,
		Changes:    changes,
	}, plan); err != nil {
		return nil, err
	}
	return plan, nil
}

func (ps *PersistenceService) applySecurityInvalidationPlan(ctx context.Context, plan *securityInvalidationPlan) {
	if plan == nil {
		return
	}

	if plan.invalidatePermissions {
		ps.permissions.InvalidateCache()
	}

	if plan.invalidateSharing {
		ps.permissions.InvalidateSharingRuleCache()
	}

	if plan.refreshRoleHierarchy {
		ps.permissions.RefreshRoleHierarchy(ctx)
	}

	for userID := range plan.userIDs {
		ps.permissions.InvalidateUserCache(userID)
	}
}

func (ps *PersistenceService) addAssignedUsersForGroups(ctx context.Context, affectedUsers map[string]struct{}, groupIDs ...string) error {
	seenGroups := make(map[string]struct{})
	for _, groupID := range groupIDs {
		if groupID == "" {
			continue
		}
		if _, seen := seenGroups[groupID]; seen {
			continue
		}
		seenGroups[groupID] = struct{}{}

		assignmentRefs, err := ps.repo.GetChildren(ctx, constants.TablePermissionSetGroupAssignment, constants.FieldPermissionSetGroupAssignment_GroupID, groupID)
		if err != nil {
			return err
		}

		assignmentIDs := make([]string, 0, len(assignmentRefs))
		for _, ref := range assignmentRefs {
			if assignmentID, ok := ref[constants.FieldID].(string); ok && assignmentID != "" {
				assignmentIDs = append(assignmentIDs, assignmentID)
			}
		}
		if len(assignmentIDs) == 0 {
			continue
		}

		assignments, err := ps.repo.FindMany(ctx, constants.TablePermissionSetGroupAssignment, assignmentIDs)
		if err != nil {
			return err
		}
		for _, assignment := range assignments {
			addUserID(affectedUsers, stringField(assignment, constants.FieldPermissionSetGroupAssignment_AssigneeID))
		}
	}

	return nil
}

func (ps *PersistenceService) groupIDsForMutingPermissionSetIDs(ctx context.Context, mutingPermissionSetIDs []string) ([]string, error) {
	groupIDs := make([]string, 0, len(mutingPermissionSetIDs))
	seen := make(map[string]struct{})
	for _, mutingPermissionSetID := range mutingPermissionSetIDs {
		if mutingPermissionSetID == "" {
			continue
		}
		if _, ok := seen[mutingPermissionSetID]; ok {
			continue
		}
		seen[mutingPermissionSetID] = struct{}{}

		mutingPermSet, err := ps.cachedFindOne(ctx, constants.TableMutingPermissionSet, mutingPermissionSetID)
		if err != nil {
			return nil, err
		}
		if mutingPermSet == nil {
			continue
		}
		groupID := stringField(mutingPermSet, constants.FieldMutingPermissionSet_GroupID)
		if groupID != "" {
			groupIDs = append(groupIDs, groupID)
		}
	}

	return groupIDs, nil
}

func collectChangedStringFields(changes []securityRecordChange, field string) []string {
	values := make([]string, 0, len(changes)*2)
	seen := make(map[string]struct{})
	for _, change := range changes {
		for _, value := range []string{
			stringField(change.before, field),
			stringField(change.after, field),
		} {
			if value == "" {
				continue
			}
			if _, ok := seen[value]; ok {
				continue
			}
			seen[value] = struct{}{}
			values = append(values, value)
		}
	}

	return values
}

func securityRecordID(before models.SObject, after models.SObject) string {
	if id := stringField(after, constants.FieldID); id != "" {
		return id
	}
	return stringField(before, constants.FieldID)
}

func fieldChanged(before models.SObject, after models.SObject, field string) bool {
	beforeVal, beforeOK := fieldValue(before, field)
	afterVal, afterOK := fieldValue(after, field)
	if !beforeOK && !afterOK {
		return false
	}
	return !reflect.DeepEqual(beforeVal, afterVal)
}

func fieldValue(record models.SObject, field string) (interface{}, bool) {
	if record == nil {
		return nil, false
	}
	value, ok := record[field]
	return value, ok
}

func stringField(record models.SObject, field string) string {
	if record == nil {
		return ""
	}
	value, ok := record[field]
	if !ok || value == nil {
		return ""
	}
	str, ok := value.(string)
	if !ok {
		return ""
	}
	return str
}

func boolField(record models.SObject, field string) bool {
	if record == nil {
		return false
	}
	value, ok := record[field]
	if !ok || value == nil {
		return false
	}
	flag, ok := value.(bool)
	if !ok {
		return false
	}
	return flag
}

func addUserID(userIDs map[string]struct{}, userID string) {
	if userID == "" {
		return
	}
	userIDs[userID] = struct{}{}
}

func normalizeOptionalStringField(record models.SObject, field string) {
	value, exists := record[field]
	if !exists {
		return
	}

	str, ok := value.(string)
	if !ok {
		return
	}

	trimmed := strings.TrimSpace(str)
	if trimmed == "" {
		record[field] = nil
		return
	}

	record[field] = trimmed
}

func normalizeSharingRuleCriteriaField(record models.SObject) {
	value, exists := record[constants.FieldSharingRule_Criteria]
	if !exists {
		return
	}

	if value == nil {
		record[constants.FieldSharingRule_Criteria] = ""
		return
	}

	str, ok := value.(string)
	if !ok {
		return
	}

	trimmed := strings.TrimSpace(str)
	if trimmed == "" || trimmed == "[]" {
		record[constants.FieldSharingRule_Criteria] = ""
		return
	}

	record[constants.FieldSharingRule_Criteria] = trimmed
}

func hasOptionalString(record models.SObject, field string) bool {
	value, exists := record[field]
	if !exists || value == nil {
		return false
	}

	str, ok := value.(string)
	return ok && strings.TrimSpace(str) != ""
}
