// Package fieldprovenance derives the Source (declaration provenance) and
// compound parenthood of a _System_Field row from L1 declaration manifests
// (system_tables.json, standard_objects_spec.json, tenant_objects_spec.json)
// and structural attributes. See ISSUE_1582_RFC.md §1.6 for the design.
//
// A Classifier is constructed once at bootstrap and is safe for concurrent
// reads thereafter — no writes after Init.
package fieldprovenance

import (
	"strings"

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

// FieldDecl is a single field declaration from an L1 manifest.
type FieldDecl struct {
	ObjectAPIName string
	FieldAPIName  string
	IsCompound    bool
	ComposedFrom  []string
}

// Manifest is an in-memory index of L1 field declarations keyed by
// lowercase (object, field) for case-insensitive lookup.
type Manifest struct {
	byObj map[string]map[string]*FieldDecl
}

// NewManifest returns an empty manifest.
func NewManifest() *Manifest {
	return &Manifest{byObj: make(map[string]map[string]*FieldDecl)}
}

// Add registers a declaration. Re-adding the same (object, field) replaces
// the previous entry. A nil decl is treated as "exists but no compound info".
func (m *Manifest) Add(objectAPIName, fieldAPIName string, decl *FieldDecl) {
	if m.byObj == nil {
		m.byObj = make(map[string]map[string]*FieldDecl)
	}
	objKey := strings.ToLower(objectAPIName)
	fieldKey := strings.ToLower(fieldAPIName)
	bucket, ok := m.byObj[objKey]
	if !ok {
		bucket = make(map[string]*FieldDecl)
		m.byObj[objKey] = bucket
	}
	if decl == nil {
		decl = &FieldDecl{ObjectAPIName: objectAPIName, FieldAPIName: fieldAPIName}
	}
	bucket[fieldKey] = decl
}

// Lookup returns the declaration for (object, field) or nil. Case-insensitive.
func (m *Manifest) Lookup(objectAPIName, fieldAPIName string) *FieldDecl {
	if m == nil {
		return nil
	}
	return m.lookupLower(strings.ToLower(objectAPIName), strings.ToLower(fieldAPIName))
}

// HasObject reports whether any field declaration is registered for the
// object. Used by ClassifySource rule 5 to distinguish
// "platform-or-pack-declared object" (rules 1-3 / 6 apply) from
// "runtime-admin-created custom_object" (rule 5 applies).
// Case-insensitive.
func (m *Manifest) HasObject(objectAPIName string) bool {
	if m == nil {
		return false
	}
	return m.hasObjectLower(strings.ToLower(objectAPIName))
}

// lookupLower is the inner form that assumes already-lowercased keys. Used by
// ClassifySource / ClassifyCompound to avoid re-lowercasing across multiple
// Manifest lookups in the same call.
func (m *Manifest) lookupLower(objL, fieldL string) *FieldDecl {
	if m == nil || m.byObj == nil {
		return nil
	}
	bucket, ok := m.byObj[objL]
	if !ok {
		return nil
	}
	return bucket[fieldL]
}

// hasObjectLower is the lowercase-key form for internal reuse.
func (m *Manifest) hasObjectLower(objL string) bool {
	if m == nil || m.byObj == nil {
		return false
	}
	_, ok := m.byObj[objL]
	return ok
}

// Classifier derives provenance for _System_Field rows. Construct via
// BuildClassifier once at bootstrap; safe for concurrent reads thereafter.
//
// ClassifySource precedence (first match wins):
//  1. systemTables manifest → FieldSourceManaged
//  2. standardSpec manifest → FieldSourceStandardSpec
//  3. tenantSpec manifest → FieldSourceTenantSpec
//  4. platform standard-field template → FieldSourceManaged
//  5. object is NOT declared in any L1 manifest → FieldSourceCustom
//     (this is a runtime admin-created custom_object via CreateSchema;
//     every non-platform-standard field on it is admin-declared by
//     construction, so provenance is 'custom' regardless of naming)
//  6. APIName suffix "__c" → FieldSourceCustom
//     (fallback for admin-added custom fields on L1-declared objects,
//     e.g. Account.Foo__c via CreateField)
//  7. default → FieldSourceDiscovered
type Classifier struct {
	systemTables *Manifest
	standardSpec *Manifest
	tenantSpec   *Manifest
	platformStd  map[string]struct{} // lowercase field APIName set
}

// NewClassifier wires up the three manifest indices and the platform
// standard-field template. Any nil manifest is treated as empty.
func NewClassifier(systemTables, standardSpec, tenantSpec *Manifest, platformStandardFields []string) *Classifier {
	if systemTables == nil {
		systemTables = NewManifest()
	}
	if standardSpec == nil {
		standardSpec = NewManifest()
	}
	if tenantSpec == nil {
		tenantSpec = NewManifest()
	}
	platformStd := make(map[string]struct{}, len(platformStandardFields))
	for _, name := range platformStandardFields {
		platformStd[strings.ToLower(name)] = struct{}{}
	}
	return &Classifier{
		systemTables: systemTables,
		standardSpec: standardSpec,
		tenantSpec:   tenantSpec,
		platformStd:  platformStd,
	}
}

// ClassifySource returns the provenance class of the field.
func (c *Classifier) ClassifySource(objectAPIName, fieldAPIName string) string {
	if c == nil {
		return constants.FieldSourceDiscovered
	}
	// Lowercase once; all Manifest lookups below reuse the lowercased keys.
	objL := strings.ToLower(objectAPIName)
	fieldL := strings.ToLower(fieldAPIName)

	if c.systemTables.lookupLower(objL, fieldL) != nil {
		return constants.FieldSourceManaged
	}
	if c.standardSpec.lookupLower(objL, fieldL) != nil {
		return constants.FieldSourceStandardSpec
	}
	if c.tenantSpec.lookupLower(objL, fieldL) != nil {
		return constants.FieldSourceTenantSpec
	}
	if _, ok := c.platformStd[fieldL]; ok {
		return constants.FieldSourceManaged
	}
	// Rule 5: the object itself is absent from every L1 manifest. That
	// can only mean it was created at runtime via CreateSchema (the
	// _System_Object row carries TableType='custom_object'). By
	// construction every field on it is admin-declared, so provenance
	// is 'custom' — no naming convention required.
	if !c.systemTables.hasObjectLower(objL) &&
		!c.standardSpec.hasObjectLower(objL) &&
		!c.tenantSpec.hasObjectLower(objL) {
		return constants.FieldSourceCustom
	}
	// Rule 6: the object IS L1-declared, but this specific field isn't.
	// Covers admin CreateField on a standard/system object (e.g.
	// Account.Foo__c). The __c convention is the metadata-driven signal
	// admin UIs enforce; a future _System_FieldDeclarationAck table will
	// replace this fallback with an explicit per-row provenance stamp.
	if strings.HasSuffix(fieldL, "__c") {
		return constants.FieldSourceCustom
	}
	return constants.FieldSourceDiscovered
}

// ClassifyCompound reports whether the field is a compound parent. Scan
// order is systemTables → standardSpec → tenantSpec; returns on the first
// declaration whose IsCompound=true. A non-compound declaration in an
// earlier manifest does NOT block a compound declaration in a later one —
// intentional, and asymmetric with ClassifySource's first-match semantics:
// real manifests should never conflict, and when they do this conservative
// behavior honors the compound declaration rather than silently dropping it.
//
// Returned composedFrom is a defensive copy.
func (c *Classifier) ClassifyCompound(objectAPIName, fieldAPIName string) (isCompound bool, composedFrom []string) {
	if c == nil {
		return false, nil
	}
	objL := strings.ToLower(objectAPIName)
	fieldL := strings.ToLower(fieldAPIName)
	for _, m := range []*Manifest{c.systemTables, c.standardSpec, c.tenantSpec} {
		if decl := m.lookupLower(objL, fieldL); decl != nil && decl.IsCompound {
			if len(decl.ComposedFrom) == 0 {
				return true, nil
			}
			return true, append([]string(nil), decl.ComposedFrom...)
		}
	}
	return false, nil
}
