package models

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/nexuscrm/shared/pkg/constants"
)

// PicklistOption represents a single picklist option with value and display label.
//
// Tags is a tenant-authored classification set used by generic Flow operators
// (e.g. EntersPickTag) to decide whether a Status transition should fire
// downstream logic, without the platform knowing about the tag's semantics.
// Platform code must never compare Tags string literals; it only asks
// "does option X carry tag Y?" The business meaning lives entirely in the
// tenant spec that populates Tags.
//
// Tags and opaque option-specific extension values are preserved only for the
// structured JSON form `{"value":"X","label":"Y","tags":["t"]}`. The legacy
// `["v1","v2"]` form (see UnmarshalJSON below) cannot carry tags or extension
// values — callers that want them must author the picklist in structured form.
type PicklistOption struct {
	Value      string                     `json:"value"`
	Label      string                     `json:"label"`
	Tags       []string                   `json:"tags,omitempty"`
	Extensions map[string]json.RawMessage `json:"extensions,omitempty"`
}

// PicklistOptions is a slice of PicklistOption that supports backward-compatible
// JSON unmarshaling: both legacy ["v1","v2"] and new [{"value":"v1","label":"L1"},...] formats.
type PicklistOptions []PicklistOption

func (po PicklistOptions) Values() []string {
	out := make([]string, len(po))
	for i, o := range po {
		out[i] = o.Value
	}
	return out
}

func (po PicklistOptions) Len() int { return len(po) }

func (po *PicklistOptions) UnmarshalJSON(data []byte) error {
	if len(data) == 0 || string(data) == "null" {
		*po = nil
		return nil
	}

	// Try legacy format first: ["v1","v2"] — value == label
	var plain []string
	if err := json.Unmarshal(data, &plain); err == nil {
		opts := make(PicklistOptions, len(plain))
		for i, v := range plain {
			opts[i] = PicklistOption{Value: v, Label: v}
		}
		*po = opts
		return nil
	}

	// New structured format: [{"value":"...","label":"..."},...]
	// Tried second because json.Unmarshal into []PicklistOption silently
	// succeeds on ["v1","v2"] (producing zero-value structs), losing data.
	var structured []PicklistOption
	if err := json.Unmarshal(data, &structured); err != nil {
		return err
	}
	*po = structured
	return nil
}

// MarshalJSON always writes the new structured format.
func (po PicklistOptions) MarshalJSON() ([]byte, error) {
	if po == nil {
		return []byte("null"), nil
	}
	type alias []PicklistOption
	return json.Marshal(alias(po))
}

// PicklistOptionsFromStrings creates PicklistOptions from plain string values (value==label).
func PicklistOptionsFromStrings(vals []string) PicklistOptions {
	if vals == nil {
		return nil
	}
	opts := make(PicklistOptions, len(vals))
	for i, v := range vals {
		opts[i] = PicklistOption{Value: v, Label: v}
	}
	return opts
}

// ScopedPicklistValueSet is the resolved value set for one
// object/record-type/field tuple. Labels and dependency metadata are derived
// from the field-level catalogue; scoped storage owns only value availability
// and default selection.
type ScopedPicklistValueSet struct {
	ObjectAPIName      string                `json:"objectApiName"`
	FieldAPIName       string                `json:"fieldApiName"`
	RecordTypeID       string                `json:"recordTypeId"`
	IsMaster           bool                  `json:"isMaster"`
	Values             []ScopedPicklistValue `json:"values"`
	DefaultValue       *string               `json:"defaultValue,omitempty"`
	BusinessProcessID  *string               `json:"businessProcessId,omitempty"`
	ControllingField   *string               `json:"controllerField,omitempty"`
	PicklistDependency map[string][]string   `json:"picklistDependency,omitempty"`
}

// ScopedPicklistValue is one record-type-scoped picklist entry.
type ScopedPicklistValue struct {
	Value     string `json:"value"`
	Label     string `json:"label"`
	IsActive  bool   `json:"active"`
	IsDefault bool   `json:"defaultValue"`
	SortOrder int    `json:"sortOrder"`

	// Internal ownership facts are used for default tiebreaking and L1/L2
	// overlay behavior. They are not public wire fields.
	OwnerPack    string `json:"-"`
	IsCustomized bool   `json:"-"`
}

// FieldType is defined in pkg/constants
type FieldType = constants.SchemaFieldType

// SharingModel is defined in pkg/constants
type SharingModel = constants.SharingModel

// DeleteRule is defined in pkg/constants
type DeleteRule = constants.DeleteRule

// RollupConfig represents rollup summary field configuration
type RollupConfig struct {
	SummaryObject     string  `json:"summaryObject"`
	SummaryField      string  `json:"summaryField"`
	RelationshipField string  `json:"relationshipField"` // The lookup field on the child object
	CalcType          string  `json:"calcType"`          // COUNT, SUM, MIN, MAX, AVG
	Filter            *string `json:"filter,omitempty"`
	MaintenanceMode   string  `json:"maintenanceMode,omitempty"` // empty/default = synchronous; "eventual" opts COUNT rollups into async dirty-marker maintenance.
	// ResultType/ResultPrecision/ResultScale are resolver-owned output facts.
	// They are persisted with the canonical rollup config rather than authored
	// in application specs, so storage precision cannot leak into the public API.
	ResultType      *FieldType `json:"resultType,omitempty"`
	ResultPrecision *int       `json:"resultPrecision,omitempty"`
	ResultScale     *int       `json:"resultScale,omitempty"`
}

// FieldProjectionConfig declares an internal, metadata-owned read projection
// for a standard field whose value is derived outside regular formula
// relationship syntax. These configs are authored by L1 metadata, not by
// admin-created formula fields.
type FieldProjectionConfig struct {
	Kind           string                 `json:"kind"`
	TargetObject   string                 `json:"targetObject,omitempty"`
	SelectField    string                 `json:"selectField,omitempty"`
	Matches        []FieldProjectionMatch `json:"matches,omitempty"`
	ExcludeDeleted bool                   `json:"excludeDeleted,omitempty"`
}

type FieldProjectionMatch struct {
	TargetField string `json:"targetField"`
	SourceField string `json:"sourceField"`
}

// FieldMetadata represents field-level metadata
type FieldMetadata struct {
	ID           string     `json:"id,omitempty"`
	APIName      string     `json:"name"`
	PhysicalName string     `json:"-"` // Actual DB column name if aliased
	Label        string     `json:"label"`
	Type         FieldType  `json:"type"`
	LogicalType  *FieldType `json:"logicalType,omitempty"` // Derived from system_tables.json for standard objects
	Required     bool       `json:"required,omitempty"`
	// DefaultedOnCreate matches the Salesforce v63 describe attribute of the
	// same name: the API caller need not supply a value on create — the
	// platform synthesizes one (records-layer defaulter, autonumber, DB
	// default, …). Independent of Required (caller obligation) and
	// nillable (storage admits NULL). Set by SchemaDecorators at cache
	// build time; not persisted to _System_Field. Issue #1425.
	DefaultedOnCreate bool `json:"defaultedOnCreate,omitempty"`
	IsUnique          bool `json:"unique,omitempty"`
	IsNameField       bool `json:"nameField,omitempty"`
	// Source is the provenance classifier for this field. One of:
	// managed, standard_spec, tenant_spec, custom, bootstrap_seed, discovered.
	// Introduced by issue #1582 RFC v1.5. Governs ComputeIntrinsic Layer 1d.
	Source string `json:"source,omitempty"`
	// IsCompound marks fields whose value is derived from ComposedFrom members
	// (e.g., User.Name from FirstName+LastName). Read-only on write per SF parity.
	// Introduced by issue #1582 RFC v1.5. Governs ComputeIntrinsic Layer 1e.
	IsCompound bool `json:"isCompound,omitempty"`
	// ComposedFrom lists the member field ApiNames a compound parent derives from.
	// Nil/empty when IsCompound is false. Introduced by issue #1582 RFC v1.5.
	ComposedFrom        []string               `json:"composedFrom,omitempty"`
	Options             PicklistOptions        `json:"options,omitempty"`
	ReferenceTo         []string               `json:"referenceTo,omitempty"`         // Supports polymorphic reference targets.
	WritableReferenceTo []string               `json:"writableReferenceTo,omitempty"` // Optional user-context writable subset of ReferenceTo.
	IsPolymorphic       bool                   `json:"polymorphic,omitempty"`         // True for multi-target or transitional polymorphic references.
	DeleteRule          *DeleteRule            `json:"deleteRule,omitempty"`
	IsSystem            bool                   `json:"system,omitempty"`
	Createable          bool                   `json:"createable"`
	Updateable          bool                   `json:"updateable"`
	Formula             *string                `json:"formula,omitempty"`
	ReturnType          *FieldType             `json:"returnType,omitempty"`
	FormulaMode         *string                `json:"formulaMode,omitempty"` // "active" = view-computed; "frozen" = write-time engine snapshot.
	FormulaStorageMode  *string                `json:"formulaStorageMode,omitempty"`
	ProjectionConfig    *FieldProjectionConfig `json:"projection,omitempty"`
	DefaultValue        *string                `json:"defaultValue,omitempty"`
	HelpText            *string                `json:"inlineHelpText,omitempty"`
	TrackHistory        bool                   `json:"trackHistory,omitempty"`
	MinValue            *float64               `json:"minValue,omitempty"`
	MaxValue            *float64               `json:"maxValue,omitempty"`
	MinLength           *int                   `json:"minLength,omitempty"`
	MaxLength           *int                   `json:"maxLength,omitempty"`
	Regex               *string                `json:"regex,omitempty"`
	RegexMessage        *string                `json:"regexMessage,omitempty"`
	Validator           *string                `json:"validator,omitempty"`
	ValidatorConfig     *string                `json:"validatorConfig,omitempty"` // JSON config for the validator
	ControllingField    *string                `json:"controllingField,omitempty"`
	PicklistDependency  map[string][]string    `json:"picklistDependency,omitempty"`
	RollupConfig        *RollupConfig          `json:"rollupConfig,omitempty"`
	IsMasterDetail      bool                   `json:"masterDetail,omitempty"`
	RelationshipName    *string                `json:"relationshipName,omitempty"`
	// ChildRelationshipName is the child-side (typically plural) name that
	// Salesforce publishes as describe(parent).childRelationships[*].relationshipName,
	// e.g. "Contacts" on Contact.AccountId, "ChildAccounts" on Account.ParentId.
	// NULL means the substrate has not yet been populated for this FK (see
	// issue #153). Runtime read paths that need this value (parent-child SOQL
	// subqueries, /sobjects/:name/describe child-relationship emit) must
	// fail-closed when it is NULL on a candidate; they must not silently
	// fall back to `childAPIName + "s"` inference.
	ChildRelationshipName *string `json:"childRelationshipName,omitempty"`
	Precision             *int    `json:"precision,omitempty"`
	Scale                 *int    `json:"scale,omitempty"`

	// IsVirtual marks fields that have no physical column in the object's
	// storage table. Physical SQL paths (INSERT/UPDATE/SELECT column lists,
	// repository column helpers) must skip these via models.PersistableFields.
	// Logical paths (validation, canonicalization, FLS, describe) continue
	// to see them via schema.Fields or models.LogicalFields.
	//
	// Any field with IsVirtual=true must declare VirtualKind; the kind
	// selects the handler responsible for intercepting writes and
	// projecting reads for that field.
	IsVirtual bool `json:"isVirtual,omitempty"`

	// VirtualKind identifies the handler class for a virtual field. See
	// constants.VirtualFieldKind for the enumerated values. Empty for
	// non-virtual fields.
	VirtualKind string `json:"virtualKind,omitempty"`

	// VirtualSourceAPIName is the kind-specific key that the handler uses
	// to translate between the field's wire-shape APIName and the
	// underlying data. Example: for a PermissionSet virtual field with
	// APIName "PermissionsViewSetup" and VirtualKind "SystemPermission",
	// VirtualSourceAPIName is "ViewSetup" — the catalog key without the
	// "Permissions" prefix. Downstream handlers read this directly; they
	// must not string-trim APIName themselves.
	VirtualSourceAPIName string `json:"virtualSourceApiName,omitempty"`

	// OwnerKind/OwnerPack are field fact ownership discriminators used by the
	// pack install lifecycle. They are not override flags; IsCustomized remains
	// the marker for admin edits to an existing row.
	OwnerKind string  `json:"ownerKind,omitempty"`
	OwnerPack *string `json:"ownerPack,omitempty"`

	// WriteProtectionPolicy narrows generic field-write capabilities. The
	// selected policy determines whether trusted platform contexts may bypass
	// the records-layer gate. Empty means ordinary field capability rules apply.
	WriteProtectionPolicy string `json:"writeProtectionPolicy,omitempty"`

	// ReadProtectionPolicy marks fields that are never readable through generic
	// record/query paths. Empty means ordinary field capability rules apply.
	ReadProtectionPolicy string `json:"readProtectionPolicy,omitempty"`

	// SiblingMaxParentField configures create-time MAX(target)+1 defaulting
	// scoped by the named parent lookup field. Explicit caller values win.
	SiblingMaxParentField *string `json:"siblingMaxParentField,omitempty"`

	// GeneratedForPolymorphicReference is transient registration context for
	// server-managed discriminator fields. It is derived from the companion
	// polymorphic reference field and is not part of the public metadata API.
	GeneratedForPolymorphicReference string `json:"-"`
}

// ChildCloneRule declares a post-insert side effect: when a record of the
// containing object is created with ParentLookupField populated, every
// SourceObject row linked via SourceLookupField to the referenced parent is
// cloned into a TargetObject row linked via TargetLookupField to the newly
// created record. Field values are carried over per FieldMappings (flat
// key:source-key pairs, no dotted paths). Optional SourceIdField stamps the
// source row's Id onto the clone as a back-reference. HydrateSource, when
// true, retrieves each source row through the normal Retrieve path so
// formula/computed fields are populated before copy.
//
// Retained as a compatibility/install primitive on
// `_System_Object.ChildCloneRules`; greenfield orgs leave the column NULL and
// the generic post-insert hook in features/records/persistence_insert.go
// no-ops. New create-from-source authoring is Flow-owned under issue #979:
// use Flow record lookup / loop / recordCreate plus _System_ObjectFieldMapping
// for flat copies and formulas / Flow expressions for dynamic inherit. The
// executor is shared with the REST action handler's retired workflow.childCopy
// surface via features/records/child_clone.go.
type ChildCloneRule struct {
	ParentLookupField string            `json:"parentLookupField"`
	SourceObject      string            `json:"sourceObject"`
	SourceLookupField string            `json:"sourceLookupField"`
	SourceIdField     string            `json:"sourceIdField,omitempty"`
	HydrateSource     bool              `json:"hydrateSource,omitempty"`
	TargetObject      string            `json:"targetObject"`
	TargetLookupField string            `json:"targetLookupField"`
	FieldMappings     map[string]string `json:"fieldMappings"`
}

// DefaultChildRow declares a post-insert constant child-row side effect:
// when a record of the containing object is created, insert one row into
// TargetObject with TargetLookupField stamped to the new parent Id and
// FieldValues copied as literal child-field values.
//
// This is L1/L2 metadata, not runtime policy. The generic insert path owns the
// mechanism; application packs own the declarations. FieldValues must use flat
// field API names only; runtime stamps TargetLookupField after copying so the
// parent Id always wins over an authored duplicate.
type DefaultChildRow struct {
	TargetObject      string  `json:"targetObject"`
	TargetLookupField string  `json:"targetLookupField"`
	FieldValues       SObject `json:"fieldValues"`
}

// ObjectAccessPolicy declares metadata-authored object/field access as a
// composition of canonical object/system permission checks.
type ObjectAccessPolicy struct {
	Mode              string                               `json:"mode,omitempty"`
	ObjectRead        *AccessPolicyExpr                    `json:"objectRead,omitempty"`
	ObjectCreate      *AccessPolicyExpr                    `json:"objectCreate,omitempty"`
	ObjectEdit        *AccessPolicyExpr                    `json:"objectEdit,omitempty"`
	ObjectDelete      *AccessPolicyExpr                    `json:"objectDelete,omitempty"`
	FieldRead         *AccessPolicyExpr                    `json:"fieldRead,omitempty"`
	FieldEdit         *AccessPolicyExpr                    `json:"fieldEdit,omitempty"`
	RowRead           *RowReadPolicy                       `json:"rowRead,omitempty"`
	RecordMutation    *RecordMutationAccessPolicy          `json:"recordMutation,omitempty"`
	Lifecycle         *RecordLifecycleAccessPolicy         `json:"lifecycle,omitempty"`
	CacheInvalidation *RecordCacheInvalidationAccessPolicy `json:"cacheInvalidation,omitempty"`
	WriteValidation   *RecordWriteValidationAccessPolicy   `json:"writeValidation,omitempty"`
}

// IsDerived reports whether the policy declares a derived/composite access
// surface that should not own self ObjectPermissions / FieldPermissions rows.
func (p *ObjectAccessPolicy) IsDerived() bool {
	return p != nil && strings.EqualFold(strings.TrimSpace(p.Mode), "derived")
}

// AccessPolicyExpr is a recursive predicate tree over canonical permissions.
type AccessPolicyExpr struct {
	Kind      string             `json:"kind,omitempty"`
	Object    string             `json:"object,omitempty"`
	Operation string             `json:"operation,omitempty"`
	APIName   string             `json:"apiName,omitempty"`
	Field     string             `json:"field,omitempty"`
	AllOf     []AccessPolicyExpr `json:"allOf,omitempty"`
	AnyOf     []AccessPolicyExpr `json:"anyOf,omitempty"`
	Not       *AccessPolicyExpr  `json:"not,omitempty"`
	Inherit   string             `json:"inherit,omitempty"`
}

// RowReadPolicy declares metadata-owned row visibility that can be compiled
// into query RLS without object-specific runtime branches.
type RowReadPolicy struct {
	Kind                    string                     `json:"kind,omitempty"`
	TargetObjectField       string                     `json:"targetObjectField,omitempty"`
	TargetRecordField       string                     `json:"targetRecordField,omitempty"`
	TargetAssociations      []RowReadTargetAssociation `json:"targetAssociations,omitempty"`
	IncludeSourceVisibility bool                       `json:"includeSourceVisibility,omitempty"`
	OwnerField              string                     `json:"ownerField,omitempty"`
	VisibilityField         string                     `json:"visibilityField,omitempty"`
	OwnerVisibleValues      []string                   `json:"ownerVisibleValues,omitempty"`
	PublicVisibleValues     []string                   `json:"publicVisibleValues,omitempty"`
}

// RowReadTargetAssociation declares one polymorphic target slot. Multiple
// present slots must all be readable; the possible target object types inside
// one slot are alternatives.
type RowReadTargetAssociation struct {
	TargetObjectField string `json:"targetObjectField,omitempty"`
	TargetRecordField string `json:"targetRecordField,omitempty"`
}

// TargetRecordReadAssociations is the sole compatibility and runtime
// structural-validation boundary between the legacy single-slot shape and the
// grouped targetRecordRead policy. The bool is false when the declaration is
// incomplete or mixes the two shapes; callers must disable the target-derived
// grant as a whole instead of retaining only the valid associations.
func (p *RowReadPolicy) TargetRecordReadAssociations() ([]RowReadTargetAssociation, bool) {
	if p == nil {
		return nil, false
	}
	if len(p.TargetAssociations) > 0 {
		if strings.TrimSpace(p.TargetObjectField) != "" || strings.TrimSpace(p.TargetRecordField) != "" {
			return nil, false
		}
		associations := make([]RowReadTargetAssociation, len(p.TargetAssociations))
		for i, association := range p.TargetAssociations {
			associations[i] = RowReadTargetAssociation{
				TargetObjectField: strings.TrimSpace(association.TargetObjectField),
				TargetRecordField: strings.TrimSpace(association.TargetRecordField),
			}
			if associations[i].TargetObjectField == "" || associations[i].TargetRecordField == "" {
				return nil, false
			}
		}
		return associations, true
	}
	targetObjectField := strings.TrimSpace(p.TargetObjectField)
	targetRecordField := strings.TrimSpace(p.TargetRecordField)
	if targetObjectField == "" || targetRecordField == "" {
		return nil, false
	}
	return []RowReadTargetAssociation{{
		TargetObjectField: targetObjectField,
		TargetRecordField: targetRecordField,
	}}, true
}

// RecordLifecycleAccessPolicy declares object-owned lifecycle hook bindings.
// The records engine dispatches app-provided hooks by these capability tokens;
// app packs own which objects carry which capabilities at which lifecycle
// phases.
type RecordLifecycleAccessPolicy struct {
	Hooks                       []RecordLifecycleHookPolicy                 `json:"hooks,omitempty"`
	AssignmentPolicyEligibility *AssignmentPolicyEligibilityLifecyclePolicy `json:"assignmentPolicyEligibility,omitempty"`
}

type RecordLifecycleHookPolicy struct {
	Phase      string `json:"phase,omitempty"`
	Capability string `json:"capability,omitempty"`
}

// AssignmentPolicyEligibilityLifecyclePolicy lowers an app-authored record
// shape into the product-neutral assignment-policy eligibility transition.
// L0 interprets this declaration without knowing the source object or field
// names. Conditions are a conjunction of boolean field comparisons. Selector
// values come either directly from the source record or from one referenced
// record locked in the surrounding Records-owned transaction.
type AssignmentPolicyEligibilityLifecyclePolicy struct {
	Role                 AssignmentPolicyEligibilitySourceRole      `json:"role"`
	SubjectIDField       string                                     `json:"subjectIdField,omitempty"`
	EnabledWhen          []AssignmentPolicyEligibilityBoolCondition `json:"enabledWhen"`
	Selector             AssignmentPolicyEligibilitySelector        `json:"selector"`
	ReconcileAfterUpdate bool                                       `json:"reconcileAfterUpdate,omitempty"`

	// ResolvedSource* is Catalog-compiled execution metadata. App packs author
	// only the logical role and field identities above.
	ResolvedSourceTable     string `json:"resolvedSourceTable,omitempty"`
	ResolvedSourceIDColumn  string `json:"resolvedSourceIdColumn,omitempty"`
	ResolvedSubjectIDColumn string `json:"resolvedSubjectIdColumn,omitempty"`
}

type AssignmentPolicyEligibilitySourceRole string

const (
	// AssignmentPolicyEligibilitySelectorAuthority identifies the source whose
	// enabled direct selector determines whether an authored selector is valid.
	AssignmentPolicyEligibilitySelectorAuthority AssignmentPolicyEligibilitySourceRole = "selector_authority"
	// AssignmentPolicyEligibilitySubjectPopulation identifies a source whose
	// enabled rows contribute opaque subject IDs for a referenced selector.
	AssignmentPolicyEligibilitySubjectPopulation AssignmentPolicyEligibilitySourceRole = "subject_population"
)

// CanonicalAssignmentPolicyEligibilitySourceRole is the single normalization
// path for the metadata-authored role. Validation and Catalog compilation must
// consume the same value so a declaration cannot pass one boundary and fail at
// the next solely because of case or surrounding whitespace.
func CanonicalAssignmentPolicyEligibilitySourceRole(role AssignmentPolicyEligibilitySourceRole) AssignmentPolicyEligibilitySourceRole {
	return AssignmentPolicyEligibilitySourceRole(strings.ToLower(strings.TrimSpace(string(role))))
}

type AssignmentPolicyEligibilityBoolCondition struct {
	Field          string `json:"field"`
	Equals         bool   `json:"equals"`
	ResolvedColumn string `json:"resolvedColumn,omitempty"`
}

type AssignmentPolicyEligibilitySelector struct {
	Field               string                                        `json:"field,omitempty"`
	ResolvedFieldColumn string                                        `json:"resolvedFieldColumn,omitempty"`
	Reference           *AssignmentPolicyEligibilityReferenceSelector `json:"reference,omitempty"`
}

type AssignmentPolicyEligibilityReferenceSelector struct {
	SourceField  string `json:"sourceField"`
	TargetObject string `json:"targetObject"`
	TargetField  string `json:"targetField"`

	// ResolvedTarget* is Catalog-compiled execution metadata. App packs author
	// only the logical source/target fields above; the Catalog projection owns
	// the physical table/column mapping consumed by the app-blind Runtime.
	ResolvedTargetTable       string `json:"resolvedTargetTable,omitempty"`
	ResolvedTargetIDColumn    string `json:"resolvedTargetIdColumn,omitempty"`
	ResolvedTargetFieldColumn string `json:"resolvedTargetFieldColumn,omitempty"`
	ResolvedSourceFieldColumn string `json:"resolvedSourceFieldColumn,omitempty"`
}

// RecordCacheInvalidationAccessPolicy declares object-owned cache invalidation
// bindings. The records engine owns dispatch and application of invalidation
// plans; object metadata owns which writes emit which capabilities.
type RecordCacheInvalidationAccessPolicy struct {
	Handlers []RecordCacheInvalidationPolicy `json:"handlers,omitempty"`
}

type RecordCacheInvalidationPolicy struct {
	Capability string `json:"capability,omitempty"`
}

// RecordWriteValidationAccessPolicy declares object-owned write validator
// bindings. The records engine owns phase dispatch; app packs own which object
// metadata opts into which validation capabilities.
type RecordWriteValidationAccessPolicy struct {
	Validators              []RecordWriteValidationPolicy          `json:"validators,omitempty"`
	Canonicalizers          []RecordWriteCanonicalizationPolicy    `json:"canonicalizers,omitempty"`
	RequiredValidationRules []RecordWriteValidationRuleRequirement `json:"requiredValidationRules,omitempty"`
	OwnerResolution         *OwnerResolutionSpec                   `json:"ownerResolution,omitempty"`
}

type RecordWriteValidationPolicy struct {
	Phase      string `json:"phase,omitempty"`
	Capability string `json:"capability,omitempty"`
}

type RecordWriteCanonicalizationPolicy struct {
	Phase      string `json:"phase,omitempty"`
	Capability string `json:"capability,omitempty"`
}

// RecordWriteValidationRuleRequirement declares source-backed ValidationRule
// invariants that must remain active before a write phase can rely on
// ValidationRule metadata for a non-skippable contract.
type RecordWriteValidationRuleRequirement struct {
	Phase         string `json:"phase,omitempty"`
	ObjectAPIName string `json:"objectApiName,omitempty"`
	Name          string `json:"name,omitempty"`
	OwnerPack     string `json:"ownerPack,omitempty"`
}

// OwnerResolutionSpec is metadata-authored: how the records engine decides a
// record is owned by a system-managed entity, so the write-validation engine
// can reject mutations of system-managed records. App packs author it per
// object; nil means the object does not opt into system-managed-owner
// immutability. The engine interprets the spec generically; it carries no
// object-specific branches — every object/table name lives in the spec.
type OwnerResolutionSpec struct {
	// Paths are OR'd: the record is system-managed if any path resolves to a
	// system-managed owner.
	Paths []OwnerResolutionPath `json:"paths,omitempty"`
}

// OwnerResolutionPath is one FK-chain to an owner. Hops are evaluated in order;
// the path matches only if every hop holds up to and including the terminal hop.
type OwnerResolutionPath struct {
	Hops []OwnerResolutionHop `json:"hops,omitempty"`
}

// OwnerResolutionHop is one step in an ownership path.
type OwnerResolutionHop struct {
	// Self evaluates the record itself rather than a referenced record. Only
	// valid as the first hop (e.g. a PermissionSet's own IsOwnedByProfile flag).
	Self bool `json:"self,omitempty"`
	// ViaField is the FK field on the current record holding the referenced Id.
	ViaField string `json:"viaField,omitempty"`
	// ToObject is the referenced object. It is spec-pinned so polymorphic FKs
	// (e.g. a ParentId that may reference a PermissionSet or a Profile) resolve
	// without relying on ReferenceTo metadata: the spec carries one path per
	// candidate target.
	ToObject string `json:"toObject,omitempty"`
	// Gate requires this predicate on the resolved record before the path
	// continues to the next hop. A failed gate makes the path not match.
	Gate *OwnerPredicate `json:"gate,omitempty"`
	// SystemWhen terminates the path: the owner is system-managed iff this
	// predicate holds. Exactly one hop per path carries SystemWhen.
	SystemWhen *OwnerPredicate `json:"systemWhen,omitempty"`
}

// OwnerPredicate is a boolean-truthy predicate over a single field, matching
// the records engine's isGuardFieldTruthy semantics.
type OwnerPredicate struct {
	Field string `json:"field,omitempty"`
	Is    bool   `json:"is,omitempty"`
}

const (
	RecordMutationSurfaceSingleCreate     = "singleCreate"
	RecordMutationSurfaceSingleUpdate     = "singleUpdate"
	RecordMutationSurfaceSingleDelete     = "singleDelete"
	RecordMutationSurfaceExternalIDUpsert = "externalIDUpsert"
	RecordMutationSurfaceCollectionCreate = "collectionCreate"
	RecordMutationSurfaceCollectionUpdate = "collectionUpdate"
	RecordMutationSurfaceCollectionDelete = "collectionDelete"
	RecordMutationSurfaceCollectionUpsert = "collectionUpsert"
	RecordMutationSurfaceTreeCreate       = "treeCreate"
)

// RecordMutationAccessPolicy declares metadata-owned closures for public record
// mutation surfaces. The generic runtime evaluates the policy; app packs own the
// object-specific denials and user-facing messages.
type RecordMutationAccessPolicy struct {
	Deny []RecordMutationDenial `json:"deny,omitempty"`
}

type RecordMutationDenial struct {
	Surfaces  []string                 `json:"surfaces,omitempty"`
	Field     string                   `json:"field,omitempty"`
	Message   string                   `json:"message,omitempty"`
	ErrorCode string                   `json:"errorCode,omitempty"`
	When      *RecordMutationCondition `json:"when,omitempty"`
}

type RecordMutationCondition struct {
	FieldEquals        *RecordMutationFieldEqualsCondition   `json:"fieldEquals,omitempty"`
	FieldPresent       *RecordMutationFieldPresentCondition  `json:"fieldPresent,omitempty"`
	FieldPresentEquals *RecordMutationFieldEqualsCondition   `json:"fieldPresentEquals,omitempty"`
	RelatedExists      *RecordMutationRelatedExistsCondition `json:"relatedExists,omitempty"`
	AllOf              []RecordMutationCondition             `json:"allOf,omitempty"`
}

type RecordMutationFieldEqualsCondition struct {
	Field           string `json:"field,omitempty"`
	Value           string `json:"value,omitempty"`
	CaseInsensitive bool   `json:"caseInsensitive,omitempty"`
}

type RecordMutationFieldPresentCondition struct {
	Field string `json:"field,omitempty"`
}

type RecordMutationRelatedExistsCondition struct {
	ObjectAPIName  string                               `json:"objectApiName,omitempty"`
	ParentField    string                               `json:"parentField,omitempty"`
	Match          []RecordMutationFieldEqualsCondition `json:"match,omitempty"`
	IncludeDeleted bool                                 `json:"includeDeleted,omitempty"`
}

// ExclusiveSiblingField declares that when FieldAPIName is true, sibling rows
// that share PartitionFieldAPIName must be reset to false by the records
// runtime. The contract is generic metadata; app packs decide which object
// fields use it.
type ExclusiveSiblingField struct {
	FieldAPIName          string `json:"fieldApiName"`
	PartitionFieldAPIName string `json:"partitionFieldApiName"`
}

// ObjectMetadata represents object-level metadata
type ObjectMetadata struct {
	ID                               string                    `json:"id,omitempty"`
	AppID                            *string                   `json:"appId,omitempty"`
	APIName                          string                    `json:"name"`
	KeyPrefix                        *string                   `json:"keyPrefix,omitempty"`
	Label                            string                    `json:"label"`
	PluralLabel                      string                    `json:"labelPlural"`
	Icon                             string                    `json:"icon,omitempty"`
	Description                      *string                   `json:"description,omitempty"`
	IsSystem                         bool                      `json:"system,omitempty"`
	IsCustom                         bool                      `json:"custom,omitempty"`
	CustomSetting                    bool                      `json:"customSetting,omitempty"`
	TableType                        constants.TableType       `json:"tableType,omitempty"`
	RequiredWritePermission          string                    `json:"requiredWritePermission,omitempty"`
	RequiredReadPermission           string                    `json:"requiredReadPermission,omitempty"`
	AccessPolicy                     *ObjectAccessPolicy       `json:"accessPolicy,omitempty"`
	DeleteMode                       string                    `json:"deleteMode,omitempty"`
	ThemeColor                       *string                   `json:"themeColor,omitempty"`
	SharingModel                     SharingModel              `json:"sharingModel"`
	SharingParentObject              *string                   `json:"sharingParentObject,omitempty"`
	SharingParentField               *string                   `json:"sharingParentField,omitempty"`
	IsCustomized                     bool                      `json:"isCustomized,omitempty"`
	EnableHierarchySharing           bool                      `json:"enableHierarchySharing"`
	Fields                           []FieldMetadata           `json:"fields"`
	DefaultListView                  *string                   `json:"defaultListView,omitempty"`
	KanbanGroupBy                    *string                   `json:"kanbanGroupBy,omitempty"`
	KanbanSummaryField               *string                   `json:"kanbanSummaryField,omitempty"`
	ListFields                       []string                  `json:"listFields,omitempty"`
	ListFieldsCustomized             bool                      `json:"listFieldsCustomized,omitempty"`
	ReadSource                       *ObjectReadSourceConfig   `json:"readSource,omitempty"`
	ChildCloneRules                  []ChildCloneRule          `json:"childCloneRules,omitempty"`
	ChildCloneRulesCustomized        bool                      `json:"childCloneRulesCustomized,omitempty"`
	ExclusiveSiblingFields           []ExclusiveSiblingField   `json:"exclusiveSiblingFields,omitempty"`
	ExclusiveSiblingFieldsCustomized bool                      `json:"exclusiveSiblingFieldsCustomized,omitempty"`
	DefaultChildRows                 []DefaultChildRow         `json:"defaultChildRows,omitempty"`
	DefaultChildRowsCustomized       bool                      `json:"defaultChildRowsCustomized,omitempty"`
	Searchable                       bool                      `json:"searchable"`
	PathField                        *string                   `json:"pathField,omitempty"`          // Field to use for Path component (must be Picklist)
	DisplayNameField                 *string                   `json:"displayNameField,omitempty"`   // Field whose expanded relationship data provides the display name in list views
	LockRule                         *string                   `json:"lockRule,omitempty"`           // Formula expression that, when true, marks the record locked (evaluated by Sub-PR 2)
	LockRuleCustomized               bool                      `json:"lockRuleCustomized,omitempty"` // Column-scoped override bit; admin explicitly authored LockRule when true
	FieldsMap                        map[string]*FieldMetadata `json:"-"`                            // Runtime lookup map for O(1) access
}

// UsesDerivedAccessPolicy reports whether the object's access surface is fully
// derived from other canonical permissions instead of self OP/FP rows.
func (o *ObjectMetadata) UsesDerivedAccessPolicy() bool {
	return o != nil && o.AccessPolicy != nil && o.AccessPolicy.IsDerived()
}

func (o *ObjectMetadata) RowReadPolicy() *RowReadPolicy {
	if o == nil || o.AccessPolicy == nil {
		return nil
	}
	return o.AccessPolicy.RowRead
}

func (o *ObjectMetadata) RecordMutationDenial(surface string, record SObject) *RecordMutationDenial {
	if o == nil || o.AccessPolicy == nil || o.AccessPolicy.RecordMutation == nil {
		return nil
	}
	return o.AccessPolicy.RecordMutation.denial(surface, record, o)
}

func (o *ObjectMetadata) RecordMutationMayNeedExisting(surface string, record SObject) bool {
	if o == nil || o.AccessPolicy == nil || o.AccessPolicy.RecordMutation == nil {
		return false
	}
	return o.AccessPolicy.RecordMutation.MayNeedExisting(surface, record)
}

func (o *ObjectMetadata) RecordLifecycleCapabilitiesForPhase(phase string) []string {
	if o == nil || o.AccessPolicy == nil || o.AccessPolicy.Lifecycle == nil {
		return nil
	}
	phase = strings.TrimSpace(phase)
	if phase == "" {
		return nil
	}
	out := make([]string, 0, len(o.AccessPolicy.Lifecycle.Hooks))
	for _, hook := range o.AccessPolicy.Lifecycle.Hooks {
		if !strings.EqualFold(strings.TrimSpace(hook.Phase), phase) {
			continue
		}
		capability := strings.TrimSpace(hook.Capability)
		if capability != "" {
			out = append(out, capability)
		}
	}
	return out
}

func (o *ObjectMetadata) RecordCacheInvalidationCapabilities() []string {
	if o == nil || o.AccessPolicy == nil || o.AccessPolicy.CacheInvalidation == nil {
		return nil
	}
	out := make([]string, 0, len(o.AccessPolicy.CacheInvalidation.Handlers))
	for _, handler := range o.AccessPolicy.CacheInvalidation.Handlers {
		capability := strings.TrimSpace(handler.Capability)
		if capability != "" {
			out = append(out, capability)
		}
	}
	return out
}

func (o *ObjectMetadata) RecordWriteValidationCapabilitiesForPhase(phase string) []string {
	if o == nil || o.AccessPolicy == nil || o.AccessPolicy.WriteValidation == nil {
		return nil
	}
	phase = strings.TrimSpace(phase)
	if phase == "" {
		return nil
	}
	out := make([]string, 0, len(o.AccessPolicy.WriteValidation.Validators))
	for _, validator := range o.AccessPolicy.WriteValidation.Validators {
		if !strings.EqualFold(strings.TrimSpace(validator.Phase), phase) {
			continue
		}
		capability := strings.TrimSpace(validator.Capability)
		if capability != "" {
			out = append(out, capability)
		}
	}
	return out
}

func (o *ObjectMetadata) RecordWriteCanonicalizationCapabilitiesForPhase(phase string) []string {
	if o == nil || o.AccessPolicy == nil || o.AccessPolicy.WriteValidation == nil {
		return nil
	}
	phase = strings.TrimSpace(phase)
	if phase == "" {
		return nil
	}
	out := make([]string, 0, len(o.AccessPolicy.WriteValidation.Canonicalizers))
	for _, canonicalizer := range o.AccessPolicy.WriteValidation.Canonicalizers {
		if !strings.EqualFold(strings.TrimSpace(canonicalizer.Phase), phase) {
			continue
		}
		capability := strings.TrimSpace(canonicalizer.Capability)
		if capability != "" {
			out = append(out, capability)
		}
	}
	return out
}

func (o *ObjectMetadata) RecordWriteValidationRequiredValidationRulesForPhase(phase string) []RecordWriteValidationRuleRequirement {
	if o == nil || o.AccessPolicy == nil || o.AccessPolicy.WriteValidation == nil {
		return nil
	}
	phase = strings.ToLower(strings.TrimSpace(phase))
	if phase == "" {
		return nil
	}
	out := make([]RecordWriteValidationRuleRequirement, 0, len(o.AccessPolicy.WriteValidation.RequiredValidationRules))
	for _, req := range o.AccessPolicy.WriteValidation.RequiredValidationRules {
		reqPhase := strings.ToLower(strings.TrimSpace(req.Phase))
		if reqPhase == "" || reqPhase != phase {
			continue
		}
		name := strings.TrimSpace(req.Name)
		if name == "" {
			continue
		}
		objectAPIName := strings.TrimSpace(req.ObjectAPIName)
		if objectAPIName == "" {
			objectAPIName = strings.TrimSpace(o.APIName)
		}
		if objectAPIName == "" {
			continue
		}
		out = append(out, RecordWriteValidationRuleRequirement{
			Phase:         reqPhase,
			ObjectAPIName: objectAPIName,
			Name:          name,
			OwnerPack:     strings.TrimSpace(req.OwnerPack),
		})
	}
	return out
}

func (o *ObjectMetadata) RecordWriteValidationMissingRequiredValidationRulesForPhase(phase string, rules []*ValidationRule) []RecordWriteValidationRuleRequirement {
	required := o.RecordWriteValidationRequiredValidationRulesForPhase(phase)
	if len(required) == 0 {
		return nil
	}
	live := make(map[string]struct{}, len(rules))
	for _, rule := range rules {
		if rule == nil || !rule.Active || rule.IsCustomized {
			continue
		}
		objectAPIName := strings.TrimSpace(rule.ObjectAPIName)
		name := strings.TrimSpace(rule.Name)
		ownerPack := ""
		if rule.OwnerPack != nil {
			ownerPack = strings.TrimSpace(*rule.OwnerPack)
		}
		if objectAPIName == "" || name == "" || ownerPack == "" {
			continue
		}
		live[recordWriteValidationRuleRequirementKey(objectAPIName, name, ownerPack)] = struct{}{}
	}
	missing := make([]RecordWriteValidationRuleRequirement, 0)
	for _, req := range required {
		if _, ok := live[recordWriteValidationRuleRequirementKey(req.ObjectAPIName, req.Name, req.OwnerPack)]; !ok {
			missing = append(missing, req)
		}
	}
	return missing
}

func recordWriteValidationRuleRequirementKey(objectAPIName, name, ownerPack string) string {
	return strings.ToLower(strings.TrimSpace(objectAPIName)) + "\x00" +
		strings.ToLower(strings.TrimSpace(name)) + "\x00" +
		strings.ToLower(strings.TrimSpace(ownerPack))
}

func (p *RecordMutationAccessPolicy) Denial(surface string, record SObject) *RecordMutationDenial {
	return p.denial(surface, record, nil)
}

func (p *RecordMutationAccessPolicy) denial(surface string, record SObject, schema *ObjectMetadata) *RecordMutationDenial {
	if p == nil {
		return nil
	}
	for i := range p.Deny {
		denial := &p.Deny[i]
		if denial.matchesWithSchema(surface, record, schema) {
			return denial
		}
	}
	return nil
}

func (p *RecordMutationAccessPolicy) MayNeedExisting(surface string, record SObject) bool {
	if p == nil {
		return false
	}
	for i := range p.Deny {
		denial := &p.Deny[i]
		if denial.appliesToSurface(surface) && denial.conditionMayNeedExisting(record) {
			return true
		}
	}
	return false
}

func (d *RecordMutationDenial) matchesWithSchema(surface string, record SObject, schema *ObjectMetadata) bool {
	if d == nil || !d.appliesToSurface(surface) {
		return false
	}
	if d.When == nil {
		return true
	}
	return d.When.matchesWithSchema(record, schema)
}

func (d *RecordMutationDenial) appliesToSurface(surface string) bool {
	if d == nil {
		return false
	}
	surface = strings.TrimSpace(surface)
	if len(d.Surfaces) == 0 {
		return surface != ""
	}
	for _, candidate := range d.Surfaces {
		if strings.EqualFold(strings.TrimSpace(candidate), surface) {
			return true
		}
	}
	return false
}

func (d *RecordMutationDenial) conditionMayNeedExisting(record SObject) bool {
	if d == nil || d.When == nil {
		return false
	}
	return d.When.mayNeedExisting(record)
}

func (c *RecordMutationCondition) matchesWithSchema(record SObject, schema *ObjectMetadata) bool {
	if c == nil {
		return true
	}
	if len(c.AllOf) > 0 {
		for i := range c.AllOf {
			if !c.AllOf[i].matchesWithSchema(record, schema) {
				return false
			}
		}
		return true
	}
	if c.FieldEquals != nil {
		return c.FieldEquals.matchesWithSchema(record, schema)
	}
	if c.FieldPresent != nil {
		return c.FieldPresent.matches(record)
	}
	if c.FieldPresentEquals != nil {
		return c.FieldPresentEquals.matchesPresentWithSchema(record, schema)
	}
	if c.RelatedExists != nil {
		// Related-record conditions require an evaluator with query access.
		// The pure metadata model fails closed so routes that have not been
		// wired through the evaluator do not silently open protected surfaces.
		return true
	}
	// Unknown condition shapes must fail closed. Returning true applies the
	// denial instead of silently opening a protected write surface.
	return true
}

func (c *RecordMutationCondition) mayNeedExisting(record SObject) bool {
	if c == nil {
		return false
	}
	if len(c.AllOf) > 0 {
		for i := range c.AllOf {
			if c.AllOf[i].mayNeedExisting(record) {
				return true
			}
		}
		return false
	}
	if c.FieldEquals != nil {
		return c.FieldEquals.mayNeedExisting(record)
	}
	if c.FieldPresent != nil || c.FieldPresentEquals != nil || c.RelatedExists != nil {
		return false
	}
	return false
}

func (c *RecordMutationFieldEqualsCondition) matchesWithSchema(record SObject, schema *ObjectMetadata) bool {
	if c == nil {
		return true
	}
	if strings.TrimSpace(c.Field) == "" {
		return true
	}
	value, ok := caseInsensitiveRecordValue(record, c.Field)
	if !ok {
		return false
	}
	return RecordMutationFieldValueEquals(value, c.Value, c.CaseInsensitive, recordMutationConditionFieldIsBoolean(schema, c.Field))
}

// RecordMutationFieldValueEquals compares a request/record value against a
// metadata-authored record-mutation literal. Boolean literal normalization is
// enabled only when the caller proved the target field is Boolean from
// ObjectMetadata; non-Boolean fields keep plain string equality.
func RecordMutationFieldValueEquals(value interface{}, want string, caseInsensitive bool, booleanField bool) bool {
	got := strings.TrimSpace(fmt.Sprint(value))
	want = strings.TrimSpace(want)
	if booleanField {
		if gotBool, gotOK := boolLikeRecordMutationValue(value); gotOK {
			if wantBool, err := strconv.ParseBool(want); err == nil {
				return gotBool == wantBool
			}
		}
	}
	if caseInsensitive {
		return strings.EqualFold(got, want)
	}
	return got == want
}

func recordMutationConditionFieldIsBoolean(schema *ObjectMetadata, field string) bool {
	fieldMetadata := FindField(schema, field)
	return fieldMetadata != nil && strings.EqualFold(strings.TrimSpace(string(fieldMetadata.Type)), string(constants.FieldTypeBoolean))
}

func boolLikeRecordMutationValue(value interface{}) (bool, bool) {
	switch v := value.(type) {
	case bool:
		return v, true
	case float64:
		if v == 0 || v == 1 {
			return v != 0, true
		}
	case string:
		parsed, err := strconv.ParseBool(strings.TrimSpace(v))
		if err == nil {
			return parsed, true
		}
	}
	return false, false
}

func (c *RecordMutationFieldEqualsCondition) mayNeedExisting(record SObject) bool {
	if c == nil || strings.TrimSpace(c.Field) == "" {
		return false
	}
	_, ok := caseInsensitiveRecordValue(record, c.Field)
	return !ok
}

func (c *RecordMutationFieldEqualsCondition) matchesPresentWithSchema(record SObject, schema *ObjectMetadata) bool {
	if c == nil || strings.TrimSpace(c.Field) == "" {
		return false
	}
	return c.matchesWithSchema(record, schema)
}

func (c *RecordMutationFieldPresentCondition) matches(record SObject) bool {
	if c == nil || strings.TrimSpace(c.Field) == "" {
		return false
	}
	_, ok := caseInsensitiveRecordValue(record, c.Field)
	return ok
}

func caseInsensitiveRecordValue(record SObject, field string) (interface{}, bool) {
	field = strings.TrimSpace(field)
	if record == nil || field == "" {
		return nil, false
	}
	for key, value := range record {
		if strings.EqualFold(strings.TrimSpace(key), field) {
			return value, true
		}
	}
	return nil, false
}

// ListView represents a list view configuration
type ListView struct {
	ID            string `json:"id"`
	ObjectAPIName string `json:"objectApiName"`
	Label         string `json:"label"`

	FilterExpr    string   `json:"filterExpr,omitempty"`
	FilterLogic   string   `json:"filterLogic,omitempty"`
	SortField     string   `json:"sortField,omitempty"`
	SortDirection string   `json:"sortDirection,omitempty"`
	Fields        []string `json:"fields,omitempty"`

	OwnerID       *string `json:"ownerId,omitempty"`
	Visibility    *string `json:"visibility,omitempty"`
	DeveloperName *string `json:"developerName,omitempty"`
}

// ListViewPatch preserves PATCH presence semantics. A nil pointer means the
// field was omitted; a non-nil pointer, including a pointer to "", is
// authoritative.
type ListViewPatch struct {
	Label         *string
	FilterExpr    *string
	FilterLogic   *string
	SortField     *string
	SortDirection *string
	Fields        *[]string
	DeveloperName *string
}

// RecordStateBanner is the banner displayed when a record-state rule matches.
type RecordStateBanner struct {
	Severity string   `json:"severity"` // info | success | warning | error
	Title    string   `json:"title"`
	Body     string   `json:"body,omitempty"`
	LinkIDs  []string `json:"linkIds,omitempty"`
}

// RecordStateLink is a quick-navigation link authored in a record-state rule.
type RecordStateLink struct {
	ID    string `json:"id"`
	Label string `json:"label"`
	// ReferenceFieldAPIName is the host record lookup/FK field whose value is
	// used as the target record ID. ObjectAPIName only disambiguates the target
	// record type.
	ReferenceFieldAPIName string `json:"referenceFieldApiName"`
	ObjectAPIName         string `json:"objectApiName,omitempty"`
	Placement             string `json:"placement,omitempty"` // banner | quick-links
}

// RecordStateActionOverride describes how a named host action is suppressed when a rule is active.
type RecordStateActionOverride struct {
	ActionKey string `json:"actionKey"`
	Mode      string `json:"mode"` // hide | disable
	Reason    string `json:"reason,omitempty"`
}

// RecordStateReadOnly declares that the detail host should be read-only when a rule is active.
type RecordStateReadOnly struct {
	Enabled bool   `json:"enabled"`
	Reason  string `json:"reason,omitempty"`
}

// RecordStateRule is a single entry in PageLayout.RecordStates. When its when
// expression matches a record, the host renders the associated banner, links,
// action overrides, and read-only state. Rules are evaluated in descending
// priority order; at most one rule is active at a time.
type RecordStateRule struct {
	ID              string                      `json:"id"`
	When            string                      `json:"when"`
	Priority        int                         `json:"priority,omitempty"`
	Banner          *RecordStateBanner          `json:"banner,omitempty"`
	Links           []RecordStateLink           `json:"links,omitempty"`
	ActionOverrides []RecordStateActionOverride `json:"actionOverrides,omitempty"`
	ReadOnly        *RecordStateReadOnly        `json:"readOnly,omitempty"`
}

// PageLayout represents page layout configuration
type PageLayout struct {
	ID            string              `json:"id"`
	ObjectAPIName string              `json:"objectApiName"`
	LayoutName    string              `json:"layoutName"`
	Type          string              `json:"type,omitempty"` // Detail, Edit, Create, List
	IsDefault     bool                `json:"isDefault,omitempty"`
	IsDeleted     bool                `json:"isDeleted,omitempty"`
	CompactLayout []string            `json:"compactLayout"`
	Tabs          []string            `json:"tabs,omitempty"`
	Sections      []PageSection       `json:"sections"`
	RelatedLists  []RelatedListConfig `json:"relatedLists"`
	RecordPanels  []RecordPanelConfig `json:"recordPanels,omitempty"`
	HeaderActions []ActionConfig      `json:"headerActions"`
	QuickActions  []ActionConfig      `json:"quickActions"`
	// HeaderActionsAuthored and QuickActionsAuthored distinguish an explicitly
	// authored empty placement list from the historical default empty slice.
	HeaderActionsAuthored bool `json:"headerActionsAuthored,omitempty"`
	QuickActionsAuthored  bool `json:"quickActionsAuthored,omitempty"`
	// RecordStates carries generic L1 record-state presentation rules (banners, quick links, action overrides, read-only override).
	RecordStates []RecordStateRule `json:"recordStates,omitempty"`
	// PrimaryHeaderActions pins which record-header actions occupy the primary
	// row (and in what order), by stable action key. The platform stores and
	// round-trips the list; interpretation (key vocabulary, budget, graceful
	// degradation when a pinned action is absent or suppressed for a record)
	// belongs to the consuming app layer, like RecordStates.
	PrimaryHeaderActions []string  `json:"primaryHeaderActions,omitempty"`
	IsCustomized         bool      `json:"isCustomized,omitempty"`      // DEPRECATED: use Source
	LayoutVersion        int       `json:"layoutVersion,omitempty"`     // DEPRECATED: use GenerationVersion
	Source               string    `json:"source,omitempty"`            // auto_generated, admin_created, cloned, curated
	GenerationVersion    int       `json:"generationVersion,omitempty"` // Algorithm version (only for Source=auto_generated)
	CreatedDate          time.Time `json:"createdDate,omitempty"`
	LastModifiedDate     time.Time `json:"lastModifiedDate,omitempty"`
}

// LayoutAssignment maps a (Role x Profile x RecordType) combination to a PageLayout.
// NULL dimensions are wildcards ("any"). Specificity = count of non-NULL dimensions.
const (
	LayoutAssignmentRouteSourceRuntimeFallback = "runtime_fallback"
	LayoutAssignmentRouteSourceManagedSeed     = "managed_seed"
	LayoutAssignmentRouteSourceAdminAuthored   = "admin_authored"
)

type LayoutAssignment struct {
	ID               string    `json:"id"`
	ObjectAPIName    string    `json:"objectApiName"`
	LayoutID         string    `json:"layoutId"`
	RoleID           *string   `json:"roleId,omitempty"`
	ProfileID        *string   `json:"profileId,omitempty"`
	RecordTypeID     *string   `json:"recordTypeId,omitempty"`
	RouteSource      *string   `json:"-"`
	Specificity      int       `json:"specificity"`
	CreatedDate      time.Time `json:"createdDate,omitempty"`
	LastModifiedDate time.Time `json:"lastModifiedDate,omitempty"`
	CreatedByID      *string   `json:"createdById,omitempty"`
}

// LayoutAuthoringAggregate is the complete object-scoped stored-authoring
// state. Persistence returns all four fact families from one transaction; the
// mutation path adds current-read locks while publication uses the identical
// projection from its transaction snapshot.
type LayoutAuthoringAggregate struct {
	ObjectAPIName     string
	OrderedListFields []string
	Layouts           []*PageLayout
	Assignments       []*LayoutAssignment
}

// RecordPanelConfig declares an app-owned related surface on a record page.
type RecordPanelConfig struct {
	ID                  string                 `json:"id"`
	RendererID          string                 `json:"rendererId"`
	Position            string                 `json:"position,omitempty"` // "tab" (default when empty) | "header-banner"
	TabID               string                 `json:"tabId,omitempty"`    // required when Position == "" or "tab"
	TabLabel            string                 `json:"tabLabel,omitempty"` // required when Position == "" or "tab"
	Order               int                    `json:"order,omitempty"`
	VisibilityCondition string                 `json:"visibilityCondition,omitempty"` // expression evaluated at render time
	Config              map[string]interface{} `json:"config,omitempty"`              // opaque renderer-owned placement configuration
}

// Normalize converts empty-string dimension pointers to nil.
// Prevents "phantom" assignments where an empty string has specificity 1 but never matches.
func (la *LayoutAssignment) Normalize() {
	if la.RoleID != nil && *la.RoleID == "" {
		la.RoleID = nil
	}
	if la.ProfileID != nil && *la.ProfileID == "" {
		la.ProfileID = nil
	}
	if la.RecordTypeID != nil && *la.RecordTypeID == "" {
		la.RecordTypeID = nil
	}
}

// ComputeSpecificity calculates the specificity from non-NULL dimensions.
func (la *LayoutAssignment) ComputeSpecificity() int {
	s := 0
	if la.RoleID != nil {
		s++
	}
	if la.ProfileID != nil {
		s++
	}
	if la.RecordTypeID != nil {
		s++
	}
	la.Specificity = s
	return s
}

// PageLayoutHistoryEntry is a content version snapshot for undo/rollback.
type PageLayoutHistoryEntry struct {
	ID            string    `json:"id"`
	LayoutID      string    `json:"layoutId"`
	VersionNumber int       `json:"versionNumber"`
	Config        string    `json:"config"` // raw JSON snapshot
	ChangedByID   *string   `json:"changedById,omitempty"`
	ChangeReason  string    `json:"changeReason"`
	CreatedDate   time.Time `json:"createdDate"`
}

// PageSection represents a section in a page layout
type PageSection struct {
	ID              string                 `json:"id"`
	Label           string                 `json:"label"`
	Tab             *string                `json:"tab,omitempty"`
	Type            *string                `json:"type,omitempty"` // Fields, Component
	ComponentName   *string                `json:"componentName,omitempty"`
	ComponentConfig map[string]interface{} `json:"componentConfig,omitempty"`
	Columns         int                    `json:"columns"`
	Fields          []string               `json:"fields"`
	FieldLabels     map[string]string      `json:"fieldLabels,omitempty"`
	// FieldBehaviors carries per-field, per-mode presentation narrowing, keyed
	// by field API name and then by mode key. The platform stores and
	// round-trips the map verbatim; the mode-key vocabulary, the behavior
	// vocabulary, and their intersection with field-level security belong to
	// the consuming app layer, like PrimaryHeaderActions. (RecordStates is not
	// the same case — layoutfls inspects rule text for hidden-field references.)
	// Unrecognized keys and values are preserved, never rewritten or pruned,
	// so an app layer can introduce vocabulary without a platform release.
	FieldBehaviors      map[string]map[string]string `json:"fieldBehaviors,omitempty"`
	VisibilityCondition *string                      `json:"visibilityCondition,omitempty"`
}

// RelatedListConfig represents a related list configuration
type RelatedListConfig struct {
	ID              string            `json:"id"`
	Label           string            `json:"label"`
	SecondaryLabel  string            `json:"secondaryLabel,omitempty"`
	ObjectAPIName   string            `json:"objectApiName"`
	LookupField     string            `json:"lookupField"`
	Fields          []string          `json:"fields"`
	PrefillMappings map[string]string `json:"prefillMappings,omitempty"`
}

// ActionConfig represents an action configuration
type ActionConfig struct {
	Name         string                 `json:"name"`
	Label        string                 `json:"label"`
	Type         string                 `json:"type"` // Standard, CreateRecord, UpdateRecord, Flow, Custom, Url
	Icon         *string                `json:"icon,omitempty"`
	TargetObject *string                `json:"targetObject,omitempty"`
	Config       map[string]interface{} `json:"config,omitempty"`
	// Extensions preserves app-owned action vocabulary which is not interpreted
	// by the platform. It is stored independently from Config so the neutral
	// authoring contract can round-trip both typed collections without merging
	// their ownership domains.
	Extensions          map[string]interface{} `json:"extensions,omitempty"`
	VisibilityCondition *string                `json:"visibilityCondition,omitempty"`
	Component           *string                `json:"component,omitempty"`
}

// ActionMetadata represents action metadata
type ActionMetadata struct {
	ID            string                 `json:"id"`
	ObjectAPIName string                 `json:"objectApiName"`
	Name          string                 `json:"name"`
	Label         string                 `json:"label"`
	Type          string                 `json:"type"`
	Icon          string                 `json:"icon"`
	TargetObject  *string                `json:"targetObject,omitempty"`
	Config        map[string]interface{} `json:"config,omitempty"`
	OwnerPack     *string                `json:"ownerPack,omitempty"`
}

type RecordType struct {
	ID                string    `json:"id"`
	ObjectAPIName     string    `json:"objectApiName"`
	Name              string    `json:"name"`
	DeveloperName     string    `json:"developerName"`
	Label             string    `json:"label"`
	Description       *string   `json:"description,omitempty"`
	IsActive          bool      `json:"isActive"`
	IsDefault         bool      `json:"isDefault"`
	BusinessProcessID *string   `json:"businessProcessId,omitempty"`
	CreatedDate       time.Time `json:"createdDate"`
	LastModifiedDate  time.Time `json:"lastModifiedDate"`
}

type AutoNumber struct {
	ID               string    `json:"id"`
	ObjectAPIName    string    `json:"objectApiName"`
	FieldAPIName     string    `json:"fieldApiName"`
	DisplayFormat    string    `json:"displayFormat"`
	StartingNumber   int       `json:"startingNumber"`
	CurrentValue     int       `json:"currentValue"`
	CreatedDate      time.Time `json:"createdDate"`
	LastModifiedDate time.Time `json:"lastModifiedDate"`
}

// IdentifierRule is the canonical in-memory representation of a row from
// `_System_IdentifierRule` (the Unified Identifier Engine — see
// docs/design/unified-identifier-engine.md).
//
// JSON tags on TargetFields, FieldLookups, IdentifierFieldLookup, and
// IdentifierLookupStep MUST stay in lock-step with the seed payload written
// by `controlplane/packs.applyIdentifierRules` (Task 1). Otherwise rows
// loaded from DB lose fields when JSON-decoded back into this struct.
type IdentifierRule struct {
	ID            string                           `json:"id"`
	ObjectAPIName string                           `json:"objectApiName"`
	RuleName      string                           `json:"ruleName"`
	Template      string                           `json:"template"`
	TargetFields  map[string]string                `json:"targetFields"`
	FieldLookups  map[string]IdentifierFieldLookup `json:"fieldLookups"`
	IsActive      bool                             `json:"isActive"`
}

// IdentifierFieldLookup describes how to resolve a single template variable
// (e.g. "Account.BillingCountryCode") for a record. Mirrors the structure
// produced by IdentifierRuleSeeds() in controlplane/packs/platform_config.go.
type IdentifierFieldLookup struct {
	DirectField string                   `json:"directField,omitempty"`
	Chains      [][]IdentifierLookupStep `json:"chains"`
}

// IdentifierLookupStep is one hop in an FK resolution chain.
type IdentifierLookupStep struct {
	FKField     string `json:"fkField"`
	TargetTable string `json:"targetTable"`
	TargetField string `json:"targetField"`
}

type Relationship struct {
	ID                  string    `json:"id"`
	ChildObjectAPIName  string    `json:"childObjectApiName"`
	ParentObjectAPIName string    `json:"parentObjectApiName"`
	FieldAPIName        string    `json:"fieldApiName"`
	RelationshipName    string    `json:"relationshipName"`
	RelationshipType    string    `json:"relationshipType"`
	IsSharingParent     bool      `json:"isSharingParent"`
	CascadeDelete       bool      `json:"cascadeDelete"`
	RestrictedDelete    bool      `json:"restrictedDelete"`
	RelatedListLabel    *string   `json:"relatedListLabel,omitempty"`
	RelatedListFields   *string   `json:"relatedListFields,omitempty"`
	CreatedDate         time.Time `json:"createdDate"`
	LastModifiedDate    time.Time `json:"lastModifiedDate"`
}

type FieldDependency struct {
	ID               string    `json:"id"`
	ObjectAPIName    string    `json:"objectApiName"`
	ControllingField string    `json:"controllingField"`
	DependentField   string    `json:"dependentField"`
	ControllingValue string    `json:"controllingValue"`
	DependentValues  []string  `json:"dependentValues"`
	IsActive         bool      `json:"isActive"`
	CreatedDate      time.Time `json:"createdDate"`
	LastModifiedDate time.Time `json:"lastModifiedDate"`
}

// ValidationRule represents a validation rule
type ValidationRule struct {
	ID                string                           `json:"id"`
	ObjectAPIName     string                           `json:"objectApiName"`
	Name              string                           `json:"name"`
	Description       *string                          `json:"description,omitempty"`
	Active            bool                             `json:"active"`
	Condition         string                           `json:"condition"`
	ErrorMessage      string                           `json:"errorMessage"`
	ErrorDisplayField *string                          `json:"errorDisplayField,omitempty"`
	RecordSetBindings []ValidationRuleRecordSetBinding `json:"recordSetBindings,omitempty"`
	IsCustomized      bool                             `json:"isCustomized"`
	OwnerPack         *string                          `json:"ownerPack,omitempty"`
}

// ValidationRuleRecordSetBinding is a NexusCRM extension to the Salesforce
// ValidationRule substrate. It lets metadata bind boolean facts derived from
// generic record-set queries into a rule formula without platform code owning
// object-specific business policy.
type ValidationRuleRecordSetBinding struct {
	Name          string                          `json:"name"`
	Mode          string                          `json:"mode"`
	ObjectAPIName string                          `json:"objectApiName"`
	FilterLogic   string                          `json:"filterLogic,omitempty"`
	Filters       []ValidationRuleRecordSetFilter `json:"filters"`
}

type ValidationRuleRecordSetFilter struct {
	Field       string      `json:"field"`
	Operator    string      `json:"operator"`
	ValueSource string      `json:"valueSource"`
	Value       interface{} `json:"value"`
}

// NavigationItem represents a navigation item in an app
type NavigationItem struct {
	ID            string `json:"id"`
	Type          string `json:"type"` // object, page, web, dashboard
	ObjectAPIName string `json:"objectApiName,omitempty"`
	PageURL       string `json:"pageUrl,omitempty"`
	DashboardID   string `json:"dashboardId,omitempty"`
	Label         string `json:"label"`
	Icon          string `json:"icon"`
}

// CanonicalTabName returns the Salesforce-modeled tab name controlled by
// ProfileTabSetting / PermissionSetTabSetting for navigation items whose
// visibility is tab-governed.
func (item NavigationItem) CanonicalTabName() (string, bool) {
	switch strings.ToLower(strings.TrimSpace(item.Type)) {
	case constants.NavigationTypeObject:
		name := strings.TrimSpace(item.ObjectAPIName)
		return name, name != ""
	case constants.NavigationTypePage:
		if name := strings.TrimSpace(item.ObjectAPIName); name != "" {
			return name, true
		}
		if name := strings.TrimSpace(item.Label); name != "" {
			return name, true
		}
		name := strings.TrimSpace(item.ID)
		return name, name != ""
	default:
		return "", false
	}
}

// UtilityItem represents a utility bar item in an app (Salesforce Lightning parity)
type UtilityItem struct {
	ID          string `json:"id"`
	Type        string `json:"type"` // notes, recent, history, custom
	Label       string `json:"label"`
	Icon        string `json:"icon"`
	PanelWidth  *int   `json:"panelWidth,omitempty"`
	PanelHeight *int   `json:"panelHeight,omitempty"`
}

// AppConfig represents application configuration
type AppConfig struct {
	ID               string           `json:"id"`
	FrontendAppID    string           `json:"frontendAppId,omitempty"`
	Label            string           `json:"label"`
	Description      string           `json:"description"`
	Icon             string           `json:"icon"`
	Color            string           `json:"color"`
	IsDefault        bool             `json:"isDefault"`
	NavigationItems  []NavigationItem `json:"navigationItems,omitempty"`
	UtilityItems     []UtilityItem    `json:"utilityItems,omitempty"`
	AssignedProfiles []string         `json:"assignedProfiles,omitempty"`
	CreatedDate      time.Time        `json:"createdDate,omitempty"`
	LastModifiedDate time.Time        `json:"lastModifiedDate,omitempty"`
}

// Theme represents a visual theme
type Theme struct {
	ID               string                 `json:"id"`
	Name             string                 `json:"name"`
	IsActive         bool                   `json:"isActive"`
	Colors           map[string]interface{} `json:"colors"`
	Density          string                 `json:"density"`
	LogoURL          *string                `json:"logoUrl,omitempty"`
	CreatedDate      time.Time              `json:"createdDate,omitempty"`
	LastModifiedDate time.Time              `json:"lastModifiedDate,omitempty"`
}

// DashboardConfig represents dashboard configuration
type DashboardConfig struct {
	ID            string         `json:"id"`
	DeveloperName string         `json:"developerName"`
	Label         string         `json:"label"`
	Description   *string        `json:"description,omitempty"`
	Layout        string         `json:"layout,omitempty"`
	LayoutMode    string         `json:"layoutMode,omitempty"` // auto = smart layout, fixed = use exact positions
	FilterMode    string         `json:"filterMode,omitempty"` // none = no filters, date = date only, full = all filters
	StartDate     *string        `json:"startDate,omitempty"`  // YYYY-MM-DD
	EndDate       *string        `json:"endDate,omitempty"`    // YYYY-MM-DD
	DateRangeType string         `json:"dateRangeType,omitempty"`
	DashboardType string         `json:"dashboardType,omitempty"` // SpecifiedUser, LoggedInUser, MyTeamUser
	RunningUserId string         `json:"runningUserId,omitempty"` // User whose RLS is used for SpecifiedUser/MyTeamUser
	Widgets       []WidgetConfig `json:"widgets"`
}

// WidgetConfig represents a dashboard widget
type WidgetConfig struct {
	ID        string                 `json:"id"`
	Title     string                 `json:"title"`
	Type      string                 `json:"type"`
	ReportID  string                 `json:"reportId,omitempty"` // Report-First: widgets reference Salesforce Reports
	Config    map[string]interface{} `json:"config,omitempty"`   // Widget-specific settings
	X         *int                   `json:"x,omitempty"`
	Y         *int                   `json:"y,omitempty"`
	W         *int                   `json:"w,omitempty"`
	H         *int                   `json:"h,omitempty"`
	Icon      *string                `json:"icon,omitempty"`
	Color     *string                `json:"color,omitempty"`
	DateField *string                `json:"dateField,omitempty"`
}

type ProfileLayoutAssignment struct {
	LayoutID string `json:"layoutId"`
}

// SetupPage represents a page in the setup area
type SetupPage struct {
	ID                 string    `json:"id"`
	Label              string    `json:"label"`
	Icon               string    `json:"icon"`
	ComponentName      string    `json:"componentName"`
	Category           string    `json:"category"`
	PageOrder          int       `json:"pageOrder"`
	PermissionRequired string    `json:"permissionRequired,omitempty"`
	IsEnabled          bool      `json:"isEnabled"`
	Description        string    `json:"description,omitempty"`
	Path               string    `json:"path,omitempty"`
	CreatedDate        time.Time `json:"createdDate"`
	LastModifiedDate   time.Time `json:"lastModifiedDate"`
}

// UIComponent represents a registered UI component
type UIComponent struct {
	ID            string    `json:"id"`
	Name          string    `json:"name"`
	Type          string    `json:"type"` // page, widget, etc.
	IsEmbeddable  bool      `json:"isEmbeddable"`
	Description   *string   `json:"description,omitempty"`
	ComponentPath *string   `json:"componentPath,omitempty"`
	CreatedDate   time.Time `json:"createdDate,omitempty"`
	LastModified  time.Time `json:"lastModifiedDate,omitempty"`
}

// ApprovalPageLayout represents approval page layout configuration for customizing approval response pages
type ApprovalPageLayout struct {
	ID                string              `json:"id"`
	ObjectAPIName     string              `json:"objectApiName"`
	LayoutName        string              `json:"layoutName"`
	Type              string              `json:"type,omitempty"` // approval_response, approval_detail, etc.
	IsDefault         bool                `json:"isDefault,omitempty"`
	IsDeleted         bool                `json:"isDeleted,omitempty"`
	Config            interface{}         `json:"config"` // Parsed layout configuration
	CompactLayout     []string            `json:"compactLayout,omitempty"`
	Sections          []PageSection       `json:"sections,omitempty"`
	RelatedLists      []RelatedListConfig `json:"relatedLists,omitempty"`
	HeaderActions     []ActionConfig      `json:"headerActions,omitempty"`
	QuickActions      []ActionConfig      `json:"quickActions,omitempty"`
	IsCustomized      bool                `json:"isCustomized,omitempty"`      // DEPRECATED: use Source
	LayoutVersion     int                 `json:"layoutVersion,omitempty"`     // DEPRECATED: use GenerationVersion
	Source            string              `json:"source,omitempty"`            // auto_generated, admin_created, cloned, curated
	GenerationVersion int                 `json:"generationVersion,omitempty"` // Algorithm version (only for Source=auto_generated)
	CreatedDate       time.Time           `json:"createdDate,omitempty"`
	LastModifiedDate  time.Time           `json:"lastModifiedDate,omitempty"`
}
