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.
//
// Two layers:
//
//	Layer 1: Universal rules (all objects) — audit fields, formula, autoNumber, rollupSummary, system-maintained dates
//	Layer 2: Object-specific overrides — Campaign aggregates, Opportunity computed fields
//
// This is the SINGLE place where intrinsic capability is decided.
// All consumers read the stored result; none re-derive.
func ComputeIntrinsic(objectAPIName string, f *models.FieldMetadata) (createable bool, updateable bool) {
	// Layer 1: Universal intrinsic deny

	// 1a. 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
	}

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

	// 1c. System-maintained date fields
	switch f.APIName {
	case "LastActivityDate", "LastViewedDate", "LastReferencedDate":
		return false, false
	}

	// 1d. Discovered-source columns — physical storage without a declaration.
	// Provenance is derived at read time by fieldprovenance.ClassifySource
	// from the L1 manifests (system_tables.json, standard_objects_spec.json,
	// tenant_objects_spec.json) + the platform standard-field template +
	// APIName 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
	}

	// 1e. 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
	}

	// Layer 2: Object-specific overrides
	if deny, ok := objectFieldDenylist[strings.ToLower(objectAPIName)]; ok {
		if deny[f.APIName] {
			return false, false
		}
	}

	// Default: createable and updateable
	return true, true
}

// objectFieldDenylist maps object API name (lowercase) to a set of field API names
// that are non-createable/non-updateable despite having normal column types.
// These are system-computed aggregate or derived fields that cannot be inferred
// from field type alone.
//
// Source: Salesforce Object Reference PDF — fields without Create/Update properties.
var objectFieldDenylist = map[string]map[string]bool{
	"campaign": {
		"AmountAllOpportunities":   true,
		"AmountWonOpportunities":   true,
		"NumberOfContacts":         true,
		"NumberOfConvertedLeads":   true,
		"NumberOfLeads":            true,
		"NumberOfOpportunities":    true,
		"NumberOfWonOpportunities": true,
		"NumberOfResponses":        true,
	},
	"opportunity": {
		"ExpectedRevenue":        true, // Amount * Probability, read-only
		"HasOpportunityLineItem": true, // system-maintained boolean
		"IsClosed":               true, // derived from StageName
		"HasOpenActivity":        true, // system-maintained
	},
	"order": {
		"ActivatedDate":  true, // system-set on activation
		"ActivatedById":  true, // system-set on activation
		"StatusCode":     true, // system-set from Status transition
	},
	"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
	},
}
