package runtimeapi

import (
	"context"
	"errors"

	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/automation/standardaction"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	sharedcontract "github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	flowsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/flowv1"
	recordsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordsv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
)

func (s *FlowExecutionServer) PreviewCatalogAction(
	ctx context.Context,
	wireRequest *runtimev1.PreviewCatalogActionRequest,
) (*runtimev1.PreviewCatalogActionResponse, error) {
	request, err := mapping.FromWirePreviewCatalogActionRequest(ctx, wireRequest)
	if err != nil {
		return nil, flowStatus(err)
	}
	scope, view, trusted, appView, err := s.catalogActionPreviewRequestAuthority(
		ctx, request.MinCatalogRevision,
	)
	if err != nil {
		return nil, flowStatus(err)
	}
	if s.permissions == nil || s.actionObjectPermissions == nil ||
		s.actionPreviewAccess == nil {
		return nil, flowStatus(&recordsemantic.Failure{
			Category: recordsemantic.FailureUnavailable,
		})
	}
	viewer := trusted.UserSession()
	if viewer.ID == "" {
		return nil, flowStatus(&recordsemantic.Failure{
			Category: recordsemantic.FailureForbidden,
		})
	}
	projected, err := s.permissions.EffectiveActionPermissions(
		ctx, &viewer, trusted.PermissionFloor(),
	)
	if err != nil {
		return nil, flowStatus(classifyEffectivePermissionError(ctx, err))
	}
	authority, err := resolveFlowCatalogActionPreviewAuthority(
		appView.AppComponents,
		appView.EffectiveComponents,
		&request.CatalogAction,
		projected,
		view,
		s.actionObjectPermissions,
	)
	if err != nil {
		return nil, flowStatus(err)
	}
	definitionRef := authority.invocation.DefinitionRef()
	if request.Mode == flowsemantic.CatalogActionPreviewOutputs {
		definitionRef = authority.invocation.PreviewDefinitionRef()
	}
	if definitionRef == "" {
		return nil, flowStatus(flowCatalogActionInvalidFailure())
	}
	if !authority.effective {
		if _, err := selectCatalogActionFlowAppMetadataComponent(
			appView.AppComponents, definitionRef,
		); err != nil {
			return nil, flowStatus(err)
		}
	}
	closure, err := selectFlowPreviewClosure(ctx, view, definitionRef)
	if err != nil {
		return nil, flowStatus(err)
	}
	if !authority.effective {
		if _, err := authorizeFlowAppMetadataClosure(
			appView.AppComponents, appView.EffectiveComponents,
			closure, definitionRef, "",
		); err != nil {
			return nil, flowStatus(err)
		}
	}
	preparedClosure := make(map[string]*models.FlowVersion, len(closure))
	for _, selected := range closure {
		preparedClosure[selected.definitionRef] = selected.version
	}

	var result any
	switch request.Mode {
	case flowsemantic.CatalogActionPreviewCreateRecords:
		inputs, decodeErr := decodeFlowInputs(request.CanonicalInputs)
		if decodeErr != nil {
			return nil, flowStatus(decodeErr)
		}
		preview, outcome, previewErr := authority.invocation.PreviewCatalogRecordCreate(
			ctx,
			authority.permissions,
			authority.metadata,
			s.actionPreviewAccess,
			inputs,
			&viewer,
			func(
				previewCtx context.Context,
				flowName string,
				previewUser *models.UserSession,
			) (bool, error) {
				return s.engine.PreparedStandardActionCreatePreviewSupported(
					previewCtx, flowName, preparedClosure, previewUser,
				)
			},
			func(
				previewCtx context.Context,
				flowName string,
				previewInputs map[string]interface{},
				previewUser *models.UserSession,
			) (*automation.FlowRecordCreatePreviewResult, error) {
				return s.engine.PreviewPreparedStandardActionRecordCreateFields(
					previewCtx, flowName, preparedClosure, previewInputs, previewUser,
				)
			},
		)
		if outcome != nil {
			return nil, flowStatus(&recordsemantic.Failure{
				// The action-level permission is also discovery authority.  Match
				// ExecuteCatalogAction and conceal an unavailable action; input
				// guard rejections remain preview results or Forbidden below.
				Category: recordsemantic.FailureMissing,
			})
		}
		if previewErr != nil {
			return nil, flowStatus(classifyCatalogActionPreviewError(ctx, previewErr))
		}
		if preview == nil {
			return nil, flowStatus(&recordsemantic.Failure{Category: recordsemantic.FailureInternal})
		}
		result = catalogActionCreatePreviewResult(*preview)
	case flowsemantic.CatalogActionPreviewOutputs:
		preview, outcome, previewErr := authority.invocation.PreviewCatalogOutputs(
			ctx,
			authority.permissions,
			request.RecordID,
			&viewer,
			func(
				previewCtx context.Context,
				flowName string,
				previewInputs map[string]interface{},
				previewUser *models.UserSession,
			) (*automation.FlowPreviewOutputsResult, error) {
				return s.engine.PreviewPreparedFlowOutputs(
					previewCtx, flowName, preparedClosure, previewInputs, previewUser,
				)
			},
		)
		if outcome != nil {
			return nil, flowStatus(&recordsemantic.Failure{
				Category: recordsemantic.FailureMissing,
			})
		}
		if previewErr != nil {
			return nil, flowStatus(classifyCatalogActionPreviewError(ctx, previewErr))
		}
		if preview == nil {
			return nil, flowStatus(&recordsemantic.Failure{Category: recordsemantic.FailureInternal})
		}
		result = catalogActionOutputPreviewResult(*preview)
	default:
		return nil, flowStatus(flowCatalogActionInvalidFailure())
	}
	if err := ctx.Err(); err != nil {
		return nil, flowStatus(err)
	}
	payload, err := marshalPlatformCanonicalJSON(result)
	if err != nil {
		return nil, flowStatus(&recordsemantic.Failure{Category: recordsemantic.FailureInternal})
	}
	response := flowsemantic.CatalogActionPreviewResponse{
		CanonicalResult: payload,
		Receipts:        receiptsForCapability(scope.receipts, flowsemantic.CapabilityName),
	}
	wireResponse, err := mapping.ToWirePreviewCatalogActionResponse(request, response)
	if err != nil {
		return nil, flowStatus(err)
	}
	return wireResponse, nil
}

func (s *FlowExecutionServer) catalogActionPreviewRequestAuthority(
	ctx context.Context,
	minimum *uint64,
) (
	RuntimeExecutionScope,
	*catalogsnapshot.RuntimeMetadataView,
	TrustedCall,
	flowAppMetadataView,
	error,
) {
	scope, scopeOK := runtimeExecutionScopeFromContext(ctx)
	view, viewOK := RuntimeMetadataViewFromContext(ctx)
	trusted, trustedOK := trustedCallFromContext(ctx)
	if !scopeOK || !viewOK || !trustedOK {
		return scope, view, trusted, flowAppMetadataView{},
			&recordsemantic.Failure{Category: recordsemantic.FailureUnavailable}
	}
	if scope.capability != flowsemantic.CapabilityName ||
		scope.orgID != trusted.orgID ||
		scope.appInstallationID != trusted.appInstallationID ||
		scope.workloadID != trusted.callerWorkloadID ||
		scope.receipts.SecurityEpoch != trusted.securityEpoch {
		return scope, view, trusted, flowAppMetadataView{},
			&recordsemantic.Failure{Category: recordsemantic.FailureForbidden}
	}
	if err := requireCatalogRevision(minimum, scope.receipts.CatalogRevision); err != nil {
		return scope, view, trusted, flowAppMetadataView{}, err
	}
	appView, err := s.flowAppMetadataAuthority(ctx, scope, view, trusted)
	if err != nil {
		return scope, view, trusted, flowAppMetadataView{}, err
	}
	return scope, view, trusted, appView, nil
}

func classifyCatalogActionPreviewError(ctx context.Context, err error) error {
	if err == nil {
		return nil
	}
	var actionErr *standardaction.Error
	if errors.As(err, &actionErr) && actionErr != nil && !actionErr.Internal {
		switch actionErr.Kind {
		case standardaction.ErrorUnauthorized:
			return &recordsemantic.Failure{Category: recordsemantic.FailureForbidden}
		case standardaction.ErrorInvalidRequest,
			standardaction.ErrorPreviewUnsupported,
			standardaction.ErrorNoPreviewProducer:
			return flowCatalogActionInvalidFailure()
		}
	}
	return classifyPlatformEngineError(
		ctx, err, commonsemantic.InvalidComponentCommandData,
	)
}

type catalogActionPreviewError struct {
	Code    string   `json:"code"`
	Message string   `json:"message"`
	Fields  []string `json:"fields,omitempty"`
}

type catalogActionPreviewImage struct {
	Object                    string                `json:"object"`
	Fields                    sharedcontract.Record `json:"fields"`
	SiblingMaxDefaultedFields []string              `json:"siblingMaxDefaultedFields,omitempty"`
}

type catalogActionChildPreview struct {
	Object string                  `json:"object"`
	Count  int                     `json:"count"`
	Rows   []sharedcontract.Record `json:"rows,omitempty"`
}

type catalogActionCreatePreview struct {
	ActionName    string                      `json:"actionName"`
	IsSuccess     bool                        `json:"isSuccess"`
	Errors        []catalogActionPreviewError `json:"errors,omitempty"`
	ParentPreview *catalogActionPreviewImage  `json:"parentPreview,omitempty"`
	ChildPreview  []catalogActionChildPreview `json:"childPreview,omitempty"`
}

type catalogActionOutputPreview struct {
	OutputValues map[string]any              `json:"outputValues"`
	IsSuccess    bool                        `json:"isSuccess"`
	Errors       []catalogActionPreviewError `json:"errors,omitempty"`
}

func catalogActionCreatePreviewResult(
	input sharedcontract.StandardActionPreviewResult,
) catalogActionCreatePreview {
	result := catalogActionCreatePreview{
		ActionName: input.ActionName,
		IsSuccess:  input.IsSuccess,
		Errors:     catalogActionPreviewErrors(input.Errors),
	}
	if input.ParentPreview != nil {
		result.ParentPreview = &catalogActionPreviewImage{
			Object: input.ParentPreview.Object,
			Fields: input.ParentPreview.Fields,
			SiblingMaxDefaultedFields: append(
				[]string(nil), input.ParentPreview.SiblingMaxDefaultedFields...,
			),
		}
	}
	for _, child := range input.ChildPreview {
		result.ChildPreview = append(result.ChildPreview, catalogActionChildPreview{
			Object: child.Object,
			Count:  child.Count,
			Rows:   append([]sharedcontract.Record(nil), child.Rows...),
		})
	}
	return result
}

func catalogActionOutputPreviewResult(
	input sharedcontract.StandardActionOutputPreview,
) catalogActionOutputPreview {
	return catalogActionOutputPreview{
		OutputValues: input.OutputValues,
		IsSuccess:    input.IsSuccess,
		Errors:       catalogActionPreviewErrors(input.Errors),
	}
}

func catalogActionPreviewErrors(
	input []sharedcontract.StandardActionError,
) []catalogActionPreviewError {
	if len(input) == 0 {
		return nil
	}
	result := make([]catalogActionPreviewError, 0, len(input))
	for _, item := range input {
		result = append(result, catalogActionPreviewError{
			Code:    item.Code,
			Message: item.Message,
			Fields:  append([]string(nil), item.Fields...),
		})
	}
	return result
}
