package bootstrap

import (
	"encoding/json"
	"fmt"

	"github.com/nexuscrm/backend/internal/controlplane/packspec"
	"github.com/nexuscrm/backend/internal/infrastructure/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/fieldprovenance"
)

// DefaultPlatformStandardFields returns the APINames of every field the
// platform injects into every custom object at CreateSchema time. A field
// name in this list is classified as FieldSourceManaged by the provenance
// classifier regardless of the owning object — they are platform-owned,
// not admin-authored.
//
// Composed from two existing sources to avoid SSoT drift:
//   - persistence.SchemaRepository.GetStandardSystemColumns() — the 8
//     audit / identity columns (Id, OwnerId, CreatedBy*, LastModifiedBy*,
//     *Date, SystemModstamp, IsDeleted). These are the "truly system-
//     generated" fields per the repo's own comment.
//   - constants.FieldName — the Name fallback injected by
//     EnrichWithSystemFields when an object lacks an explicit name field.
//     Not part of GetStandardSystemColumns because Name is a business
//     field, but for classifier purposes it IS platform-owned on any
//     custom object (the user did not author the Name shape).
//
// Callers pass the returned slice to InitFieldProvenance at startup. If
// GetStandardSystemColumns changes, this helper reflects the change
// automatically.
func DefaultPlatformStandardFields() []string {
	// Zero-value SchemaRepository — GetStandardSystemColumns is a pure
	// function that reads no receiver state; the method-on-receiver
	// shape is a legacy of the repository pattern.
	repo := &persistence.SchemaRepository{}
	cols := repo.GetStandardSystemColumns()

	out := make([]string, 0, len(cols)+1)
	for _, col := range cols {
		if col.Name == "" {
			continue
		}
		out = append(out, col.Name)
	}
	// Name is not in GetStandardSystemColumns by design (the repo comment
	// notes it belongs to the object schema, not auto-injected cols), but
	// EnrichWithSystemFields at schema_defaults.go adds it when absent.
	// For provenance purposes it is platform-standard.
	out = append(out, constants.FieldName)
	return out
}

// fpSystemTableEntry is a narrow parse of system_tables.json, carrying
// only the attributes fieldprovenance consumes. Other consumers
// (GetSystemTableDefinitions) use the richer schema.TableDefinition shape.
type fpSystemTableEntry struct {
	TableName string           `json:"tableName"`
	Columns   []fpFieldDeclRaw `json:"columns"`
}

// fpSpecObjectEntry is a narrow parse of standard_objects_spec.json and
// tenant_objects_spec.json — both share the {apiName, fields:[{apiName,
// isCompound, composedFrom}]} shape.
type fpSpecObjectEntry struct {
	APIName string           `json:"apiName"`
	Fields  []fpFieldDeclRaw `json:"fields"`
}

// fpFieldDeclRaw is the common raw JSON shape for a field entry across
// all three manifests. system_tables.json uses "name" as the key;
// standard/tenant specs use "apiName"; we accept either.
type fpFieldDeclRaw struct {
	Name         string   `json:"name,omitempty"`
	APIName      string   `json:"apiName,omitempty"`
	IsCompound   bool     `json:"isCompound,omitempty"`
	ComposedFrom []string `json:"composedFrom,omitempty"`
}

// apiName normalizes to the field's identifier regardless of which key
// the manifest used.
func (r fpFieldDeclRaw) apiName() string {
	if r.APIName != "" {
		return r.APIName
	}
	return r.Name
}

// BuildFieldProvenanceClassifier parses the three embedded L1 manifests
// and returns a ready-to-use Classifier. The caller supplies the platform
// standard-field template (the APINames injected into every custom object
// by EnrichWithSystemFields + GetStandardFieldMetadata); any field name
// in that list is classified as FieldSourceManaged regardless of the
// owning object.
//
// This function does not touch global state. Call InitFieldProvenance to
// install the result as the package-level default classifier.
func BuildFieldProvenanceClassifier(platformStandardFields []string) (*fieldprovenance.Classifier, error) {
	sysDecls, err := parseFieldProvenanceSystemTables(systemTablesJSON)
	if err != nil {
		return nil, fmt.Errorf("fieldprovenance: parse system_tables.json: %w", err)
	}

	stdDecls, err := parseFieldProvenanceSpecObjects(packspec.StandardObjectsSpecJSON())
	if err != nil {
		return nil, fmt.Errorf("fieldprovenance: parse standard_objects_spec.json: %w", err)
	}

	var tntDecls []fieldprovenance.ObjectDeclaration
	if tntManifest := packspec.TenantFieldManifestJSON(); len(tntManifest) > 0 {
		tntDecls, err = parseFieldProvenanceSpecObjects(tntManifest)
		if err != nil {
			return nil, fmt.Errorf("fieldprovenance: parse tenant field manifest: %w", err)
		}
	}

	return fieldprovenance.BuildClassifier(sysDecls, stdDecls, tntDecls, platformStandardFields), nil
}

// InitFieldProvenance builds a Classifier from the embedded L1 manifests
// and installs it as the fieldprovenance package-level default. Must be
// called once at startup, before any consumer (ComputeIntrinsic Layer
// 1d/1e, describe emitters, persistence gates) issues its first classify
// call.
//
// Safe to call multiple times (later calls replace the default); a nil
// classifier resulting from a parse error is a bootstrap failure and is
// returned as an error rather than silently installed.
func InitFieldProvenance(platformStandardFields []string) error {
	c, err := BuildFieldProvenanceClassifier(platformStandardFields)
	if err != nil {
		return err
	}
	fieldprovenance.SetDefault(c)
	return nil
}

// MustInitFieldProvenance installs the default classifier using the
// platform-standard-field list returned by DefaultPlatformStandardFields,
// panicking on any parse error. Intended for test setup (`init()` in
// _test.go files) so unit / integration tests share the same classifier
// they would have in production. Not appropriate for production binaries
// — prefer InitFieldProvenance with an explicit error handler there.
func MustInitFieldProvenance() {
	if err := InitFieldProvenance(DefaultPlatformStandardFields()); err != nil {
		panic("fieldprovenance: " + err.Error())
	}
}

// parseFieldProvenanceSystemTables turns system_tables.json bytes into a
// slice of ObjectDeclaration ready for BuildClassifier. Unknown fields in
// the raw JSON are ignored (json.Unmarshal default).
func parseFieldProvenanceSystemTables(data []byte) ([]fieldprovenance.ObjectDeclaration, error) {
	var raw []fpSystemTableEntry
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, err
	}
	out := make([]fieldprovenance.ObjectDeclaration, 0, len(raw))
	for _, obj := range raw {
		if obj.TableName == "" {
			continue
		}
		out = append(out, fieldprovenance.ObjectDeclaration{
			APIName: obj.TableName,
			Fields:  fieldDeclsFromRaw(obj.Columns),
		})
	}
	return out, nil
}

// parseFieldProvenanceSpecObjects turns standard_objects_spec.json or
// tenant_objects_spec.json bytes into a slice of ObjectDeclaration. Both
// files use the {apiName, fields} shape so a single parser handles them.
func parseFieldProvenanceSpecObjects(data []byte) ([]fieldprovenance.ObjectDeclaration, error) {
	var raw []fpSpecObjectEntry
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, err
	}
	out := make([]fieldprovenance.ObjectDeclaration, 0, len(raw))
	for _, obj := range raw {
		if obj.APIName == "" {
			continue
		}
		out = append(out, fieldprovenance.ObjectDeclaration{
			APIName: obj.APIName,
			Fields:  fieldDeclsFromRaw(obj.Fields),
		})
	}
	return out, nil
}

// fieldDeclsFromRaw normalizes raw JSON field entries into the
// fieldprovenance.FieldDeclaration shape. Skips entries with empty names.
func fieldDeclsFromRaw(raw []fpFieldDeclRaw) []fieldprovenance.FieldDeclaration {
	out := make([]fieldprovenance.FieldDeclaration, 0, len(raw))
	for _, r := range raw {
		name := r.apiName()
		if name == "" {
			continue
		}
		fd := fieldprovenance.FieldDeclaration{
			APIName:    name,
			IsCompound: r.IsCompound,
		}
		if len(r.ComposedFrom) > 0 {
			fd.ComposedFrom = append([]string(nil), r.ComposedFrom...)
		}
		out = append(out, fd)
	}
	return out
}
