package runtimeapi

import (
	"context"
	"errors"
	"log"

	"github.com/nexuscrm/automation/standardaction"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	servercontract "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	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"
)

const appMetadataInvocableActionKind = "metadata.pack.invocable-action.v1"

type flowCatalogActionAuthority struct {
	component   servercontract.AppMetadataComponent
	invocation  *standardaction.CatalogInvocation
	permissions *flowCatalogActionPermissionChecker
	metadata    flowCatalogActionMetadata
	effective   bool
}

func resolveFlowCatalogActionAuthority(
	components []servercontract.AppMetadataComponent,
	effectiveComponents []servercontract.EffectiveMetadataComponent,
	binding *flowsemantic.CatalogActionBinding,
	projected *contract.EffectiveActionPermissions,
	view *catalogsnapshot.RuntimeMetadataView,
	objectPermissions catalogActionObjectPermissionEngine,
) (*flowCatalogActionAuthority, error) {
	return resolveFlowCatalogActionAuthorityProfile(
		components, effectiveComponents, binding, projected, view, objectPermissions, true,
	)
}

func resolveFlowCatalogActionPreviewAuthority(
	components []servercontract.AppMetadataComponent,
	effectiveComponents []servercontract.EffectiveMetadataComponent,
	binding *flowsemantic.CatalogActionBinding,
	projected *contract.EffectiveActionPermissions,
	view *catalogsnapshot.RuntimeMetadataView,
	objectPermissions catalogActionObjectPermissionEngine,
) (*flowCatalogActionAuthority, error) {
	return resolveFlowCatalogActionAuthorityProfile(
		components, effectiveComponents, binding, projected, view, objectPermissions, false,
	)
}

func resolveFlowCatalogActionAuthorityProfile(
	components []servercontract.AppMetadataComponent,
	effectiveComponents []servercontract.EffectiveMetadataComponent,
	binding *flowsemantic.CatalogActionBinding,
	projected *contract.EffectiveActionPermissions,
	view *catalogsnapshot.RuntimeMetadataView,
	objectPermissions catalogActionObjectPermissionEngine,
	requireAtomic bool,
) (*flowCatalogActionAuthority, error) {
	if binding == nil || view == nil || objectPermissions == nil {
		return nil, &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
	}
	component, err := selectFlowCatalogActionComponent(components, binding.ComponentRef)
	if err != nil {
		return nil, err
	}
	component, effective, err := applyEffectiveFlowCatalogActionComponent(
		component, effectiveComponents,
	)
	if err != nil {
		return nil, err
	}
	invocation, err := standardaction.ResolveCatalogInvocation(
		component.ComponentID, component.CanonicalBehavior, binding.ActionRef,
	)
	if err != nil {
		var missing *standardaction.CatalogActionNotFoundError
		if errors.As(err, &missing) {
			return nil, &recordsemantic.Failure{Category: recordsemantic.FailureMissing}
		}
		return nil, &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
	}
	if requireAtomic && !invocation.Atomic() {
		return nil, flowCatalogActionInvalidFailure()
	}
	permissions, err := newFlowCatalogActionPermissionChecker(
		projected, objectPermissions,
	)
	if err != nil {
		return nil, err
	}
	return &flowCatalogActionAuthority{
		component: component, invocation: invocation,
		permissions: permissions, metadata: flowCatalogActionMetadata{view: view},
		effective: effective,
	}, nil
}

func applyEffectiveFlowCatalogActionComponent(
	component servercontract.AppMetadataComponent,
	effectiveComponents []servercontract.EffectiveMetadataComponent,
) (servercontract.AppMetadataComponent, bool, error) {
	var selected *servercontract.EffectiveMetadataComponent
	for index := range effectiveComponents {
		candidate := &effectiveComponents[index]
		if candidate.ComponentKind !=
			servercontract.EffectiveStandardActionComponentKind ||
			candidate.BusinessKey != component.BusinessKey {
			continue
		}
		if selected != nil || candidate.ComponentFormatVersion != 1 {
			return servercontract.AppMetadataComponent{}, false,
				&recordsemantic.Failure{Category: recordsemantic.FailureInternal}
		}
		selected = candidate
	}
	if selected == nil {
		return component, false, nil
	}
	component.CanonicalBehavior = append(
		[]byte(nil), selected.CanonicalBehavior...,
	)
	component.ContentDigest = selected.ContentDigest
	return component, true, nil
}

func selectFlowCatalogActionComponent(
	components []servercontract.AppMetadataComponent,
	componentRef string,
) (servercontract.AppMetadataComponent, error) {
	var selected servercontract.AppMetadataComponent
	found := 0
	for _, component := range components {
		if component.ComponentKind != appMetadataInvocableActionKind ||
			component.ComponentFormatVersion != 1 ||
			component.ComponentID != componentRef {
			continue
		}
		selected = component
		found++
	}
	if found == 0 {
		return servercontract.AppMetadataComponent{}, &recordsemantic.Failure{
			Category: recordsemantic.FailureMissing,
		}
	}
	if found != 1 {
		return servercontract.AppMetadataComponent{}, &recordsemantic.Failure{
			Category: recordsemantic.FailureInternal,
		}
	}
	return selected, nil
}

func (authority *flowCatalogActionAuthority) preparePlan(
	ctx context.Context,
	inputs map[string]interface{},
	user *models.UserSession,
) (*standardaction.PreparedCatalogInvocation, error) {
	if authority == nil || authority.invocation == nil || authority.permissions == nil {
		return nil, &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
	}
	prepared, outcome, reject, err := authority.invocation.PreparePlan(
		ctx, authority.permissions, authority.metadata, inputs, user,
	)
	if err != nil {
		return nil, &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
	}
	if outcome != nil {
		// Action-level authority is also discovery authority.  A caller that does
		// not hold the action's RequiredPermissions must not learn that the bound
		// action exists.  Keep input-guard rejection below as Forbidden: that is a
		// verdict about this invocation's supplied values after the action itself
		// was visible and selected.
		return nil, &recordsemantic.Failure{Category: recordsemantic.FailureMissing}
	}
	if reject != nil {
		log.Printf(
			"runtime Flow catalog action preparation rejected: component_id=%q definition_ref=%q code=%q diagnostic_kind=%q diagnostic_target=%q internal=%t",
			authority.component.ComponentID,
			authority.invocation.DefinitionRef(),
			reject.Code,
			reject.DiagnosticKind(),
			reject.DiagnosticTarget(),
			reject.Internal,
		)
		if reject.Code == standardaction.RejectInsufficientPerms {
			return nil, &recordsemantic.Failure{Category: recordsemantic.FailureForbidden}
		}
		return nil, flowCatalogActionInvalidFailure()
	}
	if prepared == nil {
		return nil, &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
	}
	return prepared, nil
}

func flowCatalogActionInvalidFailure() *recordsemantic.Failure {
	return &recordsemantic.Failure{
		Category:         recordsemantic.FailureInvalidRequest,
		InvalidComponent: commonsemantic.InvalidComponentCommandData,
	}
}

type flowCatalogActionPermissionChecker struct {
	permissions       map[string]bool
	systemPermissions map[string]bool
	objectPermissions catalogActionObjectPermissionEngine
}

func newFlowCatalogActionPermissionChecker(
	projected *contract.EffectiveActionPermissions,
	objectPermissions catalogActionObjectPermissionEngine,
) (*flowCatalogActionPermissionChecker, error) {
	if projected == nil || objectPermissions == nil {
		return nil, &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
	}
	checker := &flowCatalogActionPermissionChecker{
		permissions: make(
			map[string]bool,
			len(projected.SystemPermissions)+len(projected.CustomPermissions),
		),
		systemPermissions: make(map[string]bool, len(projected.SystemPermissions)),
		objectPermissions: objectPermissions,
	}
	for permission, granted := range projected.SystemPermissions {
		checker.permissions[permission] = checker.permissions[permission] || granted
		checker.systemPermissions[permission] = granted
	}
	for permission, granted := range projected.CustomPermissions {
		checker.permissions[permission] = checker.permissions[permission] || granted
	}
	return checker, nil
}

func (checker *flowCatalogActionPermissionChecker) HasPermission(
	_ context.Context,
	_ *models.UserSession,
	permissionAPIName string,
) bool {
	return checker != nil && checker.permissions[permissionAPIName]
}

func (checker *flowCatalogActionPermissionChecker) HasSuperUserCapability(
	ctx context.Context,
	user *models.UserSession,
) bool {
	return checker != nil && checker.systemPermissions[string(constants.PermModifyAllData)]
}

func (checker *flowCatalogActionPermissionChecker) CheckObjectPermissionWithUser(
	ctx context.Context,
	objectAPIName string,
	operation string,
	user *models.UserSession,
) bool {
	if checker == nil || checker.objectPermissions == nil {
		return false
	}
	return checker.objectPermissions.CheckObjectPermissionWithUser(
		ctx, objectAPIName, operation, user,
	)
}

type flowCatalogActionMetadata struct {
	view *catalogsnapshot.RuntimeMetadataView
}

func (metadata flowCatalogActionMetadata) GetSchema(
	_ context.Context,
	apiName string,
) *models.ObjectMetadata {
	if metadata.view == nil {
		return nil
	}
	object, _ := metadata.view.Object(apiName)
	return object
}

var _ standardaction.PermissionChecker = (*flowCatalogActionPermissionChecker)(nil)
var _ standardaction.MetadataProvider = flowCatalogActionMetadata{}
