package records

import (
	"context"
	"fmt"
	"strings"

	"github.com/nexuscrm/kernel/pkg/apierr"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/recordtypes"
)

// validatePolymorphicLookups verifies that referenced IDs in polymorphic fields exist in at least one of the allowed objects
// Returns a map of fieldName -> foundObjectType
func (ps *PersistenceService) validatePolymorphicLookups(ctx context.Context, data models.SObject, schema *models.ObjectMetadata) (map[string]string, error) {
	resolved := make(map[string]string)
	for _, field := range schema.Fields {
		// Only check polymorphic lookups that have a value
		if !isPolymorphicReferenceField(&field) {
			continue
		}

		val, ok := data[field.APIName]
		if !ok || val == nil {
			continue
		}

		idVal, ok := val.(string)
		if !ok || idVal == "" {
			continue
		}

		// Check if ID exists in ANY of the referenced objects
		found := false
		var checkedObjects []string
		var lastProbeErr error

		// Preview short-circuit: under a no-DML insert preview
		// preparation, accept synthetic preview IDs that the request-local
		// registry knows about for any of this field's allowed targets.
		// Runtime-minted preview IDs include per-run entropy, so caller-
		// supplied literals cannot predictably collide with same-run
		// RecordCreate IDs. Still respects isWritableReferenceTarget so
		// the write-shape contract is unchanged.
		if constants.HasInsertPreviewPreparation(ctx) {
			for _, refObj := range field.ReferenceTo {
				if _, ok := constants.PreviewSyntheticRecordForObject(ctx, refObj, idVal); !ok {
					continue
				}
				if !isWritableReferenceTarget(ctx, &field, refObj) {
					return nil, apierr.Invalid(fmt.Sprintf("validation error on field '%s': referenced %s record is not writable through this field", field.APIName, refObj))
				}
				resolved[field.APIName] = refObj
				found = true
				break
			}
			if found {
				continue
			}
		}

		for _, refObj := range field.ReferenceTo {
			tableToCheck := refObj
			checkedObjects = append(checkedObjects, tableToCheck)
			// Efficient existence check using ID
			exists, err := ps.checkRecordExists(ctx, tableToCheck, idVal)
			ps.logger.Info(fmt.Sprintf("Poly check: Field=%s, ID=%s, Obj=%s, Table=%s, Exists=%v, Err=%v", field.APIName, idVal, refObj, tableToCheck, exists, err))
			if err != nil {
				// Remember the last probe failure. We keep checking remaining
				// objects because a later probe may still find the record. If
				// none does, the error below distinguishes "genuinely absent"
				// from "we couldn't verify" — without this, a transient DB
				// hiccup would be reported as a 400 validation failure.
				ps.logger.Error(fmt.Sprintf("Warning: failed to check existence in %s", refObj), err)
				lastProbeErr = err
				continue
			}
			if exists {
				if !isWritableReferenceTarget(ctx, &field, refObj) {
					return nil, apierr.Invalid(fmt.Sprintf("validation error on field '%s': referenced %s record is not writable through this field", field.APIName, refObj))
				}
				found = true
				resolved[field.APIName] = refObj
				break
			}
		}

		if !found {
			if lastProbeErr != nil {
				return nil, appErrors.NewInternalError(
					fmt.Sprintf("failed to validate polymorphic lookup for field '%s' (id=%s)", field.APIName, idVal),
					lastProbeErr,
				)
			}
			return nil, apierr.Invalid(fmt.Sprintf("validation error on field '%s': referenced ID %s not found in any of the allowed objects: %s", field.APIName, idVal, strings.Join(checkedObjects, ", ")))
		}
	}
	return resolved, nil
}

// checkRecordExists checks if a record exists by ID (bypassing permissions for validation)
func (ps *PersistenceService) checkRecordExists(ctx context.Context, objectName string, id string) (bool, error) {
	return ps.repo.Exists(ctx, objectName, id)
}

// validateQueueAssignment checks two things:
//  1. Existence: the supplied OwnerId must resolve to a LIVE (non-deleted) row
//     in User or Group (existence-only, RLS-bypassing — a system/cron job may
//     assign an owner that is not RLS-visible to the session, which is
//     intentional and must not be rejected here; only a truly dangling or
//     soft-deleted id fails).
//  2. Queue support: if the owner is a Group of type "Queue", the Queue must
//     be configured to support the target object type.
func (ps *PersistenceService) validateQueueAssignment(ctx context.Context, tableName string, record models.SObject, currentUser *models.UserSession) error {
	var ownerID string

	// Check for OwnerId (SSoT)
	if v, ok := record[constants.FieldOwnerID].(string); ok {
		ownerID = v
	}

	if ownerID == "" {
		return nil
	}
	if currentUser != nil && currentUser.ID != constants.SystemUserID && ownerID == currentUser.ID {
		return nil
	}

	// The platform's own synthetic `system` actor has no seeded User row by
	// design (LoadSystemUserSession is intentionally rowless). Accept OwnerId=system
	// ONLY for the platform's own writes, gated on BOTH (defense in depth):
	//   (a) the writing actor is the system actor (connector-inbound worker,
	//       scheduled automation via newSystemAutomationUser) OR there is no user
	//       (nil: setup/apply and other platform-internal writes), mirroring
	//       generateSystemFields which defaults a missing OwnerId to `system` for
	//       exactly those two cases; AND
	//   (b) the execution context is genuinely system/internal
	//       (GetSecurityContext(ctx) >= SystemContext).
	// The actor check rejects a real authenticated user supplying OwnerId=system
	// even under SystemContext; the SystemContext check rejects a nil/system actor
	// that reaches persistence outside a system context — so the rowless-owner
	// allowance is an enforced records-layer boundary, not a caller convention.
	if ownerID == constants.SystemUserID &&
		(currentUser == nil || currentUser.ID == constants.SystemUserID) &&
		constants.GetSecurityContext(ctx) >= constants.SystemContext {
		return nil
	}

	ownerUserChecked := false
	var ownerUserRecord models.SObject
	if hasOwnerUserLookupProof(ctx, ownerID) {
		// Flow lookup proof is only a User-type hint. Liveness remains owned by
		// the write-time validator, so re-read User in the current write context
		// before accepting the OwnerId.
		ownerUserChecked = true
		var err error
		ownerUserRecord, err = ps.repo.FindOne(ctx, constants.TableUser, ownerID)
		if err != nil {
			return appErrors.NewInternalError(fmt.Sprintf("failed to validate OwnerId user lookup (id=%s)", ownerID), err)
		}
		if ownerUserRecord != nil && !isGuardFieldTruthy(ownerUserRecord[constants.FieldIsDeleted]) {
			return nil
		}
	}

	// 1. Check if Owner is a Group (covers Queue, Role, Public Group, etc.)
	// We check _System_Group directly using Repo (bypasses permissions).
	groupRecord, err := ps.cachedFindOne(ctx, constants.TableGroup, ownerID)
	if err != nil {
		// Fail closed: a transient DB error must abort the write rather than
		// silently persist an unvalidated owner (mirrors validatePolymorphicLookups
		// and validateSharingRuleOwnerReferences convention).
		return appErrors.NewInternalError(fmt.Sprintf("failed to validate OwnerId group lookup (id=%s)", ownerID), err)
	}

	if groupRecord != nil {
		// Owner is a Group. Check Queue support if applicable.
		groupType, ok := groupRecord[constants.FieldGroup_Type].(string)
		if !ok || strings.TrimSpace(groupType) == "" {
			return appErrors.NewInternalError(fmt.Sprintf("failed to validate OwnerId group lookup (id=%s): cached/raw Group row is missing Type", ownerID), nil)
		}

		// 2. If it is a Queue, check _System_QueueSobject permission
		if strings.EqualFold(groupType, constants.GroupTypeQueue) {
			supported, err := ps.repo.CheckQueueSupport(ctx, ownerID, tableName)
			if err != nil {
				return err
			}
			if !supported {
				return appErrors.NewValidationError("OwnerId", fmt.Sprintf("FIELD_INTEGRITY_EXCEPTION: Queue %s is not configured to support object type %s", ownerID, tableName))
			}
		}
		return nil
	}

	// Owner is not a Group. Verify it exists as a LIVE User via FindOne
	// (which excludes soft-deleted rows, unlike Exists). RLS is still bypassed
	// so an existing-but-RLS-invisible user (system/cron assignment) passes.
	userRecord := ownerUserRecord
	if !ownerUserChecked {
		var err error
		userRecord, err = ps.cachedFindOne(ctx, constants.TableUser, ownerID)
		if err != nil {
			// Fail closed: abort the write rather than accept an unvalidated owner.
			return appErrors.NewInternalError(fmt.Sprintf("failed to validate OwnerId user lookup (id=%s)", ownerID), err)
		}
	}
	if userRecord == nil || isGuardFieldTruthy(userRecord[constants.FieldIsDeleted]) {
		msg := fmt.Sprintf("OwnerId: referenced User record %q does not exist", ownerID)
		return appErrors.NewInvalidReferenceError(msg, constants.FieldOwnerID)
	}

	return nil
}

func hasOwnerUserLookupProof(ctx context.Context, ownerID string) bool {
	c := recordReadCacheFromCtx(ctx)
	if c == nil {
		return false
	}
	return c.hasOwnerUserProof(ownerID)
}

// validateRecordTypeId checks that a submitted RecordTypeId is valid, active,
// and belongs to the target object. If RecordTypeId is absent or empty, this
// is a no-op (the field is optional).
func (ps *PersistenceService) validateRecordTypeId(ctx context.Context, objectName string, data models.SObject, currentUser *models.UserSession) error {
	val, ok := data["RecordTypeId"]
	if !ok || val == nil {
		return nil
	}

	rtID, ok := val.(string)
	if !ok || rtID == "" {
		return nil
	}

	recordTypes, err := ps.metadata.GetRecordTypes(ctx, objectName)
	if err != nil {
		return appErrors.NewInternalError("load record types for mutation", err)
	}
	effectiveRecordTypes, hasExplicitGrant, err := ps.effectiveRecordTypeGrantsForObject(ctx, objectName, currentUser, recordTypes)
	if err != nil {
		return err
	}
	if rtID == recordtypes.MasterRecordTypeID {
		surface := recordtypes.BuildSelectionSurface(recordTypes, effectiveRecordTypes, hasExplicitGrant)
		if surface.HasConcreteRecordTypes {
			return appErrors.NewValidationError("RecordTypeId",
				fmt.Sprintf("record type '%s' is not available for object '%s'", rtID, objectName))
		}
		return nil
	}
	for _, rt := range recordTypes {
		if rt.ID == rtID {
			if !rt.IsActive {
				return appErrors.NewValidationError("RecordTypeId",
					fmt.Sprintf("record type '%s' is inactive for object '%s'", rtID, objectName))
			}
			if hasExplicitGrant {
				grant, ok := effectiveRecordTypes[rtID]
				if ok && !grant.IsVisible {
					return appErrors.NewValidationError("RecordTypeId",
						fmt.Sprintf("record type '%s' is not available for object '%s'", rtID, objectName))
				}
			}
			return nil
		}
	}

	return appErrors.NewValidationError("RecordTypeId",
		fmt.Sprintf("invalid record type ID '%s' for object '%s'", rtID, objectName))
}

func (ps *PersistenceService) effectiveRecordTypeGrantsForObject(
	ctx context.Context,
	objectName string,
	currentUser *models.UserSession,
	recordTypes []*models.RecordType,
) (map[string]models.PermissionSetRecordTypeVisibility, bool, error) {
	if ps.permissions == nil || currentUser == nil || ps.permissions.HasSuperUserCapability(ctx, currentUser) {
		return nil, false, nil
	}

	effective, err := ps.permissions.GetEffectiveRecordTypeVisibilities(ctx, currentUser.ID, currentUser.ProfileID)
	if err != nil {
		return nil, false, err
	}
	if len(effective) == 0 {
		return nil, false, nil
	}
	grants, ok := recordtypes.FilterGrantsForObject(recordTypes, effective)
	return grants, ok, nil
}
