package metadata

import (
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/metadatair"
)

// ObjectComponentIRSource describes the source component that produced one
// object metadata projection. It is provenance for the neutral IR header, not a
// second metadata authority.
type ObjectComponentIRSource struct {
	Owner          metadatair.Owner
	URI            string
	Component      string
	Dialect        string
	DialectVersion string
	Annotations    map[string]string
}

// ObjectFieldIdentityProjection is the runtime-facing object/field identity
// subset currently represented by neutral IR node headers.
type ObjectFieldIdentityProjection struct {
	ObjectAPIName string
	FieldAPINames []string
}

// LowerObjectComponentHeaders lowers one object component projection into
// neutral IR object/field node headers. It does not switch any runtime read path
// and it does not treat the runtime struct as the source of truth; callers must
// pass the source component coordinates explicitly.
func LowerObjectComponentHeaders(schema *models.ObjectMetadata, source ObjectComponentIRSource) (metadatair.Document, error) {
	identity, err := runtimeObjectFieldIdentity(schema)
	if err != nil {
		return metadatair.Document{}, err
	}

	nodes := make([]metadatair.NodeHeader, 0, len(identity.FieldAPINames)+1)
	nodes = append(nodes, objectComponentNode(source, metadatair.NodeKindObject, metadatair.NodeID(identity.ObjectAPIName), "/"))
	for _, fieldAPIName := range identity.FieldAPINames {
		fieldID := metadatair.NodeID(identity.ObjectAPIName + "." + fieldAPIName)
		nodes = append(nodes, objectComponentNode(source, metadatair.NodeKindField, fieldID, "/fields/"+irPathSegment(fieldAPIName)))
	}

	doc := metadatair.Document{
		Version: neutralMetadataIRVersion(),
		Nodes:   nodes,
	}
	if err := doc.Validate(); err != nil {
		return metadatair.Document{}, err
	}
	return doc, nil
}

// ProjectObjectFieldIdentityHeaders projects the neutral object/field headers
// back into the runtime-facing identity order used by existing metadata
// consumers. The projection is intentionally limited to facts present in the
// P2 header contract.
func ProjectObjectFieldIdentityHeaders(doc metadatair.Document) (ObjectFieldIdentityProjection, error) {
	if err := doc.Validate(); err != nil {
		return ObjectFieldIdentityProjection{}, err
	}

	var out ObjectFieldIdentityProjection
	for _, node := range doc.Nodes {
		switch node.Kind {
		case metadatair.NodeKindObject:
			if node.Source.Format != metadatair.SourceFormatPackComponentJSON {
				return ObjectFieldIdentityProjection{}, fmt.Errorf("metadata IR object node %q source format %q is not %q", node.ID, node.Source.Format, metadatair.SourceFormatPackComponentJSON)
			}
			if out.ObjectAPIName != "" {
				return ObjectFieldIdentityProjection{}, fmt.Errorf("metadata IR identity projection requires exactly one object node")
			}
			out.ObjectAPIName = string(node.ID)
		case metadatair.NodeKindField:
			if node.Source.Format != metadatair.SourceFormatPackComponentJSON {
				return ObjectFieldIdentityProjection{}, fmt.Errorf("metadata IR field node %q source format %q is not %q", node.ID, node.Source.Format, metadatair.SourceFormatPackComponentJSON)
			}
			out.FieldAPINames = append(out.FieldAPINames, string(node.ID))
		default:
			return ObjectFieldIdentityProjection{}, fmt.Errorf("metadata IR identity projection requires only object/field nodes, got %s node %q", node.Kind, node.ID)
		}
	}
	if out.ObjectAPIName == "" {
		return ObjectFieldIdentityProjection{}, fmt.Errorf("metadata IR identity projection requires an object node")
	}
	prefix := out.ObjectAPIName + "."
	for i, fieldID := range out.FieldAPINames {
		fieldAPIName, ok := strings.CutPrefix(fieldID, prefix)
		if !ok || fieldAPIName == "" {
			return ObjectFieldIdentityProjection{}, fmt.Errorf("metadata IR field node %q does not belong to object %s", fieldID, out.ObjectAPIName)
		}
		out.FieldAPINames[i] = fieldAPIName
	}
	return out, nil
}

func runtimeObjectFieldIdentity(schema *models.ObjectMetadata) (ObjectFieldIdentityProjection, error) {
	if schema == nil {
		return ObjectFieldIdentityProjection{}, fmt.Errorf("metadata IR object lowering requires object metadata")
	}
	objectAPIName := strings.TrimSpace(schema.APIName)
	if objectAPIName == "" {
		return ObjectFieldIdentityProjection{}, fmt.Errorf("metadata IR object lowering requires object API name")
	}
	out := ObjectFieldIdentityProjection{
		ObjectAPIName: objectAPIName,
		FieldAPINames: make([]string, 0, len(schema.Fields)),
	}
	for _, field := range schema.Fields {
		fieldAPIName := strings.TrimSpace(field.APIName)
		if fieldAPIName == "" {
			return ObjectFieldIdentityProjection{}, fmt.Errorf("metadata IR object %s field lowering requires field API name", objectAPIName)
		}
		out.FieldAPINames = append(out.FieldAPINames, fieldAPIName)
	}
	return out, nil
}

func objectComponentNode(source ObjectComponentIRSource, kind metadatair.NodeKind, id metadatair.NodeID, path string) metadatair.NodeHeader {
	return metadatair.NodeHeader{
		ID:    id,
		Kind:  kind,
		Owner: source.Owner,
		Source: metadatair.SourceLocation{
			Format:         metadatair.SourceFormatPackComponentJSON,
			URI:            source.URI,
			Component:      source.Component,
			Path:           path,
			Dialect:        source.Dialect,
			DialectVersion: source.DialectVersion,
			Annotations:    cloneIRAnnotations(source.Annotations),
		},
		Boundary: metadatair.LoweringBoundary{
			From: metadatair.SourceFormatPackComponentJSON,
			To:   metadatair.TargetSurfaceNeutralIR,
		},
	}
}

func irPathSegment(value string) string {
	value = strings.ReplaceAll(value, "~", "~0")
	return strings.ReplaceAll(value, "/", "~1")
}

func cloneIRAnnotations(in map[string]string) map[string]string {
	if len(in) == 0 {
		return nil
	}
	out := make(map[string]string, len(in))
	for key, value := range in {
		out[key] = value
	}
	return out
}

func neutralMetadataIRVersion() metadatair.Version {
	return metadatair.Version{Major: 1, Minor: 0, Patch: 0}
}
