package records

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"github.com/nexuscrm/kernel/pkg/apierr"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/recordlocks"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/kernel/pkg/utils"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

const profileOwnedPermissionSetAssignmentMessage = "You can't assign or unassign this permission set because it's associated with a profile."
const inactivePermissionSetAssignmentMessage = "You can't assign an inactive or deleted permission set."
const systemProfileOwnedPermissionResourceLockedMessage = "System-profile-owned permission resources are immutable via the standard sobjects write surface."
const groupUserRoleRequiredMessage = "role-backed groups must define UserRoleId"
const groupUserRoleForbiddenMessage = "UserRoleId is only allowed on role-backed groups"
const roleBackedGroupManualMembersMessage = "role-backed groups cannot retain direct GroupMember members"
const roleBackedGroupMemberMutationMessage = "role-backed groups do not allow direct GroupMember membership changes"
const userRoleDeleteChildRoleMessage = "UserRole cannot be deleted while child roles reference it; reassign child roles first"
const userRoleDeleteAssignedUsersMessage = "UserRole cannot be deleted while users are assigned to it; reassign users first"
const userDeleteLifecycleMessage = "User cannot be deleted through the generic delete path; deactivate the user by setting IsActive=false"

func authoritativePermissionLockReadContext(ctx context.Context) context.Context {
	if constants.GetSecurityContext(ctx) >= constants.SystemContext {
		return ctx
	}
	return constants.WithSecurityContext(ctx, constants.SystemContext, "system_profile_permission_resource_lock_read")
}

func (s *PersistenceService) validatePermissionSetAssignmentTarget(ctx context.Context, objectName string, data models.SObject, requireAssignableTarget bool) error {
	if !strings.EqualFold(objectName, constants.TablePermissionSetAssignment) {
		return nil
	}
	permissionSetID, _ := data[constants.FieldPermissionSetAssignment_PermissionSetID].(string)
	if permissionSetID == "" {
		return nil
	}

	key := recordlocks.ParentKey{ObjectName: constants.TablePermissionSet, RecordID: permissionSetID}
	if !constants.HasUpdatePreviewPreparation(ctx) {
		scope := recordlocks.ScopeFromContext(ctx)
		if scope == nil {
			return appErrors.NewInternalError(
				"Permission Set assignment validation requires parent-lock coordination",
				recordlocks.ErrMissingParentLockScope,
			)
		}
		if !scope.HasAcquired(key) {
			return &parentLockPrelockRetryError{
				keys:   []recordlocks.ParentKey{key},
				reason: "Permission Set assignment target",
			}
		}
	}

	permissionSet, err := s.repo.FindAny(
		persistence.WithPhysicalReadOnly(authoritativePermissionLockReadContext(ctx)),
		constants.TablePermissionSet,
		permissionSetID,
	)
	if err != nil {
		return err
	}

	if isGuardFieldTruthy(permissionSet[constants.FieldPermissionSet_IsOwnedByProfile]) {
		return appErrors.NewInvalidReferenceError(profileOwnedPermissionSetAssignmentMessage)
	}
	if requireAssignableTarget && (permissionSet == nil ||
		!isGuardFieldTruthy(permissionSet[constants.FieldPermissionSet_IsActive]) ||
		isGuardFieldTruthy(permissionSet[constants.FieldIsDeleted])) {
		return appErrors.NewInvalidReferenceError(inactivePermissionSetAssignmentMessage)
	}
	// PreviewBeforeUpdate intentionally runs in a plain disposable transaction.
	// Assignment-policy authority is a locking current read that is valid only in
	// the pessimistic commit attempt after the canonical parent lock is held.
	if !constants.HasUpdatePreviewPreparation(ctx) {
		if s.assignmentPolicyAuthority == nil {
			return appErrors.NewInternalError("assignment policy direct-write authority is unavailable", nil)
		}
		if err := s.assignmentPolicyAuthority.RejectOrdinaryAssignmentMutation(ctx, permissionSetID); err != nil {
			return err
		}
	}

	return nil
}

func (s *PersistenceService) validateSharingRuleOwnerReferences(ctx context.Context, objectName string, data models.SObject) error {
	if !strings.EqualFold(objectName, constants.TableSharingRule) {
		return nil
	}

	type refCheck struct {
		fieldName    string
		targetObject string
	}

	checks := []refCheck{
		{fieldName: constants.FieldSharingRule_SharedFromRoleID, targetObject: constants.TableUserRole},
		{fieldName: constants.FieldSharingRule_SharedFromGroupID, targetObject: constants.TableGroup},
		{fieldName: constants.FieldSharingRule_ShareWithRoleID, targetObject: constants.TableUserRole},
		{fieldName: constants.FieldSharingRule_ShareWithGroupID, targetObject: constants.TableGroup},
	}

	for _, check := range checks {
		raw, ok := data[check.fieldName]
		if !ok || raw == nil {
			continue
		}

		id, ok := raw.(string)
		if !ok {
			continue
		}
		id = strings.TrimSpace(id)
		if id == "" {
			continue
		}

		record, err := s.repo.FindOne(ctx, check.targetObject, id)
		if err != nil {
			return appErrors.NewInternalError("failed to validate SharingRule owner reference", err)
		}
		if record != nil {
			continue
		}

		msg := fmt.Sprintf("%s: referenced %s record %q does not exist", check.fieldName, check.targetObject, id)
		return appErrors.NewInvalidReferenceError(msg, check.fieldName)
	}

	return nil
}

func (s *PersistenceService) validateGroupUserRoleReferenceContract(ctx context.Context, objectName string, groupID string, record models.SObject) error {
	if !strings.EqualFold(objectName, constants.TableGroup) || record == nil {
		return nil
	}

	normalizeGroupUserRoleReferenceWriteImage(objectName, record)
	groupType := strings.TrimSpace(stringField(record, constants.FieldGroup_Type))
	hasUserRoleID := hasOptionalString(record, constants.FieldGroup_UserRoleID)

	if isRoleBackedGroupType(groupType) {
		if !hasUserRoleID {
			return appErrors.NewValidationError(constants.FieldGroup_UserRoleID, groupUserRoleRequiredMessage)
		}
		if err := s.rejectRoleBackedGroupWithManualMembers(ctx, groupID); err != nil {
			return err
		}
		return nil
	}

	if hasUserRoleID {
		return appErrors.NewValidationError(constants.FieldGroup_UserRoleID, groupUserRoleForbiddenMessage)
	}

	return nil
}

func normalizeGroupUserRoleReferenceWriteImage(objectName string, record models.SObject) {
	if !strings.EqualFold(objectName, constants.TableGroup) || record == nil {
		return
	}
	normalizeOptionalStringField(record, constants.FieldGroup_UserRoleID)
}

func (s *PersistenceService) rejectRoleBackedGroupWithManualMembers(ctx context.Context, groupID string) error {
	groupID = strings.TrimSpace(groupID)
	if groupID == "" {
		return nil
	}

	children, err := s.repo.GetChildren(authoritativeRoleBackedGroupReadContext(ctx), constants.TableGroupMember, constants.FieldGroupMember_GroupID, groupID)
	if err != nil {
		return appErrors.NewInternalError("failed to resolve authoritative GroupMember children for role-backed group", err)
	}
	if len(children) > 0 {
		return appErrors.NewValidationError(constants.FieldGroup_UserRoleID, roleBackedGroupManualMembersMessage)
	}

	return nil
}

func (s *PersistenceService) validateRoleBackedGroupMemberMutation(ctx context.Context, objectName string, record models.SObject) error {
	if !strings.EqualFold(objectName, constants.TableGroupMember) || record == nil {
		return nil
	}

	groupID := strings.TrimSpace(stringField(record, constants.FieldGroupMember_GroupID))
	if groupID == "" {
		return nil
	}

	group, err := s.repo.FindOne(authoritativeRoleBackedGroupReadContext(ctx), constants.TableGroup, groupID)
	if err != nil {
		return appErrors.NewInternalError("failed to resolve GroupMember group contract", err)
	}
	if group == nil {
		return nil
	}

	normalizeOptionalStringField(group, constants.FieldGroup_UserRoleID)
	if isRoleBackedGroupType(stringField(group, constants.FieldGroup_Type)) &&
		hasOptionalString(group, constants.FieldGroup_UserRoleID) {
		return appErrors.NewValidationError(constants.FieldGroupMember_GroupID, roleBackedGroupMemberMutationMessage)
	}

	return nil
}

func (s *PersistenceService) validateUserRoleParentRole(ctx context.Context, objectName string, recordID string, record models.SObject, pendingParents map[string]string) error {
	if !strings.EqualFold(objectName, constants.TableUserRole) || record == nil {
		return nil
	}

	parentRoleID := strings.TrimSpace(stringField(record, constants.FieldUserRole_ParentRoleID))
	if parentRoleID == "" {
		return nil
	}

	roleID := strings.TrimSpace(recordID)
	if roleID == "" {
		roleID = strings.TrimSpace(stringField(record, constants.FieldID))
	}
	if roleID == "" {
		return nil
	}
	return s.validateUserRoleParentRoleGraph(ctx, roleID, parentRoleID, pendingParents)
}

func (s *PersistenceService) validateUserRoleParentRoleGraph(ctx context.Context, roleID string, parentRoleID string, pendingParents map[string]string) error {
	if parentRoleID == roleID {
		return apierr.Invalid("ParentRoleId cannot reference self")
	}

	visited := map[string]bool{roleID: true}
	cur := parentRoleID
	for cur != "" {
		if visited[cur] {
			return apierr.Invalid("ParentRoleId would create a cycle in role hierarchy")
		}
		visited[cur] = true
		if pendingParents != nil {
			if next, ok := pendingParents[cur]; ok {
				cur = strings.TrimSpace(next)
				continue
			}
		}
		rec, err := s.repo.FindOne(ctx, constants.TableUserRole, cur)
		if err != nil || rec == nil {
			break
		}
		cur = strings.TrimSpace(stringField(rec, constants.FieldUserRole_ParentRoleID))
	}

	return nil
}

func (s *PersistenceService) validateUserRoleDeleteReferences(ctx context.Context, objectName string, roleID string) error {
	if !strings.EqualFold(objectName, constants.TableUserRole) {
		return nil
	}
	roleID = strings.TrimSpace(roleID)
	if roleID == "" {
		return nil
	}

	if err := s.rejectLiveUserRoleReferences(ctx, constants.TableUserRole, constants.FieldUserRole_ParentRoleID, roleID, userRoleDeleteChildRoleMessage); err != nil {
		return err
	}
	if err := s.rejectLiveUserRoleReferences(ctx, constants.TableUser, constants.FieldUserRoleID, roleID, userRoleDeleteAssignedUsersMessage); err != nil {
		return err
	}
	return nil
}

func validateUserDeleteLifecycle(objectName string) error {
	if !strings.EqualFold(objectName, constants.TableUser) {
		return nil
	}
	return apierr.Conflict(userDeleteLifecycleMessage)
}

func (s *PersistenceService) rejectLiveUserRoleReferences(ctx context.Context, childTable string, foreignKey string, roleID string, message string) error {
	children, err := s.repo.GetChildren(authoritativeUserRoleDeleteGuardReadContext(ctx), childTable, foreignKey, roleID)
	if err != nil {
		return appErrors.NewInternalError(fmt.Sprintf("failed to resolve %s references for UserRole delete guard", childTable), err)
	}
	for _, child := range children {
		if !isGuardFieldTruthy(child[constants.FieldIsDeleted]) {
			return apierr.Conflict(message)
		}
	}
	return nil
}

func isRoleBackedGroupType(groupType string) bool {
	return constants.IsRoleBackedGroupType(groupType)
}

func authoritativeRoleBackedGroupReadContext(ctx context.Context) context.Context {
	if constants.GetSecurityContext(ctx) >= constants.SystemContext {
		return ctx
	}
	return constants.WithSecurityContext(ctx, constants.SystemContext, "role_backed_group_member_contract_read")
}

func authoritativeUserRoleDeleteGuardReadContext(ctx context.Context) context.Context {
	if constants.GetSecurityContext(ctx) >= constants.SystemContext {
		return ctx
	}
	return constants.WithSecurityContext(ctx, constants.SystemContext, "user_role_delete_guard_read")
}

// checkUniqueness checks if unique fields are unique
func (s *PersistenceService) checkUniqueness(ctx context.Context, objectName string, data models.SObject, schema *models.ObjectMetadata, excludeID string) error {
	return s.checkUniquenessSkippingFields(ctx, objectName, data, schema, excludeID, nil)
}

func (s *PersistenceService) checkUniquenessSkippingFields(ctx context.Context, objectName string, data models.SObject, schema *models.ObjectMetadata, excludeID string, skipFields map[string]struct{}) error {
	for _, field := range schema.Fields {
		if !field.IsUnique {
			continue
		}
		if _, skip := skipFields[strings.ToLower(field.APIName)]; skip {
			continue
		}

		val, ok := data[field.APIName]
		if !ok || val == nil {
			continue // Skip if not present or null
		}

		// Skip if string is empty
		if strVal, ok := val.(string); ok && strVal == "" {
			continue
		}

		// Check database using Repository
		queryField := field.APIName
		if field.PhysicalName != "" {
			queryField = field.PhysicalName
		}
		if s.previewSyntheticUniqueConflict(ctx, objectName, &field, val, excludeID) {
			return appErrors.NewConflictError(objectName, field.APIName, fmt.Sprintf("%v", val))
		}
		exists, err := s.repo.CheckUniqueness(ctx, schema, queryField, val, excludeID)
		if err != nil {
			return err
		}

		if exists {
			// Found duplicate
			return appErrors.NewConflictError(objectName, field.APIName, fmt.Sprintf("%v", val))
		}
	}
	return nil
}

func (s *PersistenceService) previewSyntheticUniqueConflict(ctx context.Context, objectName string, field *models.FieldMetadata, val interface{}, excludeID string) bool {
	if field == nil {
		return false
	}
	for _, record := range constants.PreviewSyntheticRecordsForObject(ctx, objectName) {
		id, _ := record.Fields[constants.FieldID].(string)
		if excludeID != "" && id == excludeID {
			continue
		}
		if s.areValuesEqual(record.Fields[field.APIName], val) {
			return true
		}
		if field.PhysicalName != "" && field.PhysicalName != field.APIName && s.areValuesEqual(record.Fields[field.PhysicalName], val) {
			return true
		}
	}
	return false
}

// areValuesEqual checks if two interface values are equal via stringified
// comparison. Field-type semantics are not applied — callers that have field
// metadata available should use areFieldValuesEqual instead so that
// serialization-variant fields (e.g. multi-picklist) are compared by their
// canonical form rather than byte-identical representation.
func (s *PersistenceService) areValuesEqual(a, b interface{}) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
}

// areFieldValuesEqual compares two values using the field's type semantics.
// For multi-picklist fields, the read path (FromStorageRecord) returns a
// semicolon-joined scalar while the write path (NormalizeSObject) stores a
// JSON-array string; a byte-equal comparison would always flag the field as
// changed, which under a record-triggered flow that writes the record's own
// multi-picklist fields produces an infinite self-update loop (issue #1683).
// Here we normalize both sides into a sorted set of selected items so any
// supported serialization (scalar, JSON array string, []string, []interface{})
// compares equal when the underlying set matches.
//
// Falls back to areValuesEqual when field is nil or the type has no special
// canonicalization rule.
func (s *PersistenceService) areFieldValuesEqual(field *models.FieldMetadata, a, b interface{}) bool {
	if field != nil && constants.NormalizeFieldType(string(field.Type)) == constants.FieldTypeMultiPicklist {
		// Both sides must reach the comparator through the SAME field-aware
		// canonical form, otherwise the live update path diverges. The LHS
		// (`a`) comes from GetLock → FromStorageRecord, which already projects
		// the stored value into Salesforce wire text (semicolon-joined for
		// 2+ selections, raw declared-option literal for 1 declared-literal
		// selection). The RHS (`b`) comes from NormalizeSObject and arrives
		// as the NormalizedMultiPicklistJSON sentinel wrapping storage shape.
		// Normalising only the RHS — as the prior fix did — let a stored
		// declared option whose VALUE literally is `["A","B"]` arrive on LHS
		// split into two selections (A, B) while the RHS stayed a single
		// literal option, falsely flagging a no-op PATCH as a change and
		// re-triggering the self-update loop (#1683 extension flagged by
		// Codex on PR #898).
		//
		// Route both through the field-aware canonical-items projection:
		// NormalizeMultiPicklistStorageForField produces a sentinel whose
		// inner JSON-array is the authoritative selection set; equal
		// selections yield equal item sets regardless of input shape
		// (storage envelope, wire scalar, sentinel-wrapped JSON, []string,
		// []interface{}).
		return multiPicklistSelectionsEqual(a, b, field)
	}
	return s.areValuesEqual(a, b)
}

// multiPicklistSelectionsEqual reports whether two values represent the
// same set of selections under the given field's option-aware semantics.
// Order and duplicate-tolerant; empty / nil / blank on either side collapse
// to the empty set.
func multiPicklistSelectionsEqual(a, b interface{}, field *models.FieldMetadata) bool {
	return schemautil.CanonicalMultiPicklistSetKeyForField(a, field) == schemautil.CanonicalMultiPicklistSetKeyForField(b, field)
}

func coerceFieldDefaultValue(field models.FieldMetadata) (interface{}, bool) {
	if field.DefaultValue == nil {
		return nil, false
	}
	if constants.NormalizeFieldType(string(field.Type)) == constants.FieldTypeAutonumber {
		return nil, false
	}

	valStr := *field.DefaultValue
	var val interface{} = valStr
	if strings.EqualFold(valStr, "CURRENT_TIMESTAMP") {
		return utils.NowTimestamp(), true
	}

	switch field.Type {
	case constants.FieldTypeBoolean:
		if b, err := strconv.ParseBool(valStr); err == nil {
			val = b
		}
	case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
		if f, err := strconv.ParseFloat(valStr, 64); err == nil {
			val = f
		}
	case "int", "integer":
		if i, err := strconv.ParseInt(valStr, 10, 64); err == nil {
			val = i
		}
	}

	return val, true
}

// applyFieldDefaults applies metadata-owned defaults for non-system fields
// without generating IDs or audit stamps. Insert runs this after
// insertInheritFromLookup so inheritance can outrank schema defaults.
func applyFieldDefaults(data models.SObject, schema *models.ObjectMetadata) models.SObject {
	if schema == nil {
		return data
	}

	result := make(models.SObject, len(data))
	for k, v := range data {
		result[k] = v
	}

	for _, field := range schema.Fields {
		if field.IsSystem {
			continue
		}
		if _, exists := result[field.APIName]; exists {
			continue
		}
		if val, ok := coerceFieldDefaultValue(field); ok {
			result[field.APIName] = val
		}
	}

	return result
}

func applyCountRollupSummaryDefaults(data models.SObject, schema *models.ObjectMetadata) {
	if schema == nil || data == nil {
		return
	}
	for _, field := range schema.Fields {
		if _, exists := data[field.APIName]; exists {
			continue
		}
		if isCountRollupSummaryField(field) {
			data[field.APIName] = int64(0)
		}
	}
}

func isCountRollupSummaryField(field models.FieldMetadata) bool {
	return constants.NormalizeFieldType(string(field.Type)) == constants.FieldTypeRollupSummary &&
		field.RollupConfig != nil &&
		strings.EqualFold(strings.TrimSpace(field.RollupConfig.CalcType), "COUNT")
}

// generateSystemFields creates system field values based on metadata
// This applies system field values based on metadata definitions
func (s *PersistenceService) generateSystemFields(
	ctx context.Context,
	objectName string,
	data models.SObject,
	currentUser *models.UserSession,
	isInsert bool,
) models.SObject {
	// 1. Get Schema
	schema := s.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		// Should not happen as validated before
		return data
	}

	result := make(models.SObject)
	// Copy existing data
	for k, v := range data {
		result[k] = v
	}

	// 2. Iterate fields and apply system/default values
	for _, field := range schema.Fields {
		fieldName := field.APIName

		// Skip if value already exists and we are not forcing system fields
		// (For system fields, we overwrite)
		_, exists := result[fieldName]

		// ID
		if fieldName == constants.FieldID && isInsert {
			val, ok := result[constants.FieldID]
			if !ok || val == "" {
				if reserved := runtimeReservedCreateID(ctx); reserved != "" {
					result[constants.FieldID] = reserved
				} else {
					result[constants.FieldID] = utils.GenerateID()
				}
			}
			continue
		}

		// Owner
		if fieldName == constants.FieldOwnerID {
			if isInsert && !exists {
				if currentUser != nil {
					result[constants.FieldOwnerID] = currentUser.ID
				} else {
					result[constants.FieldOwnerID] = constants.SystemUserID
				}
			}
			continue
		}

		// Created By
		if fieldName == constants.FieldCreatedByID && isInsert {
			if !exists {
				if currentUser != nil {
					result[constants.FieldCreatedByID] = currentUser.ID
				} else {
					result[constants.FieldCreatedByID] = constants.SystemUserID
				}
			}
			continue
		}

		// Created Date
		if fieldName == constants.FieldCreatedDate && isInsert {
			if !exists {
				result[constants.FieldCreatedDate] = utils.NowTimestamp()
			}
			continue
		}

		// Last Modified By
		if fieldName == constants.FieldLastModifiedByID {
			if isInsert && exists {
				// Respect provided value on insert
				continue
			}
			if currentUser != nil {
				result[constants.FieldLastModifiedByID] = currentUser.ID
			} else {
				result[constants.FieldLastModifiedByID] = constants.SystemUserID
			}
			continue
		}

		// Last Modified Date
		if fieldName == constants.FieldLastModifiedDate {
			if isInsert && exists {
				// Respect provided value on insert
				continue
			}
			result[constants.FieldLastModifiedDate] = utils.NowTimestamp()
			continue
		}

		// System Modstamp (Always updated on write)
		if fieldName == constants.FieldSystemModstamp {
			// Always overwrite system modstamp
			result[constants.FieldSystemModstamp] = utils.NowTimestamp()
			continue
		}

		// Is Deleted - default to false on insert
		if fieldName == constants.FieldIsDeleted && isInsert && !exists {
			result[constants.FieldIsDeleted] = false
			continue
		}

		// Default values for non-system fields on insert.
		if isInsert && !exists {
			if val, ok := coerceFieldDefaultValue(field); ok {
				result[fieldName] = val
			}
		}
	}

	return result
}

// applyDefaults applies default values to fields that are missing
func (s *PersistenceService) applyDefaults(ctx context.Context, data models.SObject, schema *models.ObjectMetadata, currentUser *models.UserSession) models.SObject {
	// Wrapper around generateSystemFields for clarity
	return s.generateSystemFields(ctx, schema.APIName, data, currentUser, true)
}

// mergeRecords merges updates into base record
func (s *PersistenceService) mergeRecords(base, updates models.SObject) models.SObject {
	merged := make(models.SObject)
	for k, v := range base {
		merged[k] = v
	}
	for k, v := range updates {
		merged[k] = v
	}
	return merged
}

// getRecordName returns the name/label of a record
func (s *PersistenceService) getRecordName(record models.SObject, schema *models.ObjectMetadata) string {
	// Find name field
	nameField := constants.FieldID // Fallback
	for _, f := range schema.Fields {
		if f.IsNameField {
			nameField = f.APIName
			break
		}
	}

	if val, ok := record[nameField]; ok {
		return fmt.Sprintf("%v", val)
	}
	return ""
}
