package programchecker

import (
	"context"
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	sharedcontract "github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

type ObjectAccessPolicyReferenceCheckOptions struct {
	Metadata      ProgramMetadataReader
	Policy        PolicyReader
	Capabilities  CapabilityCatalogReader
	Validation    ValidationRuleReader
	ComponentPath string
	OwnerPack     string
}

// CheckObjectAccessPolicyReferences validates metadata-owned policy references
// without re-evaluating policy decisions. Runtime access remains owned by the
// policy reader; this pass only verifies that authored policy shapes can resolve
// their object, field, operation, and system-permission references.
func CheckObjectAccessPolicyReferences(ctx context.Context, opts ObjectAccessPolicyReferenceCheckOptions) Diagnostics {
	c := &objectAccessPolicyReferenceChecker{ctx: ctx, opts: opts}
	c.check()
	return c.diagnostics
}

type objectAccessPolicyReferenceChecker struct {
	ctx         context.Context
	opts        ObjectAccessPolicyReferenceCheckOptions
	schema      *models.ObjectMetadata
	diagnostics Diagnostics
}

func (c *objectAccessPolicyReferenceChecker) check() {
	if c.opts.Metadata == nil {
		c.addDiagnostic(DiagnosticCodeReaderError, "", "", "metadata reader is required to check access-policy references")
		return
	}
	schemas, err := c.opts.Metadata.Schemas(c.ctx)
	if err != nil {
		c.addDiagnostic(DiagnosticCodeReaderError, "", "", fmt.Sprintf("metadata reader failed while loading schemas for access-policy reference check: %v", err))
		return
	}
	for _, schema := range schemas {
		if schema == nil || schema.AccessPolicy == nil {
			continue
		}
		c.schema = schema
		c.checkPolicyReader(schema.AccessPolicy)
		c.checkAccessExpressions(schema.AccessPolicy)
		c.checkRowRead(schema.AccessPolicy.RowRead, "/accessPolicy/rowRead")
		c.checkRecordMutation(schema.AccessPolicy.RecordMutation, "/accessPolicy/recordMutation")
		c.checkLifecycle(schema.AccessPolicy.Lifecycle, "/accessPolicy/lifecycle")
		c.checkCacheInvalidation(schema.AccessPolicy.CacheInvalidation, "/accessPolicy/cacheInvalidation")
		c.checkWriteValidation(schema.AccessPolicy.WriteValidation, "/accessPolicy/writeValidation")
	}
	c.schema = nil
}

func (c *objectAccessPolicyReferenceChecker) checkPolicyReader(policy *models.ObjectAccessPolicy) {
	if !accessPolicyHasAccessExpressions(policy) {
		return
	}
	if c.opts.Policy == nil {
		c.addDiagnostic(DiagnosticCodeReaderError, "/accessPolicy", c.schema.APIName+".accessPolicy", "policy reader is required to check access-policy metadata")
		return
	}
	if _, err := c.opts.Policy.Access(c.ctx, nil, c.schema.APIName, sharedcontract.AccessRequest{IncludeFields: true, IncludeRecordTypes: true}); err != nil {
		c.addDiagnostic(DiagnosticCodeReaderError, "/accessPolicy", c.schema.APIName+".accessPolicy", fmt.Sprintf("policy reader failed for %s accessPolicy: %v", c.schema.APIName, err))
	}
}

func (c *objectAccessPolicyReferenceChecker) checkAccessExpressions(policy *models.ObjectAccessPolicy) {
	if policy == nil {
		return
	}
	surfaces := accessPolicyExpressionSurfaces(policy)
	if accessPolicyHasAccessExpressions(policy) && !policy.IsDerived() {
		c.addDiagnosticWithTypes(
			"metadata_program.invalid_policy_reference",
			"/accessPolicy/mode",
			c.schema.APIName+".accessPolicy.mode",
			"derived",
			strings.TrimSpace(policy.Mode),
			fmt.Sprintf("%s access policy object/field expressions require mode %q because runtime evaluates them only for derived policies", c.schema.APIName, "derived"),
		)
	}
	for _, surface := range surfaces {
		c.checkAccessExpr(policy, surface.expr, "/accessPolicy/"+surface.name, surface.name, map[string]bool{})
	}
}

func accessPolicyExpressionSurfaces(policy *models.ObjectAccessPolicy) []struct {
	name string
	expr *models.AccessPolicyExpr
} {
	if policy == nil {
		return nil
	}
	return []struct {
		name string
		expr *models.AccessPolicyExpr
	}{
		{name: "objectRead", expr: policy.ObjectRead},
		{name: "objectCreate", expr: policy.ObjectCreate},
		{name: "objectEdit", expr: policy.ObjectEdit},
		{name: "objectDelete", expr: policy.ObjectDelete},
		{name: "fieldRead", expr: policy.FieldRead},
		{name: "fieldEdit", expr: policy.FieldEdit},
	}
}

func accessPolicyHasAccessExpressions(policy *models.ObjectAccessPolicy) bool {
	for _, surface := range accessPolicyExpressionSurfaces(policy) {
		if surface.expr != nil {
			return true
		}
	}
	return false
}

func (c *objectAccessPolicyReferenceChecker) checkAccessExpr(policy *models.ObjectAccessPolicy, expr *models.AccessPolicyExpr, pointer, surface string, stack map[string]bool) {
	if expr == nil {
		return
	}
	for i := range expr.AllOf {
		c.checkAccessExpr(policy, &expr.AllOf[i], fmt.Sprintf("%s/allOf/%d", pointer, i), surface, stack)
	}
	for i := range expr.AnyOf {
		c.checkAccessExpr(policy, &expr.AnyOf[i], fmt.Sprintf("%s/anyOf/%d", pointer, i), surface, stack)
	}
	if expr.Not != nil {
		c.checkAccessExpr(policy, expr.Not, pointer+"/not", surface, stack)
	}
	if strings.TrimSpace(expr.Inherit) != "" {
		c.checkAccessPolicyInherit(policy, expr.Inherit, pointer+"/inherit", stack)
	}
	if len(expr.AllOf) > 0 || len(expr.AnyOf) > 0 || expr.Not != nil || strings.TrimSpace(expr.Inherit) != "" {
		return
	}

	switch strings.ToLower(strings.TrimSpace(expr.Kind)) {
	case "objectpermission":
		c.checkObjectPermissionExpr(expr, pointer)
	case "systempermission":
		c.checkSystemPermissionExpr(expr, pointer)
	case "recordfieldequalscurrentuser":
		c.requireCurrentField(expr.Field, pointer+"/field", c.schema.APIName+".accessPolicy."+surface)
	default:
		c.addDiagnosticWithTypes(
			"metadata_program.invalid_policy_reference",
			pointer+"/kind",
			c.schema.APIName+".accessPolicy."+surface,
			"objectPermission|systemPermission|recordFieldEqualsCurrentUser|allOf|anyOf|not|inherit",
			strings.TrimSpace(expr.Kind),
			fmt.Sprintf("%s access policy expression has unsupported kind %q", c.schema.APIName, expr.Kind),
		)
	}
}

func (c *objectAccessPolicyReferenceChecker) checkAccessPolicyInherit(policy *models.ObjectAccessPolicy, inherit, pointer string, stack map[string]bool) {
	target := normalizeAccessPolicyOperation(inherit)
	if target == "" {
		c.addDiagnosticWithTypes(
			"metadata_program.invalid_policy_reference",
			pointer,
			c.schema.APIName+".accessPolicy.inherit",
			"objectRead|objectCreate|objectEdit|objectDelete|fieldRead|fieldEdit",
			strings.TrimSpace(inherit),
			fmt.Sprintf("%s access policy inherits unsupported target %q", c.schema.APIName, inherit),
		)
		return
	}
	key := strings.ToLower(c.schema.APIName + ":" + target)
	if stack[key] {
		c.addDiagnostic("metadata_program.invalid_policy_reference", pointer, c.schema.APIName+".accessPolicy."+target, fmt.Sprintf("%s access policy inherit cycle on %s", c.schema.APIName, target))
		return
	}
	stack[key] = true
	defer delete(stack, key)
	c.checkAccessExpr(policy, accessPolicyExprForOperation(policy, target), "/accessPolicy/"+target, target, stack)
}

func (c *objectAccessPolicyReferenceChecker) checkObjectPermissionExpr(expr *models.AccessPolicyExpr, pointer string) {
	objectName := strings.TrimSpace(expr.Object)
	var object *models.ObjectMetadata
	var objectOK bool
	if objectName == "" {
		c.addDiagnostic("metadata_program.invalid_policy_reference", pointer+"/object", c.schema.APIName+".accessPolicy.objectPermission", "objectPermission policy expression requires object")
	} else {
		object, objectOK = c.resolveObject(objectName, pointer+"/object", c.schema.APIName+".accessPolicy.objectPermission")
		if objectOK {
			c.checkPolicyAccess(objectName, pointer+"/object")
		}
	}
	if !isConcreteObjectPermissionOperation(expr.Operation) {
		c.addDiagnosticWithTypes(
			"metadata_program.invalid_policy_reference",
			pointer+"/operation",
			c.schema.APIName+".accessPolicy.objectPermission",
			"Read|Create|Edit|Delete|ViewAllRecords|ModifyAllRecords",
			strings.TrimSpace(expr.Operation),
			fmt.Sprintf("objectPermission policy expression on %s uses unsupported operation %q", c.schema.APIName, expr.Operation),
		)
	} else if objectOK && object != nil && object.UsesDerivedAccessPolicy() && isAllRecordObjectPermissionOperation(expr.Operation) {
		c.addDiagnosticWithTypes(
			"metadata_program.invalid_policy_reference",
			pointer+"/operation",
			c.schema.APIName+".accessPolicy.objectPermission",
			"ObjectPermissions-backed object",
			object.APIName+".accessPolicy.mode=derived",
			fmt.Sprintf("objectPermission policy expression on %s cannot reference %s %q because derived access-policy objects do not own ObjectPermissions rows for all-record operations", c.schema.APIName, object.APIName, expr.Operation),
		)
	}
}

func (c *objectAccessPolicyReferenceChecker) checkPolicyAccess(objectName, pointer string) {
	if c.opts.Policy == nil {
		return
	}
	if _, err := c.opts.Policy.Access(c.ctx, nil, objectName, sharedcontract.AccessRequest{}); err != nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, objectName+".accessPolicy", fmt.Sprintf("policy reader failed for objectPermission reference %s: %v", objectName, err))
	}
}

func (c *objectAccessPolicyReferenceChecker) checkSystemPermissionExpr(expr *models.AccessPolicyExpr, pointer string) {
	apiName := strings.TrimSpace(expr.APIName)
	if apiName == "" {
		c.addDiagnostic("metadata_program.invalid_policy_reference", pointer+"/apiName", c.schema.APIName+".accessPolicy.systemPermission", "systemPermission policy expression requires apiName")
		return
	}
	if c.opts.Capabilities == nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer+"/apiName", apiName, "capability catalog reader is required to check systemPermission policy references")
		return
	}
	ok, err := c.opts.Capabilities.Capability(c.ctx, apiName)
	if err != nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer+"/apiName", apiName, fmt.Sprintf("capability catalog reader failed for %q: %v", apiName, err))
		return
	}
	if !ok {
		c.addDiagnosticWithTypes("metadata_program.missing_capability", pointer+"/apiName", apiName, "system permission capability", apiName, fmt.Sprintf("systemPermission policy reference %q does not exist", apiName))
	}
}

func (c *objectAccessPolicyReferenceChecker) checkRowRead(policy *models.RowReadPolicy, pointer string) {
	if policy == nil {
		return
	}
	switch strings.ToLower(strings.TrimSpace(policy.Kind)) {
	case "targetrecordread":
		if len(policy.TargetAssociations) > 0 &&
			(strings.TrimSpace(policy.TargetObjectField) != "" || strings.TrimSpace(policy.TargetRecordField) != "") {
			c.addDiagnostic(
				"metadata_program.invalid_policy_reference",
				pointer+"/targetAssociations",
				c.schema.APIName+".accessPolicy.rowRead",
				"targetRecordRead cannot mix targetAssociations with legacy targetObjectField/targetRecordField",
			)
			return
		}
		associations := policy.TargetAssociations
		if len(associations) == 0 {
			var valid bool
			associations, valid = policy.TargetRecordReadAssociations()
			if !valid {
				c.addDiagnostic(
					"metadata_program.invalid_policy_reference",
					pointer+"/targetAssociations",
					c.schema.APIName+".accessPolicy.rowRead",
					"targetRecordRead requires at least one target association",
				)
				return
			}
		}
		seen := make(map[string]struct{}, len(associations))
		for i, association := range associations {
			associationPointer := pointer
			if len(policy.TargetAssociations) > 0 {
				associationPointer = fmt.Sprintf("%s/targetAssociations/%d", pointer, i)
			}
			c.requireCurrentField(association.TargetObjectField, associationPointer+"/targetObjectField", c.schema.APIName+".accessPolicy.rowRead")
			c.requireCurrentField(association.TargetRecordField, associationPointer+"/targetRecordField", c.schema.APIName+".accessPolicy.rowRead")
			key := strings.ToLower(strings.TrimSpace(association.TargetObjectField)) + "\x00" + strings.ToLower(strings.TrimSpace(association.TargetRecordField))
			if _, duplicate := seen[key]; duplicate {
				c.addDiagnostic(
					"metadata_program.invalid_policy_reference",
					associationPointer,
					c.schema.APIName+".accessPolicy.rowRead",
					"targetRecordRead declares a duplicate target association",
				)
			}
			seen[key] = struct{}{}
		}
	case "ownervisibility":
		if strings.TrimSpace(policy.OwnerField) != "" {
			c.requireCurrentField(policy.OwnerField, pointer+"/ownerField", c.schema.APIName+".accessPolicy.rowRead")
		}
		c.requireCurrentField(policy.VisibilityField, pointer+"/visibilityField", c.schema.APIName+".accessPolicy.rowRead")
	default:
		c.addDiagnosticWithTypes(
			"metadata_program.invalid_policy_reference",
			pointer+"/kind",
			c.schema.APIName+".accessPolicy.rowRead",
			"targetRecordRead|ownerVisibility",
			strings.TrimSpace(policy.Kind),
			fmt.Sprintf("%s rowRead policy has unsupported kind %q", c.schema.APIName, policy.Kind),
		)
	}
}

func (c *objectAccessPolicyReferenceChecker) checkRecordMutation(policy *models.RecordMutationAccessPolicy, pointer string) {
	if policy == nil {
		return
	}
	for i := range policy.Deny {
		denial := &policy.Deny[i]
		base := fmt.Sprintf("%s/deny/%d", pointer, i)
		for j, surface := range denial.Surfaces {
			c.requireKnownValue(surface, fmt.Sprintf("%s/surfaces/%d", base, j), c.schema.APIName+".accessPolicy.recordMutation", recordMutationSurfaces)
		}
		if strings.TrimSpace(denial.Field) != "" {
			c.requireCurrentField(denial.Field, base+"/field", c.schema.APIName+".accessPolicy.recordMutation")
		}
		c.checkRecordMutationCondition(denial.When, base+"/when")
	}
}

var recordMutationSurfaces = []string{
	models.RecordMutationSurfaceSingleCreate,
	models.RecordMutationSurfaceSingleUpdate,
	models.RecordMutationSurfaceSingleDelete,
	models.RecordMutationSurfaceExternalIDUpsert,
	models.RecordMutationSurfaceCollectionCreate,
	models.RecordMutationSurfaceCollectionUpdate,
	models.RecordMutationSurfaceCollectionDelete,
	models.RecordMutationSurfaceCollectionUpsert,
	models.RecordMutationSurfaceTreeCreate,
}

func (c *objectAccessPolicyReferenceChecker) checkRecordMutationCondition(condition *models.RecordMutationCondition, pointer string) {
	if condition == nil {
		return
	}
	for i := range condition.AllOf {
		c.checkRecordMutationCondition(&condition.AllOf[i], fmt.Sprintf("%s/allOf/%d", pointer, i))
	}
	if condition.FieldEquals != nil {
		c.requireCurrentField(condition.FieldEquals.Field, pointer+"/fieldEquals/field", c.schema.APIName+".accessPolicy.recordMutation")
	}
	if condition.FieldPresent != nil {
		c.requireCurrentField(condition.FieldPresent.Field, pointer+"/fieldPresent/field", c.schema.APIName+".accessPolicy.recordMutation")
	}
	if condition.FieldPresentEquals != nil {
		c.requireCurrentField(condition.FieldPresentEquals.Field, pointer+"/fieldPresentEquals/field", c.schema.APIName+".accessPolicy.recordMutation")
	}
	if condition.RelatedExists != nil {
		c.checkRecordMutationRelatedExists(condition.RelatedExists, pointer+"/relatedExists")
	}
}

func (c *objectAccessPolicyReferenceChecker) checkRecordMutationRelatedExists(condition *models.RecordMutationRelatedExistsCondition, pointer string) {
	objectName := strings.TrimSpace(condition.ObjectAPIName)
	if objectName == "" {
		c.addDiagnostic("metadata_program.invalid_policy_reference", pointer+"/objectApiName", c.schema.APIName+".accessPolicy.recordMutation", "recordMutation relatedExists condition requires objectApiName")
		return
	}
	if !c.requireObject(objectName, pointer+"/objectApiName", c.schema.APIName+".accessPolicy.recordMutation") {
		return
	}
	c.requireField(objectName, condition.ParentField, pointer+"/parentField", c.schema.APIName+".accessPolicy.recordMutation")
	for i := range condition.Match {
		c.requireField(objectName, condition.Match[i].Field, fmt.Sprintf("%s/match/%d/field", pointer, i), c.schema.APIName+".accessPolicy.recordMutation")
	}
}

func (c *objectAccessPolicyReferenceChecker) checkLifecycle(policy *models.RecordLifecycleAccessPolicy, pointer string) {
	if policy == nil {
		return
	}
	assignmentPolicyPhases := make(map[string]struct{})
	for i, hook := range policy.Hooks {
		base := fmt.Sprintf("%s/hooks/%d", pointer, i)
		c.requirePolicyCapability(hook.Capability, base+"/capability", c.schema.APIName+".accessPolicy.lifecycle")
		c.requireKnownValue(hook.Phase, base+"/phase", c.schema.APIName+".accessPolicy.lifecycle", []string{
			string(sharedcontract.RecordLifecycleAfterCreateInTx),
			string(sharedcontract.RecordLifecycleAfterUpdateInTx),
			string(sharedcontract.RecordLifecycleAfterDeleteInTx),
		})
		if strings.EqualFold(strings.TrimSpace(hook.Capability), sharedcontract.AssignmentPolicyEligibilityLifecycleCapability) {
			assignmentPolicyPhases[strings.ToLower(strings.TrimSpace(hook.Phase))] = struct{}{}
		}
	}
	config := policy.AssignmentPolicyEligibility
	if config == nil {
		if len(assignmentPolicyPhases) != 0 {
			c.addDiagnostic(
				"metadata_program.invalid_policy_reference",
				pointer+"/assignmentPolicyEligibility",
				c.schema.APIName+".accessPolicy.lifecycle",
				"assignment-policy eligibility capability requires its Catalog projection",
			)
		}
		return
	}
	if len(assignmentPolicyPhases) != 3 {
		c.addDiagnostic(
			"metadata_program.invalid_policy_reference",
			pointer+"/hooks",
			c.schema.APIName+".accessPolicy.lifecycle",
			"assignment-policy eligibility projection requires create, update, and delete lifecycle hooks",
		)
	}
	base := pointer + "/assignmentPolicyEligibility"
	role := models.CanonicalAssignmentPolicyEligibilitySourceRole(config.Role)
	switch role {
	case models.AssignmentPolicyEligibilitySelectorAuthority, models.AssignmentPolicyEligibilitySubjectPopulation:
	default:
		c.addDiagnostic(
			"metadata_program.invalid_policy_reference",
			base+"/role",
			c.schema.APIName+".accessPolicy.lifecycle",
			"assignment-policy eligibility role must be selector_authority or subject_population",
		)
	}
	if len(config.EnabledWhen) == 0 {
		c.addDiagnostic(
			"metadata_program.invalid_policy_reference",
			base+"/enabledWhen",
			c.schema.APIName+".accessPolicy.lifecycle",
			"assignment-policy eligibility requires at least one boolean condition",
		)
	}
	seenConditions := make(map[string]struct{}, len(config.EnabledWhen))
	for i, condition := range config.EnabledWhen {
		fieldPointer := fmt.Sprintf("%s/enabledWhen/%d/field", base, i)
		c.requireFieldType(
			c.schema.APIName,
			condition.Field,
			fieldPointer,
			c.schema.APIName+".accessPolicy.lifecycle",
			[]models.FieldType{constants.FieldTypeBoolean},
		)
		key := strings.ToLower(strings.TrimSpace(condition.Field))
		if _, duplicate := seenConditions[key]; key != "" && duplicate {
			c.addDiagnostic(
				"metadata_program.invalid_policy_reference",
				fieldPointer,
				c.schema.APIName+".accessPolicy.lifecycle",
				fmt.Sprintf("assignment-policy eligibility condition repeats field %s", condition.Field),
			)
		}
		seenConditions[key] = struct{}{}
	}
	directField := strings.TrimSpace(config.Selector.Field)
	reference := config.Selector.Reference
	if (directField == "") == (reference == nil) {
		c.addDiagnostic(
			"metadata_program.invalid_policy_reference",
			base+"/selector",
			c.schema.APIName+".accessPolicy.lifecycle",
			"assignment-policy eligibility selector requires exactly one direct field or reference",
		)
		return
	}
	if role == models.AssignmentPolicyEligibilitySelectorAuthority {
		if strings.TrimSpace(config.SubjectIDField) != "" {
			c.addDiagnostic(
				"metadata_program.invalid_policy_reference",
				base+"/subjectIdField",
				c.schema.APIName+".accessPolicy.lifecycle",
				"selector_authority must not declare subjectIdField",
			)
		}
		if directField == "" {
			c.addDiagnostic(
				"metadata_program.invalid_policy_reference",
				base+"/selector",
				c.schema.APIName+".accessPolicy.lifecycle",
				"selector_authority requires a direct selector field",
			)
		}
	}
	if role == models.AssignmentPolicyEligibilitySubjectPopulation {
		if strings.TrimSpace(config.SubjectIDField) == "" {
			c.addDiagnostic(
				"metadata_program.invalid_policy_reference",
				base+"/subjectIdField",
				c.schema.APIName+".accessPolicy.lifecycle",
				"subject_population requires subjectIdField",
			)
		} else {
			c.requireField(
				c.schema.APIName,
				config.SubjectIDField,
				base+"/subjectIdField",
				c.schema.APIName+".accessPolicy.lifecycle",
			)
		}
		if reference == nil {
			c.addDiagnostic(
				"metadata_program.invalid_policy_reference",
				base+"/selector",
				c.schema.APIName+".accessPolicy.lifecycle",
				"subject_population requires a reference selector",
			)
		}
	}
	if directField != "" {
		c.requireFieldType(
			c.schema.APIName,
			directField,
			base+"/selector/field",
			c.schema.APIName+".accessPolicy.lifecycle",
			[]models.FieldType{constants.FieldTypeString},
		)
		return
	}
	if c.requireFieldType(
		c.schema.APIName,
		reference.SourceField,
		base+"/selector/reference/sourceField",
		c.schema.APIName+".accessPolicy.lifecycle",
		[]models.FieldType{constants.FieldTypeReference},
	) {
		c.requireReferenceFieldTarget(
			c.schema.APIName,
			reference.SourceField,
			reference.TargetObject,
			base+"/selector/reference/sourceField",
			c.schema.APIName+".accessPolicy.lifecycle",
		)
	}
	if c.requireObject(reference.TargetObject, base+"/selector/reference/targetObject", c.schema.APIName+".accessPolicy.lifecycle") {
		c.requireFieldType(
			reference.TargetObject,
			reference.TargetField,
			base+"/selector/reference/targetField",
			c.schema.APIName+".accessPolicy.lifecycle",
			[]models.FieldType{constants.FieldTypeString},
		)
	}
}

func (c *objectAccessPolicyReferenceChecker) checkCacheInvalidation(policy *models.RecordCacheInvalidationAccessPolicy, pointer string) {
	if policy == nil {
		return
	}
	for i, handler := range policy.Handlers {
		c.requirePolicyCapability(handler.Capability, fmt.Sprintf("%s/handlers/%d/capability", pointer, i), c.schema.APIName+".accessPolicy.cacheInvalidation")
	}
}

func (c *objectAccessPolicyReferenceChecker) checkWriteValidation(policy *models.RecordWriteValidationAccessPolicy, pointer string) {
	if policy == nil {
		return
	}
	validatorPhases := []string{
		string(sharedcontract.RecordWriteValidationBeforeCreate),
		string(sharedcontract.RecordWriteValidationBeforeUpdate),
		string(sharedcontract.RecordWriteValidationBeforeDelete),
		string(sharedcontract.RecordWriteValidationBeforeRestore),
		string(sharedcontract.RecordWriteValidationBeforePurge),
	}
	canonicalizerPhases := []string{
		string(sharedcontract.RecordWriteValidationBeforeCreate),
		string(sharedcontract.RecordWriteValidationBeforeUpdate),
		string(sharedcontract.RecordWriteValidationBeforeRestore),
	}
	for i, validator := range policy.Validators {
		base := fmt.Sprintf("%s/validators/%d", pointer, i)
		c.requirePolicyCapability(validator.Capability, base+"/capability", c.schema.APIName+".accessPolicy.writeValidation")
		c.requireKnownValue(validator.Phase, base+"/phase", c.schema.APIName+".accessPolicy.writeValidation", validatorPhases)
	}
	for i, canonicalizer := range policy.Canonicalizers {
		base := fmt.Sprintf("%s/canonicalizers/%d", pointer, i)
		c.requirePolicyCapability(canonicalizer.Capability, base+"/capability", c.schema.APIName+".accessPolicy.writeValidation")
		c.requireKnownValue(canonicalizer.Phase, base+"/phase", c.schema.APIName+".accessPolicy.writeValidation", canonicalizerPhases)
	}
	for i, req := range policy.RequiredValidationRules {
		base := fmt.Sprintf("%s/requiredValidationRules/%d", pointer, i)
		phaseOK := knownPolicyValue(req.Phase, validatorPhases)
		c.requireKnownValue(req.Phase, base+"/phase", c.schema.APIName+".accessPolicy.writeValidation", validatorPhases)
		objectName := strings.TrimSpace(req.ObjectAPIName)
		if objectName == "" {
			objectName = c.schema.APIName
		}
		objectOK := c.requireObject(objectName, base+"/objectApiName", c.schema.APIName+".accessPolicy.writeValidation")
		if strings.TrimSpace(req.Name) == "" {
			c.addDiagnostic("metadata_program.invalid_policy_reference", base+"/name", c.schema.APIName+".accessPolicy.writeValidation", "requiredValidationRules entry requires name")
		}
		if strings.TrimSpace(req.OwnerPack) == "" {
			c.addDiagnostic("metadata_program.invalid_policy_reference", base+"/ownerPack", c.schema.APIName+".accessPolicy.writeValidation", "requiredValidationRules entry requires ownerPack")
		}
		if objectOK && phaseOK {
			c.checkRequiredValidationRule(req, objectName, base)
		}
	}
	c.checkOwnerResolution(policy.OwnerResolution, pointer+"/ownerResolution")
}

func (c *objectAccessPolicyReferenceChecker) checkRequiredValidationRule(req models.RecordWriteValidationRuleRequirement, objectName, pointer string) {
	if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.OwnerPack) == "" {
		return
	}
	if c.opts.Validation == nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, c.schema.APIName+".accessPolicy.writeValidation", "validation-rule reader is required to check requiredValidationRules references")
		return
	}
	rules, err := c.opts.Validation.ValidationRules(c.ctx, objectName)
	if err != nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, c.schema.APIName+".accessPolicy.writeValidation", fmt.Sprintf("validation-rule reader failed while resolving requiredValidationRules for %s: %v", objectName, err))
		return
	}
	requirement := req
	requirement.ObjectAPIName = objectName
	missing := c.schema.RecordWriteValidationMissingRequiredValidationRulesForPhase(req.Phase, rules)
	for _, candidate := range missing {
		if !recordWriteValidationRequirementMatches(candidate, requirement) {
			continue
		}
		c.addDiagnosticWithTypes(
			"metadata_program.missing_validation_rule",
			pointer,
			c.schema.APIName+".accessPolicy.writeValidation",
			"active source-backed validation rule",
			fmt.Sprintf("%s.%s ownerPack=%s", requirement.ObjectAPIName, requirement.Name, requirement.OwnerPack),
			fmt.Sprintf("required validation rule %s.%s from owner pack %s is not active source-backed metadata", requirement.ObjectAPIName, requirement.Name, requirement.OwnerPack),
		)
		return
	}
}

func recordWriteValidationRequirementMatches(left, right models.RecordWriteValidationRuleRequirement) bool {
	return strings.EqualFold(strings.TrimSpace(left.Phase), strings.TrimSpace(right.Phase)) &&
		strings.EqualFold(strings.TrimSpace(left.ObjectAPIName), strings.TrimSpace(right.ObjectAPIName)) &&
		strings.EqualFold(strings.TrimSpace(left.Name), strings.TrimSpace(right.Name)) &&
		strings.EqualFold(strings.TrimSpace(left.OwnerPack), strings.TrimSpace(right.OwnerPack))
}

func (c *objectAccessPolicyReferenceChecker) checkOwnerResolution(spec *models.OwnerResolutionSpec, pointer string) {
	if spec == nil {
		return
	}
	for i, path := range spec.Paths {
		base := fmt.Sprintf("%s/paths/%d", pointer, i)
		if len(path.Hops) == 0 {
			c.addDiagnostic("metadata_program.invalid_policy_reference", base+"/hops", c.schema.APIName+".accessPolicy.writeValidation.ownerResolution", "ownerResolution path requires at least one hop")
			continue
		}
		currentObject := c.schema.APIName
		terminalCount := 0
		for j := range path.Hops {
			hop := &path.Hops[j]
			hopBase := fmt.Sprintf("%s/hops/%d", base, j)
			if hop.Self {
				if j != 0 {
					c.addDiagnostic("metadata_program.invalid_policy_reference", hopBase+"/self", c.schema.APIName+".accessPolicy.writeValidation.ownerResolution", "ownerResolution self hop is only valid as the first hop")
				}
			} else {
				c.requireField(currentObject, hop.ViaField, hopBase+"/viaField", c.schema.APIName+".accessPolicy.writeValidation.ownerResolution")
				targetObject := strings.TrimSpace(hop.ToObject)
				if c.requireObject(targetObject, hopBase+"/toObject", c.schema.APIName+".accessPolicy.writeValidation.ownerResolution") {
					currentObject = targetObject
				} else {
					currentObject = ""
				}
			}
			c.checkOwnerPredicate(currentObject, hop.Gate, hopBase+"/gate/field")
			if hop.SystemWhen != nil {
				terminalCount++
				c.checkOwnerPredicate(currentObject, hop.SystemWhen, hopBase+"/systemWhen/field")
			}
		}
		if terminalCount != 1 {
			c.addDiagnosticWithTypes(
				"metadata_program.invalid_policy_reference",
				base+"/hops",
				c.schema.APIName+".accessPolicy.writeValidation.ownerResolution",
				"exactly one systemWhen",
				fmt.Sprintf("%d systemWhen predicates", terminalCount),
				fmt.Sprintf("%s ownerResolution path must declare exactly one terminal systemWhen predicate", c.schema.APIName),
			)
		}
	}
}

func (c *objectAccessPolicyReferenceChecker) checkOwnerPredicate(objectName string, pred *models.OwnerPredicate, pointer string) {
	if pred == nil {
		return
	}
	fieldName := strings.TrimSpace(pred.Field)
	if fieldName == "" {
		c.addDiagnostic("metadata_program.invalid_policy_reference", pointer, c.schema.APIName+".accessPolicy.writeValidation.ownerResolution", "ownerResolution predicate requires field")
		return
	}
	if strings.TrimSpace(objectName) == "" {
		return
	}
	c.requireField(objectName, fieldName, pointer, c.schema.APIName+".accessPolicy.writeValidation.ownerResolution")
}

func (c *objectAccessPolicyReferenceChecker) requirePolicyCapability(capability, pointer, reference string) {
	if strings.TrimSpace(capability) == "" {
		c.addDiagnostic("metadata_program.invalid_policy_reference", pointer, reference, "policy capability reference is required")
	}
}

func (c *objectAccessPolicyReferenceChecker) requireKnownValue(actual, pointer, reference string, allowed []string) {
	if knownPolicyValue(actual, allowed) {
		return
	}
	c.addDiagnosticWithTypes(
		"metadata_program.invalid_policy_reference",
		pointer,
		reference,
		strings.Join(allowed, "|"),
		strings.TrimSpace(actual),
		fmt.Sprintf("%s uses unsupported policy value %q", reference, strings.TrimSpace(actual)),
	)
}

func knownPolicyValue(actual string, allowed []string) bool {
	actual = strings.TrimSpace(actual)
	for _, value := range allowed {
		if strings.EqualFold(actual, value) {
			return true
		}
	}
	return false
}

func (c *objectAccessPolicyReferenceChecker) requireCurrentField(fieldName, pointer, reference string) bool {
	return c.requireField(c.schema.APIName, fieldName, pointer, reference)
}

func (c *objectAccessPolicyReferenceChecker) requireField(objectName, fieldName, pointer, reference string) bool {
	fieldName = strings.TrimSpace(fieldName)
	if fieldName == "" {
		c.addDiagnostic("metadata_program.invalid_policy_reference", pointer, reference, "policy field reference is required")
		return false
	}
	if c.opts.Metadata == nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, reference, "metadata reader is required to resolve policy field reference")
		return false
	}
	if _, ok, err := c.opts.Metadata.Field(c.ctx, objectName, fieldName); err != nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, reference, fmt.Sprintf("metadata reader failed while resolving policy field %s.%s: %v", objectName, fieldName, err))
		return false
	} else if !ok {
		c.addDiagnosticWithTypes("metadata_program.missing_field", pointer, reference, "field", objectName+"."+fieldName, fmt.Sprintf("policy field reference %s.%s does not exist", objectName, fieldName))
		return false
	}
	return true
}

func (c *objectAccessPolicyReferenceChecker) requireFieldType(
	objectName,
	fieldName,
	pointer,
	reference string,
	allowed []models.FieldType,
) bool {
	fieldName = strings.TrimSpace(fieldName)
	if fieldName == "" {
		c.addDiagnostic("metadata_program.invalid_policy_reference", pointer, reference, "policy field reference is required")
		return false
	}
	if c.opts.Metadata == nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, reference, "metadata reader is required to resolve policy field reference")
		return false
	}
	field, ok, err := c.opts.Metadata.Field(c.ctx, objectName, fieldName)
	if err != nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, reference, fmt.Sprintf("metadata reader failed while resolving policy field %s.%s: %v", objectName, fieldName, err))
		return false
	}
	if !ok || field == nil {
		c.addDiagnosticWithTypes("metadata_program.missing_field", pointer, reference, "field", objectName+"."+fieldName, fmt.Sprintf("policy field reference %s.%s does not exist", objectName, fieldName))
		return false
	}
	actualType := field.Type
	if field.LogicalType != nil {
		actualType = *field.LogicalType
	}
	for _, allowedType := range allowed {
		if actualType == allowedType {
			return true
		}
	}
	want := make([]string, len(allowed))
	for i, allowedType := range allowed {
		want[i] = string(allowedType)
	}
	c.addDiagnosticWithTypes(
		"metadata_program.invalid_policy_reference",
		pointer,
		reference,
		strings.Join(want, "|"),
		string(actualType),
		fmt.Sprintf("policy field reference %s.%s has type %s, want %s", objectName, fieldName, actualType, strings.Join(want, "|")),
	)
	return false
}

func (c *objectAccessPolicyReferenceChecker) requireReferenceFieldTarget(
	objectName,
	fieldName,
	targetObject,
	pointer,
	reference string,
) bool {
	field, ok, err := c.opts.Metadata.Field(c.ctx, strings.TrimSpace(objectName), strings.TrimSpace(fieldName))
	if err != nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, reference, fmt.Sprintf("metadata reader failed while resolving policy field %s.%s: %v", objectName, fieldName, err))
		return false
	}
	if !ok || field == nil {
		return false
	}
	targetObject = strings.TrimSpace(targetObject)
	for _, candidate := range field.ReferenceTo {
		if strings.EqualFold(strings.TrimSpace(candidate), targetObject) {
			return true
		}
	}
	c.addDiagnosticWithTypes(
		"metadata_program.invalid_policy_reference",
		pointer,
		reference,
		"referenceTo "+targetObject,
		strings.Join(field.ReferenceTo, "|"),
		fmt.Sprintf("policy reference field %s.%s does not target %s", objectName, fieldName, targetObject),
	)
	return false
}

func (c *objectAccessPolicyReferenceChecker) requireObject(objectName, pointer, reference string) bool {
	_, ok := c.resolveObject(objectName, pointer, reference)
	return ok
}

func (c *objectAccessPolicyReferenceChecker) resolveObject(objectName, pointer, reference string) (*models.ObjectMetadata, bool) {
	objectName = strings.TrimSpace(objectName)
	if objectName == "" {
		c.addDiagnostic("metadata_program.invalid_policy_reference", pointer, reference, "policy object reference is required")
		return nil, false
	}
	if c.opts.Metadata == nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, reference, "metadata reader is required to resolve policy object reference")
		return nil, false
	}
	if object, ok, err := c.opts.Metadata.Object(c.ctx, objectName); err != nil {
		c.addDiagnostic(DiagnosticCodeReaderError, pointer, reference, fmt.Sprintf("metadata reader failed while resolving policy object %s: %v", objectName, err))
		return nil, false
	} else if !ok {
		c.addDiagnosticWithTypes("metadata_program.missing_object", pointer, reference, "object", objectName, fmt.Sprintf("policy object reference %s does not exist", objectName))
		return nil, false
	} else {
		return object, true
	}
}

func (c *objectAccessPolicyReferenceChecker) addDiagnostic(code, pointer, reference, message string) {
	c.addDiagnosticWithTypes(code, pointer, reference, "", "", message)
}

func (c *objectAccessPolicyReferenceChecker) addDiagnosticWithTypes(code, pointer, reference, expected, actual, message string) {
	diag := Diagnostic{
		Code:          code,
		Severity:      SeverityError,
		ComponentPath: c.componentPath(),
		JSONPointer:   pointer,
		ElementName:   c.elementName(),
		OwnerPack:     strings.TrimSpace(c.opts.OwnerPack),
		Reference:     reference,
		ExpectedType:  expected,
		ActualType:    actual,
		Message:       message,
	}
	c.diagnostics = append(c.diagnostics, diag)
}

func (c *objectAccessPolicyReferenceChecker) componentPath() string {
	if path := strings.TrimSpace(c.opts.ComponentPath); path != "" {
		return path
	}
	if c.schema == nil || strings.TrimSpace(c.schema.APIName) == "" {
		return ""
	}
	return "object/" + strings.TrimSpace(c.schema.APIName) + ".object.json"
}

func (c *objectAccessPolicyReferenceChecker) elementName() string {
	if c.schema == nil {
		return ""
	}
	return strings.TrimSpace(c.schema.APIName)
}

func normalizeAccessPolicyOperation(operation string) string {
	switch strings.ToLower(strings.TrimSpace(operation)) {
	case "objectread", strings.ToLower(constants.PermRead):
		return "objectRead"
	case "objectcreate", strings.ToLower(constants.PermCreate):
		return "objectCreate"
	case "objectedit", strings.ToLower(constants.PermEdit):
		return "objectEdit"
	case "objectdelete", strings.ToLower(constants.PermDelete):
		return "objectDelete"
	case "fieldread":
		return "fieldRead"
	case "fieldedit":
		return "fieldEdit"
	default:
		return ""
	}
}

func isConcreteObjectPermissionOperation(operation string) bool {
	switch strings.TrimSpace(operation) {
	case constants.PermRead, constants.PermCreate, constants.PermEdit, constants.PermDelete:
		return true
	case constants.PermViewAllRecords, constants.PermModifyAllRecords:
		return true
	default:
		return false
	}
}

func isAllRecordObjectPermissionOperation(operation string) bool {
	switch strings.TrimSpace(operation) {
	case constants.PermViewAllRecords, constants.PermModifyAllRecords:
		return true
	default:
		return false
	}
}

func accessPolicyExprForOperation(policy *models.ObjectAccessPolicy, operation string) *models.AccessPolicyExpr {
	if policy == nil {
		return nil
	}
	switch normalizeAccessPolicyOperation(operation) {
	case "objectRead":
		return policy.ObjectRead
	case "objectCreate":
		return policy.ObjectCreate
	case "objectEdit":
		return policy.ObjectEdit
	case "objectDelete":
		return policy.ObjectDelete
	case "fieldRead":
		if policy.FieldRead != nil {
			return policy.FieldRead
		}
		if policy.IsDerived() {
			return &models.AccessPolicyExpr{Inherit: "objectRead"}
		}
	case "fieldEdit":
		if policy.FieldEdit != nil {
			return policy.FieldEdit
		}
		if policy.IsDerived() {
			return &models.AccessPolicyExpr{Inherit: "objectEdit"}
		}
	}
	return nil
}
