package fieldcapabilities

import (
	"strings"

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

// ComputeIntrinsic determines whether a field is inherently createable/updateable,
// independent of user permissions.
//
// This function is platform substrate: it only applies rules that can be
// inferred from generic metadata shape. Application-pack facts such as CRM
// aggregate/derived fields are declared by pack metadata and persisted on
// _System_Field.Createable / _System_Field.Updateable.
//
// Runtime consumers read the stored result; they must not call this function to
// rediscover app-pack facts at request time.
func ComputeIntrinsic(objectAPIName string, f *models.FieldMetadata) (createable bool, updateable bool) {
	// Audit fields — always non-editable
	switch f.APIName {
	case constants.FieldID,
		constants.FieldCreatedDate, constants.FieldCreatedByID,
		constants.FieldLastModifiedDate, constants.FieldLastModifiedByID,
		constants.FieldSystemModstamp, constants.FieldIsDeleted:
		return false, false
	}

	// Type-based — formula, autoNumber, rollupSummary
	normalizedType := constants.NormalizeFieldType(string(f.Type))
	if (f.Formula != nil && *f.Formula != "") ||
		f.ProjectionConfig != nil ||
		normalizedType == constants.FieldTypeAutonumber ||
		normalizedType == constants.FieldTypeRollupSummary {
		return false, false
	}

	// System-maintained date fields
	switch f.APIName {
	case constants.FieldLastActivityDate, constants.FieldLastViewedDate, constants.FieldLastReferencedDate:
		return false, false
	}

	// Discovered-source columns — physical storage without a declaration.
	// Provenance is derived at read time by fieldprovenance.ClassifySource
	// from installed manifests plus platform structural rules. A field classified as `discovered` exists
	// physically but has no declaration path, so it is legacy / read-only.
	// Issue #1582 RFC v1.6 (derived-on-read; replaces the stored f.Source
	// column in v1.5).
	if fieldprovenance.ClassifySource(objectAPIName, f.APIName) == constants.FieldSourceDiscovered {
		return false, false
	}

	// Compound parent fields — derived from ComposedFrom member fields
	// (e.g., Quote.Name from QuoteNumber, User.Name from FirstName+LastName).
	// Read-only on write per SF parity: real SF describes User.Name as
	// `createable=false, updateable=false`. The persistence-layer compound
	// resolver synthesizes the value from members. Compound declaration is
	// derived at read time from the L1 manifests via
	// fieldprovenance.ClassifyCompound. Issue #1582 RFC v1.6.
	if isCompound, _ := fieldprovenance.ClassifyCompound(objectAPIName, f.APIName); isCompound {
		return false, false
	}

	if deny, ok := platformFieldDenylist[strings.ToLower(objectAPIName)]; ok {
		if deny[f.APIName] {
			return false, false
		}
	}

	// A metadata policy can only narrow the default capabilities of a field
	// that passed every universal intrinsic read-only rule above. In particular,
	// it must never make system-maintained, discovered, compound, or denylisted
	// fields createable.
	return ApplyWriteProtectionPolicy(true, true, f.WriteProtectionPolicy)
}

// ApplyWriteProtectionPolicy narrows declared capabilities according to the
// generic field write-protection policy. It is safe for runtime projections:
// callers supply the persisted capability facts, and this helper never widens
// either one. Unknown policy values fail closed.
func ApplyWriteProtectionPolicy(createable, updateable bool, policy string) (bool, bool) {
	switch strings.TrimSpace(policy) {
	case constants.FieldWriteProtectionImmutableAfterCreate:
		return createable, false
	case constants.FieldWriteProtectionTrustedOnly,
		constants.FieldWriteProtectionAuthorityOnly:
		return false, false
	case "":
		return createable, updateable
	default:
		// Metadata validation rejects unknown policies. Fail closed here as
		// well so a malformed imported or legacy declaration cannot advertise
		// a writable field.
		return false, false
	}
}

// ApplyIntrinsicBounds combines metadata-declared capabilities with universal
// platform deny rules. It is used by pack/custom metadata persistence so a
// declaration can make a normal field read-only, while formula/system/compound
// fields remain read-only even if a payload accidentally declares otherwise.
func ApplyIntrinsicBounds(objectAPIName string, f *models.FieldMetadata, createable bool, updateable bool) (bool, bool) {
	intrinsicCreateable, intrinsicUpdateable := ComputeIntrinsic(objectAPIName, f)
	return createable && intrinsicCreateable, updateable && intrinsicUpdateable
}

// platformFieldDenylist is limited to platform-owned admin/security object
// fields. Application-pack field capabilities belong in pack metadata.
var platformFieldDenylist = map[string]map[string]bool{
	"user": {
		// NOTE: Password is intentionally NOT in this denylist. It must remain
		// createable/updateable at the persistence layer for password hashing to work.
		// It is hidden from the UI via: (1) isLayoutInvisibleField, (2) frontend
		// handleBeforeSubmit payload stripping, (3) encryptedstring type rendering.
		"FailedLoginAttempts": true, // security-internal: login failure counter
		"LockedUntil":         true, // security-internal: account lock timestamp
		"LastLoginDate":       true, // system-maintained: last successful login
	},
}
