package catalogreconcile

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"sort"
	"strings"

	metadatafeature "github.com/nexuscrm/metadata"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
	"github.com/pingcap-inc/nexuscrm-controlplane/strictjson"
)

// objectSpecReleaseComponentKind is the neutral, immutable object metadata
// component already carried by a verified AppRelease. Catalog consumes only
// the generic compound membership declared by this component; it never reads a
// product checkout or installs a process-global L1 classifier.
const objectSpecReleaseComponentKind = "metadata.pack.object-spec.v1"

type objectSpecReleaseObject struct {
	APIName string                   `json:"apiName"`
	Fields  []objectSpecReleaseField `json:"fields"`
}

type objectSpecReleaseField struct {
	APIName               string   `json:"apiName"`
	IsNameField           bool     `json:"isNameField,omitempty"`
	IsCompound            bool     `json:"isCompound,omitempty"`
	ComposedFrom          []string `json:"composedFrom,omitempty"`
	CompoundFieldName     string   `json:"compoundFieldName,omitempty"`
	CompoundComponentRole string   `json:"compoundComponentRole,omitempty"`
}

type compoundReleaseDeclaration struct {
	objectAPIName string
	fieldAPIName  string
	members       []string
	memberRoles   map[string]string
	isNameField   bool
}

// decorateEffectiveCompoundFields assembles both platform-owned and
// AppRelease-owned compound shape before Catalog compilation. Independent
// Runtime consumers treat the request-bound Catalog view as authoritative, so
// platform declarations must be published here rather than reintroduced by a
// second request-time schema decorator.
func decorateEffectiveCompoundFields(
	ctx context.Context,
	objects []*models.ObjectMetadata,
	release *appauthority.VerifiedPackage,
) ([]*models.ObjectMetadata, error) {
	decorator := metadatafeature.NewCompoundVirtualFieldDecorator(
		metadatafeature.PlatformCompoundVirtualFieldDeclarations()...,
	)
	platformDecorated := make([]*models.ObjectMetadata, len(objects))
	for index, object := range objects {
		decorated, err := decorator.DecorateSchema(ctx, object)
		if err != nil {
			return nil, fmt.Errorf("decorate platform compound fields for object %d: %w", index, err)
		}
		platformDecorated[index] = decorated
	}
	return decorateCompoundFieldsFromRelease(platformDecorated, release)
}

// decorateCompoundFieldsFromRelease binds compound membership to the same
// verified AppRelease and repository snapshot used for Catalog compilation.
// A declaration whose object or physical member is absent fails closed:
// combining a release declaration with a different installed metadata
// generation would be a mixed-revision Catalog view. A compound parent may be
// absent from the repository because it is virtual; in that case the verified
// AppRelease is its sole declaration authority and Catalog synthesizes it.
func decorateCompoundFieldsFromRelease(
	objects []*models.ObjectMetadata,
	release *appauthority.VerifiedPackage,
) ([]*models.ObjectMetadata, error) {
	declarations, err := readCompoundReleaseDeclarations(release)
	if err != nil {
		return nil, err
	}
	if len(declarations) == 0 {
		return objects, nil
	}

	byObject := make(map[string][]compoundReleaseDeclaration)
	for _, declaration := range declarations {
		key := canonicalCompoundIdentity(declaration.objectAPIName)
		byObject[key] = append(byObject[key], declaration)
	}

	result := make([]*models.ObjectMetadata, len(objects))
	seenObjects := make(map[string]struct{}, len(objects))
	for index, object := range objects {
		if object == nil {
			result[index] = nil
			continue
		}
		objectKey := canonicalCompoundIdentity(object.APIName)
		seenObjects[objectKey] = struct{}{}
		objectDeclarations := byObject[objectKey]
		if len(objectDeclarations) == 0 {
			result[index] = object
			continue
		}

		replacements := make([]models.FieldMetadata, 0, len(objectDeclarations))
		for _, declaration := range objectDeclarations {
			members := make([]string, 0, len(declaration.members))
			seenMembers := make(map[string]struct{}, len(declaration.members))
			for _, memberName := range declaration.members {
				member := models.FindField(object, memberName)
				if member == nil {
					return nil, fmt.Errorf(
						"AppRelease compound field %s.%s member %s is absent from the installed metadata revision",
						declaration.objectAPIName,
						declaration.fieldAPIName,
						memberName,
					)
				}
				memberKey := canonicalCompoundIdentity(member.APIName)
				if memberKey == canonicalCompoundIdentity(declaration.fieldAPIName) {
					return nil, fmt.Errorf(
						"AppRelease compound field %s.%s contains itself",
						declaration.objectAPIName,
						declaration.fieldAPIName,
					)
				}
				if _, duplicate := seenMembers[memberKey]; duplicate {
					return nil, fmt.Errorf(
						"AppRelease compound field %s.%s repeats member %s",
						declaration.objectAPIName,
						declaration.fieldAPIName,
						member.APIName,
					)
				}
				seenMembers[memberKey] = struct{}{}
				members = append(members, member.APIName)
			}

			parent := models.FindField(object, declaration.fieldAPIName)
			replacement := models.FieldMetadata{
				APIName:     declaration.fieldAPIName,
				Label:       declaration.fieldAPIName,
				Type:        constants.FieldTypeString,
				IsNameField: declaration.isNameField,
			}
			if parent != nil {
				replacement = *parent
				replacement.IsNameField = replacement.IsNameField || declaration.isNameField
			} else {
				// Only a parent absent from installed metadata is virtual. An
				// existing parent already owns a physical storage column; marking
				// it virtual would make Runtime's generic persistence filter drop
				// the correctly synthesized compound value before SQL execution.
				replacement.IsVirtual = true
				replacement.VirtualKind = constants.VirtualFieldCompound
			}
			// Compound parents are derived from their members. This intrinsic
			// write bound applies equally to physical and virtual parents and
			// must override permissive source metadata.
			replacement.Createable = false
			replacement.Updateable = false
			replacement.IsCompound = true
			replacement.ComposedFrom = members
			replacements = append(replacements, replacement)
		}
		result[index] = models.CloneWithAdditionalFields(object, replacements)
	}

	for _, declaration := range declarations {
		if _, present := seenObjects[canonicalCompoundIdentity(declaration.objectAPIName)]; present {
			continue
		}
		return nil, fmt.Errorf(
			"AppRelease compound object %s is absent from the installed metadata revision",
			declaration.objectAPIName,
		)
	}
	return result, nil
}

func readCompoundReleaseDeclarations(
	release *appauthority.VerifiedPackage,
) ([]compoundReleaseDeclaration, error) {
	if release == nil {
		return nil, nil
	}
	hasObjectSpec := false
	for _, component := range release.Bundle.Components {
		if component.ComponentKind == objectSpecReleaseComponentKind {
			hasObjectSpec = true
			break
		}
	}
	if !hasObjectSpec {
		return nil, nil
	}
	verified, err := appauthority.VerifyPackage(release.Package)
	if err != nil {
		return nil, fmt.Errorf("verify compound metadata AppRelease package: %w", err)
	}
	if verified.AppReleaseDigest != release.AppReleaseDigest {
		return nil, fmt.Errorf("compound metadata AppRelease identity disagrees with its verified package")
	}

	declarations := make([]compoundReleaseDeclaration, 0)
	seenObjects := make(map[string]string)
	for index, component := range verified.Bundle.Components {
		if component.ComponentKind != objectSpecReleaseComponentKind {
			continue
		}
		if component.ComponentFormatVersion != 1 {
			return nil, fmt.Errorf(
				"object-spec component %q has unsupported format version %d",
				component.ComponentID,
				component.ComponentFormatVersion,
			)
		}
		behavior := verified.ComponentBlobs[index].CanonicalBehavior
		if err := strictjson.RejectDuplicateKeys(behavior, "object-spec AppRelease component"); err != nil {
			return nil, fmt.Errorf("decode object-spec AppRelease component: %w", err)
		}
		decoder := json.NewDecoder(bytes.NewReader(behavior))
		var objects []objectSpecReleaseObject
		if err := decoder.Decode(&objects); err != nil {
			return nil, fmt.Errorf("decode object-spec AppRelease component: %w", err)
		}
		if err := decoder.Decode(&struct{}{}); err != io.EOF {
			if err == nil {
				err = fmt.Errorf("trailing JSON value")
			}
			return nil, fmt.Errorf("decode object-spec AppRelease component: %w", err)
		}
		for _, object := range objects {
			objectName := strings.TrimSpace(object.APIName)
			if objectName == "" {
				return nil, fmt.Errorf("object-spec AppRelease component contains an object without apiName")
			}
			objectKey := canonicalCompoundIdentity(objectName)
			if priorComponent, duplicate := seenObjects[objectKey]; duplicate {
				return nil, fmt.Errorf(
					"object-spec AppRelease components %q and %q both own object %s",
					priorComponent,
					component.ComponentID,
					objectName,
				)
			}
			seenObjects[objectKey] = component.ComponentID

			objectDeclarations, err := compoundDeclarationsForObject(object)
			if err != nil {
				return nil, err
			}
			declarations = append(declarations, objectDeclarations...)
		}
	}
	return declarations, nil
}

func compoundDeclarationsForObject(
	object objectSpecReleaseObject,
) ([]compoundReleaseDeclaration, error) {
	type parentDeclaration struct {
		name     string
		explicit []string
		derived  []string
		nameLike bool
	}
	parents := make(map[string]*parentDeclaration)
	fields := make(map[string]string, len(object.Fields))
	for _, field := range object.Fields {
		name := strings.TrimSpace(field.APIName)
		if name == "" {
			return nil, fmt.Errorf("object-spec AppRelease object %s contains a field without apiName", object.APIName)
		}
		fieldKey := canonicalCompoundIdentity(name)
		if _, duplicate := fields[fieldKey]; duplicate {
			return nil, fmt.Errorf("object-spec AppRelease object %s repeats field %s", object.APIName, name)
		}
		fields[fieldKey] = name
		if field.IsCompound || len(field.ComposedFrom) > 0 {
			parent := parents[fieldKey]
			if parent == nil {
				parent = &parentDeclaration{name: name}
				parents[fieldKey] = parent
			}
			parent.explicit = append([]string(nil), field.ComposedFrom...)
			parent.nameLike = parent.nameLike || field.IsNameField
		}
	}
	for _, field := range object.Fields {
		parentName := strings.TrimSpace(field.CompoundFieldName)
		componentRole := strings.TrimSpace(field.CompoundComponentRole)
		if componentRole != "" && parentName == "" {
			return nil, fmt.Errorf(
				"object-spec AppRelease field %s.%s declares compoundComponentRole without compoundFieldName",
				object.APIName, field.APIName,
			)
		}
		if parentName == "" {
			continue
		}
		parentKey := canonicalCompoundIdentity(parentName)
		parent := parents[parentKey]
		if parent == nil {
			parent = &parentDeclaration{name: parentName}
			parents[parentKey] = parent
		}
		parent.derived = append(parent.derived, strings.TrimSpace(field.APIName))
		parent.nameLike = parent.nameLike || field.IsNameField
	}

	declarations := make([]compoundReleaseDeclaration, 0, len(parents))
	parentKeys := make([]string, 0, len(parents))
	for parentKey := range parents {
		parentKeys = append(parentKeys, parentKey)
	}
	sort.Strings(parentKeys)
	for _, parentKey := range parentKeys {
		parent := parents[parentKey]
		canonicalParent := parent.name
		if declared, present := fields[parentKey]; present {
			canonicalParent = declared
		}
		members := parent.explicit
		if len(members) == 0 {
			members = parent.derived
		} else if len(parent.derived) > 0 && !sameCompoundMembers(members, parent.derived) {
			return nil, fmt.Errorf(
				"object-spec AppRelease compound parent %s.%s has conflicting parent and member declarations",
				object.APIName,
				canonicalParent,
			)
		}
		if len(members) == 0 {
			return nil, fmt.Errorf(
				"object-spec AppRelease compound parent %s.%s has no members",
				object.APIName,
				canonicalParent,
			)
		}
		memberRoles := make(map[string]string)
		for _, member := range members {
			memberKey := canonicalCompoundIdentity(member)
			for _, field := range object.Fields {
				if canonicalCompoundIdentity(field.APIName) != memberKey {
					continue
				}
				if role := strings.TrimSpace(field.CompoundComponentRole); role != "" {
					memberRoles[memberKey] = role
				}
				break
			}
		}
		declarations = append(declarations, compoundReleaseDeclaration{
			objectAPIName: strings.TrimSpace(object.APIName),
			fieldAPIName:  canonicalParent,
			members:       append([]string(nil), members...),
			memberRoles:   memberRoles,
			isNameField:   parent.nameLike,
		})
	}
	if err := validateCompoundDeclarationGraph(object.APIName, declarations); err != nil {
		return nil, err
	}
	return declarations, nil
}

// compoundComponentRolesFromRelease keeps authored member roles on the same
// verified AppRelease read path as compound membership. Catalog transports
// these neutral facts; it never derives product roles from field names.
func compoundComponentRolesFromRelease(
	release *appauthority.VerifiedPackage,
) (effectiveCompoundMemberRole, error) {
	declarations, err := readCompoundReleaseDeclarations(release)
	if err != nil {
		return nil, err
	}
	roles := make(effectiveCompoundMemberRole)
	for _, declaration := range declarations {
		objectKey := canonicalCompoundIdentity(declaration.objectAPIName)
		for memberKey, role := range declaration.memberRoles {
			if roles[objectKey] == nil {
				roles[objectKey] = make(map[string]string)
			}
			if prior, duplicate := roles[objectKey][memberKey]; duplicate && prior != role {
				return nil, fmt.Errorf(
					"object-spec AppRelease field %s.%s has conflicting compound component roles %q and %q",
					declaration.objectAPIName, memberKey, prior, role,
				)
			}
			roles[objectKey][memberKey] = role
		}
	}
	return roles, nil
}

// validateCompoundDeclarationGraph rejects recursive derived-field authority
// before it can be combined with an installed metadata revision. Members that
// are not themselves compound parents are leaves; only parent-to-parent edges
// participate in the object-local graph.
func validateCompoundDeclarationGraph(
	objectAPIName string,
	declarations []compoundReleaseDeclaration,
) error {
	parents := make(map[string]compoundReleaseDeclaration, len(declarations))
	for _, declaration := range declarations {
		parents[canonicalCompoundIdentity(declaration.fieldAPIName)] = declaration
	}
	const (
		unvisited uint8 = iota
		visiting
		visited
	)
	states := make(map[string]uint8, len(parents))
	var visit func(string) error
	visit = func(parentKey string) error {
		switch states[parentKey] {
		case visiting:
			return fmt.Errorf(
				"object-spec AppRelease object %s contains a recursive compound field graph at %s",
				strings.TrimSpace(objectAPIName),
				parents[parentKey].fieldAPIName,
			)
		case visited:
			return nil
		}
		states[parentKey] = visiting
		for _, member := range parents[parentKey].members {
			memberKey := canonicalCompoundIdentity(member)
			if _, compoundParent := parents[memberKey]; !compoundParent {
				continue
			}
			if err := visit(memberKey); err != nil {
				return err
			}
		}
		states[parentKey] = visited
		return nil
	}
	for _, declaration := range declarations {
		if err := visit(canonicalCompoundIdentity(declaration.fieldAPIName)); err != nil {
			return err
		}
	}
	return nil
}

func sameCompoundMembers(left, right []string) bool {
	if len(left) != len(right) {
		return false
	}
	for index := range left {
		if canonicalCompoundIdentity(left[index]) != canonicalCompoundIdentity(right[index]) {
			return false
		}
	}
	return true
}

func canonicalCompoundIdentity(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}
