package records

import (
	"context"
	"fmt"
	"strings"

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

// rejectSystemManagedOwnerWrite is the metadata-driven system-profile-owned
// immutability guard. It resolves the system-managed owner of both the existing
// (pre-write) and final (post-write) record images via the object's authored
// OwnerResolutionSpec and rejects the write if either image is owned by a
// system-managed entity.
//
// It is the sole owner-resolution path: all 13 system-security tables have been
// migrated to metadata-driven OwnerResolutionSpecs, and the legacy per-object
// switch has been deleted. The contract: both images are checked (in
// existing-then-final order), an empty or orphan FK is a definitive non-system
// owner, FK reads run under the authoritative (system) security context, and
// FindOne errors propagate as internal errors. Object/table vocabulary lives
// entirely in the spec, so this path carries no SObject-name branches.
func (s *PersistenceService) rejectSystemManagedOwnerWrite(ctx context.Context, spec *models.OwnerResolutionSpec, existingRecord models.SObject, finalRecord models.SObject) error {
	existingOwned, err := s.resolveSystemManagedOwner(ctx, spec, existingRecord)
	if err != nil {
		return err
	}
	if existingOwned {
		return appErrors.NewForbiddenError(systemProfileOwnedPermissionResourceLockedMessage)
	}

	finalOwned, err := s.resolveSystemManagedOwner(ctx, spec, finalRecord)
	if err != nil {
		return err
	}
	if finalOwned {
		return appErrors.NewForbiddenError(systemProfileOwnedPermissionResourceLockedMessage)
	}

	return nil
}

// resolveSystemManagedOwner reports whether the record is owned by a
// system-managed entity per the spec. Paths are OR'd: the record is
// system-managed if any path resolves affirmatively. A nil record or nil/empty
// spec is definitively not system-managed. The first path that errors short-
// circuits the error.
func (s *PersistenceService) resolveSystemManagedOwner(ctx context.Context, spec *models.OwnerResolutionSpec, record models.SObject) (bool, error) {
	if spec == nil || record == nil || len(spec.Paths) == 0 {
		return false, nil
	}
	for _, path := range spec.Paths {
		owned, err := s.resolveSystemManagedOwnerPath(ctx, path, record)
		if err != nil {
			return false, err
		}
		if owned {
			return true, nil
		}
	}
	return false, nil
}

// resolveSystemManagedOwnerPath evaluates one FK chain. Hops are evaluated in
// order. A hop whose FK is empty or references a non-existent record, or whose
// gate does not hold, makes the path not match (definitively false, not an
// error). Exactly one hop carries SystemWhen and terminates the path: the owner
// is system-managed iff that predicate holds. A Self hop evaluates the current
// record directly (no FK traversal); it is valid only as the first hop.
//
// A path that traverses every hop without reaching a SystemWhen terminal is
// malformed metadata: fail closed rather than treat the owner as non-system.
func (s *PersistenceService) resolveSystemManagedOwnerPath(ctx context.Context, path models.OwnerResolutionPath, record models.SObject) (bool, error) {
	current := record
	for _, hop := range path.Hops {
		if !hop.Self {
			if hop.ToObject == "" {
				return false, appErrors.NewInternalError("system-managed owner resolution hop is missing toObject", nil)
			}
			fk := strings.TrimSpace(stringField(current, hop.ViaField))
			if fk == "" {
				return false, nil
			}
			readCtx := authoritativePermissionLockReadContext(ctx)
			resolved, err := s.repo.FindOne(readCtx, hop.ToObject, fk)
			if err != nil {
				return false, appErrors.NewInternalError("failed to resolve authoritative system-managed owner", err)
			}
			if resolved == nil {
				return false, nil
			}
			current = resolved
		}
		if hop.Gate != nil && !ownerPredicateHolds(hop.Gate, current) {
			return false, nil
		}
		if hop.SystemWhen != nil {
			return ownerPredicateHolds(hop.SystemWhen, current), nil
		}
	}
	return false, appErrors.NewInternalError("system-managed owner resolution path has no terminal systemWhen predicate", fmt.Errorf("path has %d hops", len(path.Hops)))
}

// ownerPredicateHolds reports whether the single-field boolean-truthy predicate
// holds on the record, matching the records engine's isGuardFieldTruthy
// semantics. A predicate with no field never holds.
func ownerPredicateHolds(pred *models.OwnerPredicate, record models.SObject) bool {
	if pred == nil || pred.Field == "" || record == nil {
		return false
	}
	return isGuardFieldTruthy(record[pred.Field]) == pred.Is
}
