package runtimeapi

import (
	"context"
	"encoding/json"
	"errors"
	"sort"
	"strings"

	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/models"
	pagelayoutsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/pagelayoutresolutionv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
)

// PageLayoutRoleHierarchy is the authenticated security-owned role ancestry
// reader. The request never carries role or profile identity.
type PageLayoutRoleHierarchy interface {
	GetRoleAncestorsAtSecurityEpoch(context.Context, string, string, uint64) ([]string, error)
}

type PageLayoutResolver interface {
	ResolveCurrentPageLayouts(context.Context, string, string, []string, []string) (map[string]*models.PageLayout, error)
	ReadCurrentObjectListFields(context.Context, []string) (map[string][]string, error)
}

// PageLayoutResolutionServer resolves current Layout content and assignment
// from Runtime storage. The immutable Catalog view admits the object and
// RecordType context and resolves virtual related-list routes.
type PageLayoutResolutionServer struct {
	runtimev1.UnimplementedPageLayoutResolutionServiceServer
	resolver PageLayoutResolver
	roles    PageLayoutRoleHierarchy
}

func NewPageLayoutResolutionServer(
	resolver PageLayoutResolver,
	roles PageLayoutRoleHierarchy,
) (*PageLayoutResolutionServer, error) {
	if resolver == nil {
		return nil, errors.New("page Layout resolution requires a current database resolver")
	}
	return &PageLayoutResolutionServer{resolver: resolver, roles: roles}, nil
}

func (server *PageLayoutResolutionServer) ResolvePageLayout(
	ctx context.Context,
	wireRequest *runtimev1.ResolvePageLayoutRequest,
) (*runtimev1.ResolvePageLayoutResponse, error) {
	request, err := mapping.FromWireResolvePageLayoutRequest(ctx, wireRequest)
	if err != nil {
		return nil, pageLayoutResolutionStatus(err)
	}
	scope, scopeOK := runtimeExecutionScopeFromContext(ctx)
	trusted, trustedOK := trustedCallFromContext(ctx)
	if !scopeOK || !trustedOK || scope.capability != pagelayoutsemantic.CapabilityName {
		return nil, pageLayoutResolutionStatus(&pagelayoutsemantic.Failure{
			Category: pagelayoutsemantic.FailureUnavailable,
		})
	}
	if err := requireCatalogRevision(request.MinCatalogRevision, scope.receipts.CatalogRevision); err != nil {
		return nil, pageLayoutResolutionStatus(err)
	}

	user := trusted.UserSession()
	roles := make([]string, 0)
	if user.RoleID != nil && *user.RoleID != "" {
		if server.roles == nil {
			return nil, pageLayoutResolutionStatus(&pagelayoutsemantic.Failure{
				Category: pagelayoutsemantic.FailureUnavailable,
			})
		}
		roles = append(roles, *user.RoleID)
		ancestors, roleErr := server.roles.GetRoleAncestorsAtSecurityEpoch(
			ctx,
			trusted.orgID,
			*user.RoleID,
			scope.receipts.SecurityEpoch,
		)
		if roleErr != nil {
			return nil, pageLayoutResolutionStatus(&pagelayoutsemantic.Failure{
				Category: pagelayoutsemantic.FailureUnavailable,
			})
		}
		roles = append(roles, ancestors...)
	}
	recordTypeIDs := append([]string(nil), request.RecordTypeIDs...)
	if len(recordTypeIDs) == 0 {
		recordTypeIDs = []string{""}
	}
	for _, recordTypeID := range recordTypeIDs {
		if contextErr := scope.view.ValidatePageLayoutContext(request.ObjectID, recordTypeID); contextErr != nil {
			return nil, pageLayoutResolutionStatus(classifyPageLayoutResolutionError(contextErr))
		}
	}
	currentLayouts, resolveErr := server.resolver.ResolveCurrentPageLayouts(
		ctx, request.ObjectID, user.ProfileID, roles, recordTypeIDs,
	)
	if resolveErr != nil {
		return nil, pageLayoutResolutionStatus(classifyPageLayoutResolutionError(resolveErr))
	}
	selections := make([]pagelayoutsemantic.Selection, 0, len(recordTypeIDs))
	layoutsByID := make(map[string]pagelayoutsemantic.ResolvedLayout)
	for _, recordTypeID := range recordTypeIDs {
		layout := currentLayouts[recordTypeID]
		if layout == nil {
			return nil, pageLayoutResolutionStatus(&pagelayoutsemantic.Failure{
				Category: pagelayoutsemantic.FailureInternal,
			})
		}
		related, resolveErr := scope.view.ResolvePageLayoutRelatedLists(request.ObjectID, layout)
		if resolveErr != nil {
			return nil, pageLayoutResolutionStatus(classifyPageLayoutResolutionError(resolveErr))
		}
		selection := pagelayoutsemantic.Selection{LayoutID: layout.ID}
		if recordTypeID != "" {
			selectedRecordTypeID := recordTypeID
			selection.RecordTypeID = &selectedRecordTypeID
		}
		selections = append(selections, selection)
		if _, exists := layoutsByID[layout.ID]; exists {
			continue
		}
		resolved, resolveErr := resolvedPageLayout(layout, related)
		if resolveErr != nil {
			return nil, pageLayoutResolutionStatus(resolveErr)
		}
		layoutsByID[layout.ID] = resolved
	}
	layoutIDs := make([]string, 0, len(layoutsByID))
	for layoutID := range layoutsByID {
		layoutIDs = append(layoutIDs, layoutID)
	}
	sort.Strings(layoutIDs)
	layouts := make([]pagelayoutsemantic.ResolvedLayout, 0, len(layoutIDs))
	for _, layoutID := range layoutIDs {
		layouts = append(layouts, layoutsByID[layoutID])
	}

	response, err := mapping.ToWireResolvePageLayoutResponse(request, pagelayoutsemantic.Response{
		ObjectID: request.ObjectID, Selections: selections, Layouts: layouts,
		Receipts: receiptsForCapability(scope.receipts, pagelayoutsemantic.CapabilityName),
	})
	if err != nil {
		return nil, pageLayoutResolutionStatus(err)
	}
	return response, nil
}

func (server *PageLayoutResolutionServer) ReadObjectListFields(
	ctx context.Context,
	wireRequest *runtimev1.ReadObjectListFieldsRequest,
) (*runtimev1.ReadObjectListFieldsResponse, error) {
	request, err := mapping.FromWireReadObjectListFieldsRequest(ctx, wireRequest)
	if err != nil {
		return nil, pageLayoutResolutionStatus(err)
	}
	scope, scopeOK := runtimeExecutionScopeFromContext(ctx)
	_, trustedOK := trustedCallFromContext(ctx)
	if !scopeOK || !trustedOK || scope.capability != pagelayoutsemantic.CapabilityName {
		return nil, pageLayoutResolutionStatus(&pagelayoutsemantic.Failure{
			Category: pagelayoutsemantic.FailureUnavailable,
		})
	}
	if err := requireCatalogRevision(request.MinCatalogRevision, scope.receipts.CatalogRevision); err != nil {
		return nil, pageLayoutResolutionStatus(err)
	}
	for _, objectID := range request.ObjectIDs {
		if contextErr := scope.view.ValidatePageLayoutContext(objectID, ""); contextErr != nil {
			return nil, pageLayoutResolutionStatus(classifyPageLayoutResolutionError(contextErr))
		}
	}
	fieldsByObject, err := server.resolver.ReadCurrentObjectListFields(ctx, request.ObjectIDs)
	if err != nil {
		return nil, pageLayoutResolutionStatus(classifyPageLayoutResolutionError(err))
	}
	objects := make([]pagelayoutsemantic.ObjectListFields, 0, len(request.ObjectIDs))
	for _, objectID := range request.ObjectIDs {
		fields, found := fieldsByObject[strings.ToLower(objectID)]
		if !found {
			return nil, pageLayoutResolutionStatus(&pagelayoutsemantic.Failure{
				Category: pagelayoutsemantic.FailureInternal,
			})
		}
		if fields == nil {
			fields = []string{}
		}
		objects = append(objects, pagelayoutsemantic.ObjectListFields{
			ObjectID: objectID, OrderedFieldIDs: fields,
		})
	}
	response, err := mapping.ToWireReadObjectListFieldsResponse(
		request,
		pagelayoutsemantic.ObjectListFieldsResponse{
			Objects:  objects,
			Receipts: receiptsForCapability(scope.receipts, pagelayoutsemantic.CapabilityName),
		},
	)
	if err != nil {
		return nil, pageLayoutResolutionStatus(err)
	}
	return response, nil
}

func resolvedPageLayout(
	layout *models.PageLayout,
	related []catalogsnapshot.RelatedListResolutionInput,
) (pagelayoutsemantic.ResolvedLayout, error) {
	if layout == nil {
		return pagelayoutsemantic.ResolvedLayout{}, &pagelayoutsemantic.Failure{
			Category: pagelayoutsemantic.FailureInternal,
		}
	}
	layoutDocument, err := json.Marshal(layout)
	if err != nil {
		return pagelayoutsemantic.ResolvedLayout{}, &pagelayoutsemantic.Failure{
			Category: pagelayoutsemantic.FailureInternal,
		}
	}
	resolved := make([]pagelayoutsemantic.RelatedListResolution, 0, len(related))
	for _, value := range related {
		mappings := make([]pagelayoutsemantic.FieldMapping, 0, len(value.Create.PrefillMappings))
		for target, source := range value.Create.PrefillMappings {
			mappings = append(mappings, pagelayoutsemantic.FieldMapping{
				TargetFieldID: target,
				SourceFieldID: source,
			})
		}
		sort.Slice(mappings, func(left, right int) bool {
			return mappings[left].TargetFieldID < mappings[right].TargetFieldID
		})
		resolved = append(resolved, pagelayoutsemantic.RelatedListResolution{
			DisplayObjectID:    value.DisplayObjectID,
			DisplayLookupField: value.DisplayLookupField,
			Query: pagelayoutsemantic.ResolvedQueryTarget{
				ObjectID:      value.Query.ObjectID,
				LookupField:   value.Query.LookupField,
				ParentFieldID: value.Query.ParentFieldID,
			},
			Detail: pagelayoutsemantic.ResolvedDetailTarget{ObjectID: value.Detail.ObjectID},
			List: pagelayoutsemantic.ResolvedListTarget{
				ObjectID:      value.ListTarget.ObjectID,
				FilterField:   value.ListTarget.FilterField,
				ParentFieldID: value.ListTarget.ParentFieldID,
			},
			Create: pagelayoutsemantic.ResolvedCreateTarget{
				Kind:            value.Create.Kind,
				ObjectID:        value.Create.ObjectID,
				PrefillMappings: mappings,
			},
		})
	}
	sort.Slice(resolved, func(left, right int) bool {
		leftKey := resolved[left].DisplayObjectID + "\x00" + resolved[left].DisplayLookupField
		rightKey := resolved[right].DisplayObjectID + "\x00" + resolved[right].DisplayLookupField
		return leftKey < rightKey
	})
	return pagelayoutsemantic.ResolvedLayout{
		LayoutID:              layout.ID,
		DocumentKind:          pagelayoutsemantic.PageLayoutDocumentKind,
		DocumentFormatVersion: pagelayoutsemantic.PageLayoutDocumentFormatVersion,
		LayoutDocument:        layoutDocument,
		RelatedLists:          resolved,
	}, nil
}

func classifyPageLayoutResolutionError(err error) error {
	switch {
	case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
		return err
	case errors.Is(err, catalogsnapshot.ErrPageLayoutNotFound),
		errors.Is(err, catalogsnapshot.ErrPageLayoutRecordTypeMissing):
		return &pagelayoutsemantic.Failure{Category: pagelayoutsemantic.FailureMissing}
	case errors.Is(err, catalogsnapshot.ErrIncompatible):
		return &pagelayoutsemantic.Failure{Category: pagelayoutsemantic.FailureUnavailable}
	default:
		return &pagelayoutsemantic.Failure{Category: pagelayoutsemantic.FailureInternal}
	}
}

func pageLayoutResolutionStatus(err error) error {
	return mapping.ToPlatformStatusError(err, pagelayoutsemantic.CapabilityName)
}
