package bootstrapimpl

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

	"github.com/pingcap-inc/nexuscrm-controlplane/packspec"
	"github.com/nexuscrm/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.GetCustomObjectSystemColumns() — the custom-object
//     audit / identity columns plus RecordTypeId.
//   - 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 {
	cols := persistence.GetCustomObjectSystemColumns()

	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 object-spec payloads. System specs,
// source-backed pack specs, and legacy tenant manifests all 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"`
	CompoundFieldName     string   `json:"compoundFieldName,omitempty"`
	CompoundComponentRole string   `json:"compoundComponentRole,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 only the L0 system object spec plus
// legacy tenant manifests and returns a ready-to-use Classifier. Installed L1
// app packs are added by RegisterInstalledRuntimeMetadata /
// RegisterRuntimeMetadataForInstall, not by this bootstrap default.
// 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) {
	systemSpec, err := packspec.StandardObjectsSpecByCategoryJSON("system")
	if err != nil {
		return nil, fmt.Errorf("fieldprovenance: load L0 system object spec: %w", err)
	}
	return BuildFieldProvenanceClassifierFromStandardObjectSpecs(platformStandardFields, systemSpec)
}

func BuildFieldProvenanceClassifierFromStandardObjectSpecs(platformStandardFields []string, objectSpecPayloads ...[]byte) (*fieldprovenance.Classifier, error) {
	sysDecls, err := parseFieldProvenanceSystemTables(systemTablesJSON)
	if err != nil {
		return nil, fmt.Errorf("fieldprovenance: parse system_tables.json: %w", err)
	}

	var stdDecls []fieldprovenance.ObjectDeclaration
	for _, payload := range objectSpecPayloads {
		decls, err := parseFieldProvenanceSpecObjects(payload)
		if err != nil {
			return nil, fmt.Errorf("fieldprovenance: parse standard object spec payload: %w", err)
		}
		stdDecls = append(stdDecls, decls...)
	}

	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
}

func InitFieldProvenanceFromStandardObjectSpecs(platformStandardFields []string, objectSpecPayloads ...[]byte) error {
	c, err := BuildFieldProvenanceClassifierFromStandardObjectSpecs(platformStandardFields, objectSpecPayloads...)
	if err != nil {
		return err
	}
	fieldprovenance.SetDefault(c)
	return nil
}

// InitFieldProvenance builds a Classifier from L0 system metadata and installs
// it as the fieldprovenance package-level default. App-pack payloads must enter
// through the installed runtime metadata context, not this process 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.
// Source-backed app packs may declare compound membership child-side through
// compoundFieldName (for example child field -> compound parent). Synthesize the
// parent declaration here so runtime compound reads use the installed payload
// instead of the old embedded packspec fallback.
func fieldDeclsFromRaw(raw []fpFieldDeclRaw) []fieldprovenance.FieldDeclaration {
	out := make([]fieldprovenance.FieldDeclaration, 0, len(raw))
	indexByLowerName := map[string]int{}
	componentsByParent := map[string][]string{}
	parentNameByLower := map[string]string{}
	for _, r := range raw {
		name := r.apiName()
		if name == "" {
			continue
		}
		fd := fieldprovenance.FieldDeclaration{
			APIName:               name,
			IsCompound:            r.IsCompound,
			CompoundFieldName:     strings.TrimSpace(r.CompoundFieldName),
			CompoundComponentRole: strings.TrimSpace(r.CompoundComponentRole),
		}
		if len(r.ComposedFrom) > 0 {
			fd.ComposedFrom = append([]string(nil), r.ComposedFrom...)
		}
		indexByLowerName[canonicalFPFieldName(name)] = len(out)
		out = append(out, fd)

		parent := strings.TrimSpace(r.CompoundFieldName)
		parentKey := canonicalFPFieldName(parent)
		if parentKey == "" || parentKey == canonicalFPFieldName(name) {
			continue
		}
		if parentNameByLower[parentKey] == "" {
			parentNameByLower[parentKey] = parent
		}
		componentsByParent[parentKey] = append(componentsByParent[parentKey], name)
	}

	for parentKey, components := range componentsByParent {
		if len(components) == 0 {
			continue
		}
		if idx, ok := indexByLowerName[parentKey]; ok {
			out[idx].IsCompound = true
			if len(out[idx].ComposedFrom) == 0 {
				out[idx].ComposedFrom = append([]string(nil), components...)
			}
			continue
		}
		out = append(out, fieldprovenance.FieldDeclaration{
			APIName:      parentNameByLower[parentKey],
			IsCompound:   true,
			ComposedFrom: append([]string(nil), components...),
		})
	}
	return out
}

func canonicalFPFieldName(name string) string {
	return strings.ToLower(strings.TrimSpace(name))
}
