package sfcrmappservice

// This is the NexusCRM layout-editor extension, not a Salesforce standard
// resource. Its existing candidates envelope is retained. L1 assembles it from
// one effective metadata view and the existing live object-list-fields read.

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/url"
	"sort"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
)

type relatedListCandidate struct {
	ID             string   `json:"id"`
	ObjectAPIName  string   `json:"objectApiName"`
	LookupField    string   `json:"lookupField"`
	Label          string   `json:"label"`
	SecondaryLabel string   `json:"secondaryLabel"`
	Fields         []string `json:"fields"`
	FieldLabel     string   `json:"fieldLabel"`
	IsSystem       bool     `json:"isSystem"`
	IsRecommended  bool     `json:"isRecommended"`
	Bucket         string   `json:"bucket"`
}

type relatedListPresentation struct {
	Rules         []relatedListPresentationRule    `json:"rules"`
	Relationships []relatedListPresentationColumns `json:"relationships"`
}

type relatedListPresentationRule struct {
	Key                 string                    `json:"key,omitempty"`
	ParentObjectAPIName string                    `json:"parentObjectApiName"`
	ChildObjectAPIName  string                    `json:"childObjectApiName"`
	LookupField         string                    `json:"lookupField"`
	Bucket              string                    `json:"bucket,omitempty"`
	SortOrder           int                       `json:"sortOrder,omitempty"`
	AppendIfMissing     bool                      `json:"appendIfMissing,omitempty"`
	Label               string                    `json:"label,omitempty"`
	SecondaryLabel      string                    `json:"secondaryLabel,omitempty"`
	LookupFieldLabel    string                    `json:"lookupFieldLabel,omitempty"`
	Fields              []string                  `json:"fields,omitempty"`
	Resolution          *appRelatedListResolution `json:"resolution,omitempty"`
}

type relatedListPresentationColumns struct {
	ChildObjectAPIName  string  `json:"childObjectApiName"`
	ParentObjectAPIName string  `json:"parentObjectApiName"`
	FieldAPIName        string  `json:"fieldApiName"`
	RelatedListLabel    *string `json:"relatedListLabel,omitempty"`
	RelatedListFields   *string `json:"relatedListFields,omitempty"`
}

func relatedListIdentity(object, field string) string {
	return strings.ToLower(object + "." + field)
}

func relatedListPresentationFromView(view catalogsemantic.AppMetadataViewV3) (relatedListPresentation, error) {
	var selected *catalogsemantic.EffectiveMetadataComponent
	for index := range view.EffectiveComponents {
		component := &view.EffectiveComponents[index]
		if component.ComponentKind != catalogsemantic.EffectiveRelatedListPresentationComponentKind {
			continue
		}
		if selected != nil || component.ComponentFormatVersion != 1 || component.BusinessKey != catalogsemantic.EffectiveRelatedListPresentationComponentBusinessKey {
			return relatedListPresentation{}, unavailablePageLayout()
		}
		selected = component
	}
	if selected == nil {
		return relatedListPresentation{}, unavailablePageLayout()
	}
	var result relatedListPresentation
	decoder := json.NewDecoder(bytes.NewReader(selected.CanonicalBehavior))
	decoder.DisallowUnknownFields()
	if decoder.Decode(&result) != nil || requireJSONEOF(decoder) != nil || result.Rules == nil || result.Relationships == nil {
		return relatedListPresentation{}, unavailablePageLayout()
	}
	seen := make(map[string]bool)
	for _, rule := range result.Rules {
		key := strings.ToLower(rule.ParentObjectAPIName) + "\x00" + relatedListIdentity(rule.ChildObjectAPIName, rule.LookupField)
		if seen[key] || !validSearchIdentifier(rule.ParentObjectAPIName) || !validSearchIdentifier(rule.ChildObjectAPIName) || !validSearchIdentifier(rule.LookupField) {
			return relatedListPresentation{}, unavailablePageLayout()
		}
		seen[key] = true
		switch rule.Bucket {
		case "", "recommended", "additional", "system":
		default:
			return relatedListPresentation{}, unavailablePageLayout()
		}
	}
	clear(seen)
	for _, columns := range result.Relationships {
		key := strings.ToLower(columns.ParentObjectAPIName) + "\x00" + relatedListIdentity(columns.ChildObjectAPIName, columns.FieldAPIName)
		if seen[key] || !validSearchIdentifier(columns.ParentObjectAPIName) || !validSearchIdentifier(columns.ChildObjectAPIName) || !validSearchIdentifier(columns.FieldAPIName) {
			return relatedListPresentation{}, unavailablePageLayout()
		}
		seen[key] = true
	}
	return result, nil
}

func (service *Service) serveRelatedListCandidates(writer http.ResponseWriter, request *http.Request) bool {
	parts := strings.Split(strings.Trim(request.URL.EscapedPath(), "/"), "/")
	if len(parts) != 6 || parts[0] != "services" || parts[1] != "data" || parts[3] != "ui-api" || parts[4] != "related-list-candidates" {
		return false
	}
	if _, ok := recordViewVersions[parts[2]]; !ok {
		return false
	}
	if request.Method != http.MethodGet {
		writer.Header().Set("Allow", http.MethodGet)
		writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
		return true
	}
	service.requests.Add(1)
	if service.draining.Load() {
		service.drainRejected.Add(1)
		writeRuntimeFailure(writer, unavailablePageLayout())
		return true
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		service.localRejected.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return true
	}
	objectID, err := url.PathUnescape(parts[5])
	if err != nil || !validSearchIdentifier(objectID) {
		service.localRejected.Add(1)
		writeCRMError(writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest, "Invalid object API name")
		return true
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()
	candidates, err := service.readRelatedListCandidates(ctx, bearer, objectID)
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	service.successes.Add(1)
	writeJSON(writer, http.StatusOK, struct {
		Candidates []relatedListCandidate `json:"candidates"`
	}{candidates})
	return true
}

func (service *Service) readRelatedListCandidates(ctx context.Context, bearer, objectID string) ([]relatedListCandidate, error) {
	if service.permissionMetadata == nil {
		return nil, unavailablePageLayout()
	}
	view, specs, err := readObjectSpecs(ctx, service.appMetadataV3)
	if err != nil {
		return nil, err
	}
	permissionRequest := systemPermissionAuthorityRequest()
	revision := view.Receipts.CatalogRevision
	permissionRequest.MinCatalogRevision = &revision
	permissions, err := service.permissionMetadata.readEffective(ctx, bearer, permissionRequest)
	if err != nil {
		return nil, err
	}
	if err := permissionsemantic.ValidateResponseForRequest(permissionRequest, permissions); err != nil {
		return nil, err
	}
	if err := validateSearchMetadataAuthority(ctx, service.reconcileAppMetadataV3, view, permissions.Receipts); err != nil {
		return nil, err
	}
	if !toolingQueryPermissionGranted(permissions.SystemPermissions) {
		return nil, &commonsemantic.Failure{Category: commonsemantic.FailureForbidden}
	}
	parent, found := specs[strings.ToLower(objectID)]
	if !found {
		return nil, &commonsemantic.Failure{Category: commonsemantic.FailureMissing}
	}
	if !describeAuthorityIsCurrent(parent) {
		return nil, unavailablePageLayout()
	}
	presentation, err := relatedListPresentationFromView(view)
	if err != nil {
		return nil, err
	}
	candidates, targetObjects, err := discoverRelatedListCandidates(parent.APIName, specs, presentation)
	if err != nil {
		return nil, err
	}
	history, hasHistory := historyRelatedListCandidate(parent, specs)
	if len(candidates) == 0 {
		if hasHistory {
			return []relatedListCandidate{history}, nil
		}
		return []relatedListCandidate{}, nil
	}
	listFields, receipts, err := service.readCurrentObjectListFields(ctx, bearer, view, targetObjects)
	if err != nil {
		return nil, err
	}
	if !sameAppMetadataAuthority(permissions.Receipts, receipts) {
		return nil, unavailablePageLayout()
	}
	result, err := assembleRelatedListCandidates(candidates, specs, listFields)
	if err != nil {
		return nil, err
	}
	if hasHistory {
		result = append(result, history)
	}
	return result, nil
}

type relatedListCandidateInput struct {
	candidate    relatedListCandidate
	targetObject string
	sortOrder    int
}

func discoverRelatedListCandidates(parent string, specs map[string]searchObjectSpec, presentation relatedListPresentation) ([]relatedListCandidateInput, []string, error) {
	byID := make(map[string]relatedListCandidateInput)
	addCandidate := func(child searchObjectSpec, field searchFieldSpec) {
		id := relatedListIdentity(child.APIName, field.APIName)
		label := child.PluralLabel
		if label == "" {
			label = child.Label
		}
		byID[id] = relatedListCandidateInput{candidate: relatedListCandidate{
			ID: child.APIName + "-" + field.APIName, ObjectAPIName: child.APIName, LookupField: field.APIName, Label: label,
			SecondaryLabel: "via " + field.Label, FieldLabel: field.Label, Fields: []string{}, Bucket: "additional",
		}, targetObject: child.APIName}
	}
	// Transaction-current Layout authoring resolves relations from field
	// ReferenceTo. Presentation rules and relationship columns do not own shape.
	for _, child := range specs {
		for _, field := range child.Fields {
			for _, target := range field.ReferenceTo {
				if strings.EqualFold(target, parent) {
					addCandidate(child, field)
					break
				}
			}
		}
	}
	for _, columns := range presentation.Relationships {
		if !strings.EqualFold(columns.ParentObjectAPIName, parent) {
			continue
		}
		id := relatedListIdentity(columns.ChildObjectAPIName, columns.FieldAPIName)
		input, ok := byID[id]
		if !ok {
			continue
		}
		if columns.RelatedListLabel != nil && strings.TrimSpace(*columns.RelatedListLabel) != "" {
			input.candidate.Label = *columns.RelatedListLabel
		}
		if columns.RelatedListFields != nil && strings.TrimSpace(*columns.RelatedListFields) != "" {
			raw := strings.TrimSpace(*columns.RelatedListFields)
			if strings.HasPrefix(raw, "[") {
				if json.Unmarshal([]byte(raw), &input.candidate.Fields) != nil {
					return nil, nil, unavailablePageLayout()
				}
			} else {
				input.candidate.Fields = strings.Split(raw, ",")
			}
		}
		byID[id] = input
	}
	for _, rule := range presentation.Rules {
		if !strings.EqualFold(rule.ParentObjectAPIName, parent) {
			continue
		}
		id := relatedListIdentity(rule.ChildObjectAPIName, rule.LookupField)
		input, ok := byID[id]
		if !ok {
			// Presentation rules do not create schema or authorable relations.
			// In particular, AppendIfMissing cannot mint a display-only entry
			// that Layout authoring or record-UI authorization would reject.
			continue
		}
		if rule.Resolution != nil {
			if !relatedListCandidateResolutionIsCurrent(parent, *rule.Resolution, specs) {
				return nil, nil, unavailablePageLayout()
			}
			input.targetObject = rule.Resolution.Query.ObjectAPIName
		}
		if rule.Label != "" {
			input.candidate.Label = rule.Label
		}
		if rule.LookupFieldLabel != "" {
			input.candidate.FieldLabel = rule.LookupFieldLabel
			input.candidate.SecondaryLabel = "via " + rule.LookupFieldLabel
		}
		if rule.SecondaryLabel != "" {
			input.candidate.SecondaryLabel = rule.SecondaryLabel
		}
		input.candidate.Bucket = rule.Bucket
		if input.candidate.Bucket == "" {
			// An applicable rule defaults to recommended; an unruled
			// relationship keeps discovery's additional classification.
			input.candidate.Bucket = "recommended"
		}
		if len(rule.Fields) > 0 {
			input.candidate.Fields = append([]string(nil), rule.Fields...)
		}
		input.candidate.IsSystem = input.candidate.Bucket == "system"
		input.candidate.IsRecommended = input.candidate.Bucket == "recommended"
		input.sortOrder = rule.SortOrder
		byID[id] = input
	}
	inputs := make([]relatedListCandidateInput, 0, len(byID))
	objects := make([]string, 0, len(byID))
	for _, input := range byID {
		inputs = append(inputs, input)
		objects = append(objects, input.targetObject)
	}
	sort.Slice(inputs, func(i, j int) bool {
		if inputs[i].sortOrder != inputs[j].sortOrder {
			return inputs[i].sortOrder < inputs[j].sortOrder
		}
		if inputs[i].candidate.Label != inputs[j].candidate.Label {
			return inputs[i].candidate.Label < inputs[j].candidate.Label
		}
		return inputs[i].candidate.ID < inputs[j].candidate.ID
	})
	return inputs, objects, nil
}

func assembleRelatedListCandidates(inputs []relatedListCandidateInput, specs map[string]searchObjectSpec, listFields map[string][]string) ([]relatedListCandidate, error) {
	result := make([]relatedListCandidate, 0, len(inputs))
	for _, input := range inputs {
		spec, ok := specs[strings.ToLower(input.targetObject)]
		if !ok || !describeAuthorityIsCurrent(spec) {
			return nil, unavailablePageLayout()
		}
		fields, found := listFields[strings.ToLower(spec.APIName)]
		if !found {
			return nil, unavailablePageLayout()
		}
		if len(input.candidate.Fields) > 0 {
			fields = input.candidate.Fields
		}
		if len(fields) == 0 {
			fields = []string{spec.DisplayNameField, "CreatedDate"}
		}
		display, found := specs[strings.ToLower(input.candidate.ObjectAPIName)]
		if !found || !describeAuthorityIsCurrent(display) {
			return nil, unavailablePageLayout()
		}
		candidate := input.candidate
		candidate.Fields = make([]string, 0, len(fields))
		seen := make(map[string]bool)
		for _, field := range fields {
			// Saved related-list columns are direct display-object fields.
			// A Runtime resolution must also be able to read those columns.
			column, valid := layoutFieldSpec(display, strings.TrimSpace(field))
			_, readable := layoutFieldSpec(spec, strings.TrimSpace(field))
			if valid && readable && !seen[strings.ToLower(column.APIName)] {
				candidate.Fields = append(candidate.Fields, column.APIName)
				seen[strings.ToLower(column.APIName)] = true
			}
		}
		result = append(result, candidate)
	}
	return result, nil
}

// A resolved list still needs every read target required by layout authorization.
// Creation may be disabled independently by that owner; it cannot hide a list.
func relatedListCandidateResolutionIsCurrent(parent string, resolution appRelatedListResolution, specs map[string]searchObjectSpec) bool {
	references := []struct{ object, field string }{
		{resolution.Query.ObjectAPIName, resolution.Query.LookupField},
		{resolution.Detail.ObjectAPIName, ""},
		{resolution.List.ObjectAPIName, resolution.List.FilterField},
	}
	if resolution.Query.LookupField == "" || resolution.List.FilterField == "" {
		return false
	}
	for _, field := range []string{resolution.Query.ParentFieldAPIName, resolution.List.ParentFieldAPIName} {
		if field != "" {
			references = append(references, struct{ object, field string }{parent, field})
		}
	}
	for _, reference := range references {
		spec, found := specs[strings.ToLower(reference.object)]
		if !found || !describeAuthorityIsCurrent(spec) {
			return false
		}
		if reference.field != "" {
			if _, found := layoutFieldSpec(spec, reference.field); !found {
				return false
			}
		}
	}
	return true
}
