package sfcrmappservice

import (
	"context"
	"sort"
	"strings"
	"unicode/utf8"

	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	pagelayoutsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/pagelayoutresolutionv1"
)

func validPageLayoutRecordTypeID(value string, allowEmpty bool) bool {
	return (allowEmpty || value != "") &&
		len(value) <= pagelayoutsemantic.MaxOpaqueIdentifierBytes &&
		utf8.ValidString(value) && strings.TrimSpace(value) == value
}

// RuntimeResolvePageLayoutFunc is the sole L1 read path for selecting a
// caller-effective PageLayout. Runtime derives Profile and Role from the
// authenticated session; L1 supplies only the object and optional record type.
type RuntimeResolvePageLayoutFunc func(
	context.Context,
	string,
	pagelayoutsemantic.Request,
) (pagelayoutsemantic.Response, error)

type RuntimeReadObjectListFieldsFunc func(
	context.Context,
	string,
	pagelayoutsemantic.ObjectListFieldsRequest,
) (pagelayoutsemantic.ObjectListFieldsResponse, error)

func pageLayoutListFieldObjectIDs(rootObjectID string, specs map[string]searchObjectSpec, layouts ...appPageLayout) []string {
	result := []string{rootObjectID}
	for _, layout := range layouts {
		for _, related := range layout.RelatedLists {
			objectID := related.ObjectAPIName
			if related.Resolution != nil {
				objectID = related.Resolution.Query.ObjectAPIName
			}
			if parent, virtual := virtualHistoryParent(objectID, specs); virtual {
				objectID = parent.APIName
			}
			result = append(result, objectID)
		}
	}
	return result
}

func (service *Service) readCurrentObjectListFields(
	ctx context.Context,
	bearer string,
	view catalogsemantic.AppMetadataViewV3,
	objectIDs []string,
) (map[string][]string, commonsemantic.Receipts, error) {
	if service.readObjectListFieldsRuntime == nil {
		return nil, commonsemantic.Receipts{}, unavailablePageLayout()
	}
	selected := make([]string, 0, len(objectIDs))
	seen := make(map[string]struct{}, len(objectIDs))
	for _, objectID := range objectIDs {
		key := strings.ToLower(objectID)
		if _, duplicate := seen[key]; duplicate {
			continue
		}
		seen[key] = struct{}{}
		selected = append(selected, objectID)
	}
	sort.Strings(selected)
	revision := view.Receipts.CatalogRevision
	request := pagelayoutsemantic.ObjectListFieldsRequest{
		ObjectIDs: selected, MinCatalogRevision: &revision,
	}
	response, err := service.readObjectListFieldsRuntime(ctx, bearer, request)
	if err != nil {
		return nil, commonsemantic.Receipts{}, err
	}
	if err := pagelayoutsemantic.ValidateObjectListFieldsResponse(request, response); err != nil {
		return nil, commonsemantic.Receipts{}, err
	}
	if err := validateSearchMetadataAuthority(
		ctx, service.reconcileAppMetadataV3, view, response.Receipts,
	); err != nil {
		return nil, commonsemantic.Receipts{}, err
	}
	result := make(map[string][]string, len(response.Objects))
	for _, object := range response.Objects {
		fields := make([]string, len(object.OrderedFieldIDs))
		copy(fields, object.OrderedFieldIDs)
		result[strings.ToLower(object.ObjectID)] = fields
	}
	return result, response.Receipts, nil
}

func (service *Service) resolvePageLayout(
	ctx context.Context,
	bearer string,
	view catalogsemantic.AppMetadataViewV3,
	objectID string,
	recordTypeID string,
) (appPageLayout, commonsemantic.Receipts, error) {
	recordTypeIDs := []string(nil)
	if recordTypeID = strings.TrimSpace(recordTypeID); recordTypeID != "" {
		if recordTypeID == masterRecordTypeID {
			recordTypeID = ""
		} else {
			recordTypeIDs = []string{recordTypeID}
		}
	}
	layouts, receipts, err := service.resolvePageLayouts(
		ctx, bearer, view, objectID, recordTypeIDs,
	)
	if err != nil {
		return appPageLayout{}, commonsemantic.Receipts{}, err
	}
	layout, found := layouts[recordTypeID]
	if !found {
		return appPageLayout{}, commonsemantic.Receipts{}, unavailablePageLayout()
	}
	return layout, receipts, nil
}

func (service *Service) resolvePageLayouts(
	ctx context.Context,
	bearer string,
	view catalogsemantic.AppMetadataViewV3,
	objectID string,
	recordTypeIDs []string,
) (map[string]appPageLayout, commonsemantic.Receipts, error) {
	if service.resolvePageLayoutRuntime == nil {
		return nil, commonsemantic.Receipts{}, unavailablePageLayout()
	}
	selectors := make([]string, 0, len(recordTypeIDs))
	seen := make(map[string]struct{}, len(recordTypeIDs))
	for _, recordTypeID := range recordTypeIDs {
		recordTypeID = strings.TrimSpace(recordTypeID)
		if recordTypeID == "" {
			return nil, commonsemantic.Receipts{}, unavailablePageLayout()
		}
		if _, duplicate := seen[recordTypeID]; duplicate {
			continue
		}
		seen[recordTypeID] = struct{}{}
		selectors = append(selectors, recordTypeID)
	}
	sort.Strings(selectors)
	revision := view.Receipts.CatalogRevision
	request := pagelayoutsemantic.Request{
		ObjectID: objectID, RecordTypeIDs: selectors, MinCatalogRevision: &revision,
	}
	response, err := service.resolvePageLayoutRuntime(ctx, bearer, request)
	if err != nil {
		return nil, commonsemantic.Receipts{}, err
	}
	if err := pagelayoutsemantic.ValidateResponseForRequest(request, response); err != nil {
		return nil, commonsemantic.Receipts{}, err
	}
	if err := validateSearchMetadataAuthority(
		ctx, service.reconcileAppMetadataV3, view, response.Receipts,
	); err != nil {
		return nil, commonsemantic.Receipts{}, err
	}
	documents := make(map[string]pagelayoutsemantic.ResolvedLayout, len(response.Layouts))
	for _, document := range response.Layouts {
		documents[document.LayoutID] = document
	}
	result := make(map[string]appPageLayout, len(response.Selections))
	for _, selection := range response.Selections {
		recordTypeID := ""
		if selection.RecordTypeID != nil {
			recordTypeID = *selection.RecordTypeID
		}
		document, found := documents[selection.LayoutID]
		if !found {
			return nil, commonsemantic.Receipts{}, unavailablePageLayout()
		}
		layout, decodeErr := decodeAppPageLayout(document.LayoutDocument)
		if decodeErr != nil || !strings.EqualFold(layout.ObjectAPIName, objectID) || layout.ID != selection.LayoutID {
			return nil, commonsemantic.Receipts{}, unavailablePageLayout()
		}
		if attachErr := attachRelatedListResolutions(&layout, document.RelatedLists); attachErr != nil {
			return nil, commonsemantic.Receipts{}, attachErr
		}
		if actionErr := resolveLayoutActionDefinitions(&layout, view); actionErr != nil {
			return nil, commonsemantic.Receipts{}, actionErr
		}
		result[recordTypeID] = layout
	}
	return result, response.Receipts, nil
}

func attachRelatedListResolutions(
	layout *appPageLayout,
	resolutions []pagelayoutsemantic.RelatedListResolution,
) error {
	if layout == nil {
		return unavailablePageLayout()
	}
	byDisplay := make(map[string]int, len(layout.RelatedLists))
	for index, related := range layout.RelatedLists {
		key := relatedListDisplayKey(related.ObjectAPIName, related.LookupField)
		if _, duplicate := byDisplay[key]; duplicate {
			return unavailablePageLayout()
		}
		byDisplay[key] = index
	}
	for _, resolved := range resolutions {
		key := relatedListDisplayKey(resolved.DisplayObjectID, resolved.DisplayLookupField)
		index, found := byDisplay[key]
		if !found || layout.RelatedLists[index].Resolution != nil {
			return unavailablePageLayout()
		}
		prefill := make(map[string]string, len(resolved.Create.PrefillMappings))
		for _, mapping := range resolved.Create.PrefillMappings {
			if _, duplicate := prefill[mapping.TargetFieldID]; duplicate {
				return unavailablePageLayout()
			}
			prefill[mapping.TargetFieldID] = mapping.SourceFieldID
		}
		if len(prefill) == 0 {
			prefill = nil
		}
		layout.RelatedLists[index].Resolution = &appRelatedListResolution{
			Query: appRelatedListQueryTarget{
				ObjectAPIName:      resolved.Query.ObjectID,
				LookupField:        resolved.Query.LookupField,
				ParentFieldAPIName: resolved.Query.ParentFieldID,
			},
			Detail: appRelatedListDetailTarget{ObjectAPIName: resolved.Detail.ObjectID},
			List: appRelatedListListTarget{
				ObjectAPIName:      resolved.List.ObjectID,
				FilterField:        resolved.List.FilterField,
				ParentFieldAPIName: resolved.List.ParentFieldID,
			},
			Create: appRelatedListCreateTarget{
				Kind:            resolved.Create.Kind,
				ObjectAPIName:   resolved.Create.ObjectID,
				PrefillMappings: prefill,
			},
		}
	}
	return nil
}

func relatedListDisplayKey(objectID string, lookupField string) string {
	return strings.ToLower(strings.TrimSpace(objectID)) + "\x00" +
		strings.ToLower(strings.TrimSpace(lookupField))
}

func unavailablePageLayout() error {
	return &commonsemantic.Failure{Category: commonsemantic.FailureUnavailable}
}
