package catalogsnapshot

import (
	"fmt"
	"sort"
	"strings"

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

// CompareRuntimeView is the non-serving parity oracle. The caller captures one
// controlled legacy effective graph; this function compares every projected
// behavior fact without reading a repository or selecting a serving source.
func CompareRuntimeView(view *RuntimeMetadataView, expected ParityInput) ParityReport {
	if view == nil {
		return ParityReport{Differences: []ParityDifference{{Path: "view", Kind: "missing"}}}
	}

	expectedObjects := make(map[string][]byte, len(expected.Objects))
	expectedObjectCatalog := make(map[string]*models.ObjectMetadata, len(expected.Objects))
	for _, object := range expected.Objects {
		if object != nil {
			expectedObjectCatalog[foldIdentity(object.APIName)] = object
		}
	}
	for i, object := range expected.Objects {
		if object == nil {
			return ParityReport{Differences: []ParityDifference{{Path: fmt.Sprintf("objects[%d]", i), Kind: "invalid_expected", Expected: "non-nil object"}}}
		}
		record, err := buildRuntimeObject(object, view.doc.Profile, expectedObjectCatalog, view.doc.Schema)
		if err != nil {
			return ParityReport{Differences: []ParityDifference{{Path: "objects." + object.APIName, Kind: "invalid_expected", Expected: err.Error()}}}
		}
		data, err := marshalCanonical(record)
		if err != nil {
			return ParityReport{Differences: []ParityDifference{{Path: "objects." + object.APIName, Kind: "invalid_expected", Expected: err.Error()}}}
		}
		expectedObjects[foldIdentity(object.APIName)] = data
	}
	actualObjects := make(map[string][]byte, len(view.doc.Objects))
	for _, object := range view.doc.Objects {
		data, _ := marshalCanonical(object)
		actualObjects[foldIdentity(object.APIName)] = data
	}

	expectedRelationships := make(map[string][]byte, len(expected.Relationships))
	for i, relationship := range expected.Relationships {
		if relationship == nil {
			return ParityReport{Differences: []ParityDifference{{Path: fmt.Sprintf("relationships[%d]", i), Kind: "invalid_expected", Expected: "non-nil relationship"}}}
		}
		if view.doc.Profile != ProjectionRecordQueryV1 && view.doc.Profile != ProjectionRecordMutationV1 && !relationship.IsSharingParent {
			continue
		}
		record := runtimeRelationshipRecord{
			ChildObjectAPIName:  relationship.ChildObjectAPIName,
			ParentObjectAPIName: relationship.ParentObjectAPIName, FieldAPIName: relationship.FieldAPIName,
			RelationshipType: relationship.RelationshipType, IsSharingParent: relationship.IsSharingParent,
		}
		if view.doc.Profile == ProjectionRecordQueryV1 {
			record.RelationshipName = relationship.RelationshipName
		}
		if view.doc.Profile == ProjectionRecordMutationV1 {
			record.RelationshipName = relationship.RelationshipName
			record.Mutation = &runtimeMutationRelationshipRecord{
				CascadeDelete: relationship.CascadeDelete, RestrictedDelete: relationship.RestrictedDelete,
			}
		}
		data, _ := marshalCanonical(record)
		expectedRelationships[runtimeRelationshipKey(record)] = data
	}
	actualRelationships := make(map[string][]byte, len(view.doc.Relationships))
	for _, relationship := range view.doc.Relationships {
		data, _ := marshalCanonical(relationship)
		actualRelationships[runtimeRelationshipKey(relationship)] = data
	}

	expectedRules := make(map[string][]byte, len(expected.SharingRules))
	for i, rule := range expected.SharingRules {
		if rule == nil {
			return ParityReport{Differences: []ParityDifference{{Path: fmt.Sprintf("sharingRules[%d]", i), Kind: "invalid_expected", Expected: "non-nil sharing rule"}}}
		}
		record := runtimeSharingRuleRecord{
			ObjectAPIName: rule.ObjectAPIName,
			Criteria:      rule.Criteria, AccessLevel: rule.AccessLevel,
			SharedFromRoleID: rule.SharedFromRoleID, SharedFromGroupID: rule.SharedFromGroupID,
			ShareWithRoleID: rule.ShareWithRoleID, ShareWithGroupID: rule.ShareWithGroupID,
		}
		data, _ := marshalCanonical(record)
		expectedRules[runtimeSharingRuleKey(record)] = data
	}
	actualRules := make(map[string][]byte, len(view.doc.SharingRules))
	for _, rule := range view.doc.SharingRules {
		data, _ := marshalCanonical(rule)
		actualRules[runtimeSharingRuleKey(rule)] = data
	}

	differences := []ParityDifference{}
	differences = append(differences, compareParityCollection("objects", expectedObjects, actualObjects)...)
	differences = append(differences, compareParityCollection("relationships", expectedRelationships, actualRelationships)...)
	differences = append(differences, compareParityCollection("sharingRules", expectedRules, actualRules)...)
	if len(view.doc.Definitions) != 0 {
		differences = append(differences, ParityDifference{
			Path: "definitions", Kind: "unexpected", Actual: fmt.Sprintf("%d definition(s)", len(view.doc.Definitions)),
		})
	}
	sort.Slice(differences, func(i, j int) bool {
		if differences[i].Path == differences[j].Path {
			return differences[i].Kind < differences[j].Kind
		}
		return differences[i].Path < differences[j].Path
	})
	return ParityReport{Differences: differences}
}

func runtimeSharingRuleKey(record runtimeSharingRuleRecord) string {
	parts := []string{
		foldIdentity(record.ObjectAPIName), record.Criteria, foldIdentity(record.AccessLevel),
		optionalIdentity(record.SharedFromRoleID), optionalIdentity(record.SharedFromGroupID),
		optionalIdentity(record.ShareWithRoleID), optionalIdentity(record.ShareWithGroupID),
	}
	return strings.Join(parts, "\x00")
}

func optionalIdentity(value *string) string {
	if value == nil {
		return ""
	}
	return foldIdentity(*value)
}

func compareParityCollection(path string, expected, actual map[string][]byte) []ParityDifference {
	differences := []ParityDifference{}
	for key, want := range expected {
		got, exists := actual[key]
		if !exists {
			differences = append(differences, ParityDifference{Path: path + "." + key, Kind: "missing", Expected: string(want)})
			continue
		}
		if string(want) != string(got) {
			differences = append(differences, ParityDifference{Path: path + "." + key, Kind: "mismatch", Expected: string(want), Actual: string(got)})
		}
	}
	for key, got := range actual {
		if _, exists := expected[key]; !exists {
			differences = append(differences, ParityDifference{Path: path + "." + key, Kind: "unexpected", Actual: string(got)})
		}
	}
	return differences
}
