//go:build crm

package crm

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/nexuscrm/automation"
	dialectsfcrm "github.com/nexuscrm/dialect-sf-crm"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

const (
	changeOwnerHandlerRef                = "crm:standard-action/change-owner"
	sendRecordEmailHandlerRef            = "crm:standard-action/send-record-email"
	stampConvertedAnchorsHandlerRef      = "crm:standard-action/stamp-converted-anchors"
	stampConvertedAnchorsConvertedDate   = "ConvertedDate"
	stampConvertedAnchorsConvertedPrefix = "Converted"
)

func sendRecordEmailFlowActionCallHandlerImplementation(runtime *sendRecordEmailRuntimeBinding) automation.FlowActionCallHandlerImplementation {
	return automation.FlowActionCallHandlerImplementation{
		HandlerRef: sendRecordEmailHandlerRef,
		Build: func(descriptor automation.FlowActionCallRegistrationDescriptor) (automation.FlowActionCallRegistration, error) {
			return automation.FlowActionCallRegistration{
				ActionType:      descriptor.ActionType,
				HandlerRef:      descriptor.HandlerRef,
				SendsEmail:      true,
				AtomicPolicy:    automation.FlowActionCallAtomicPolicyReject,
				PreviewPolicy:   automation.FlowActionCallPreviewPolicyRejectEmailIO,
				RequiresContext: automation.FlowActionCallContextStandardActionProtected,
				Handler: automation.RequireStandardActionProtectedContext(descriptor.ActionType,
					func(ctx context.Context, e *automation.FlowExecutor, interview *models.FlowInterview, metadata *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
						return executeSendRecordEmail(ctx, e, interview, metadata, action, user, runtime)
					}),
			}, nil
		},
	}
}

func executeSendRecordEmail(ctx context.Context, e *automation.FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession, runtime *sendRecordEmailRuntimeBinding) (string, error) {
	fault := func(err error) (string, error) {
		return e.RouteActionCallFault(ctx, interview, action, err)
	}
	deps, ok := runtime.Get()
	if !ok {
		return fault(fmt.Errorf("send record email runtime is unavailable"))
	}
	params, err := e.ResolveActionCallInputParameters(ctx, interview, action, user)
	if err != nil {
		return fault(err)
	}
	_, err = dialectsfcrm.ExecuteSendRecordEmailAction(ctx, deps, dialectsfcrm.SendRecordEmailInput{
		RequestID:     changeOwnerStringParam(params, "requestId"),
		RecordID:      changeOwnerStringParam(params, "recordId"),
		ObjectAPIName: changeOwnerStringParam(params, "objectApiName"),
		Subject:       changeOwnerStringParam(params, "subject"),
		Body:          changeOwnerStringParam(params, "body"),
	}, user)
	if err != nil {
		return fault(err)
	}
	if action.Connector != nil {
		return action.Connector.TargetReference, nil
	}
	return "", nil
}

func changeOwnerFlowActionCallHandlerImplementation() automation.FlowActionCallHandlerImplementation {
	return automation.FlowActionCallHandlerImplementation{
		HandlerRef: changeOwnerHandlerRef,
		Build: func(descriptor automation.FlowActionCallRegistrationDescriptor) (automation.FlowActionCallRegistration, error) {
			return automation.FlowActionCallRegistration{
				ActionType:      descriptor.ActionType,
				HandlerRef:      descriptor.HandlerRef,
				AtomicPolicy:    automation.FlowActionCallAtomicPolicyReject,
				PreviewPolicy:   automation.FlowActionCallPreviewPolicyReject,
				RequiresContext: automation.FlowActionCallContextStandardActionProtected,
				Handler:         automation.RequireStandardActionProtectedContext(descriptor.ActionType, executeChangeOwner),
			}, nil
		},
	}
}

func executeChangeOwner(ctx context.Context, e *automation.FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
	fault := func(err error) (string, error) {
		return e.RouteActionCallFault(ctx, interview, action, err)
	}

	params, err := e.ResolveActionCallInputParameters(ctx, interview, action, user)
	if err != nil {
		return fault(err)
	}
	deps := e.StandardActionContractDeps()
	policy, _ := deps.Policy.(contract.CapabilityPolicy)
	result, err := dialectsfcrm.ExecuteChangeOwnerAction(ctx, dialectsfcrm.ChangeOwnerActionDeps{
		Records:                   deps.Records,
		RecordMutationCoordinator: deps.RecordMutationCoordinator,
		Metadata:                  deps.Metadata,
		Policy:                    policy,
		Notifications:             deps.Notifications,
	}, dialectsfcrm.ChangeOwnerActionInput{
		RecordID:         strings.TrimSpace(changeOwnerStringParam(params, "recordId")),
		ObjectAPIName:    strings.TrimSpace(changeOwnerStringParam(params, "objectApiName")),
		NewOwnerID:       strings.TrimSpace(changeOwnerStringParam(params, "newOwnerId")),
		SendNotification: changeOwnerBoolParam(params, "sendNotification"),
	}, user)
	if err != nil {
		return fault(err)
	}
	if result != nil {
		e.AssignActionCallOutputs(interview, action, result.OutputValues)
	}
	if action.Connector != nil {
		return action.Connector.TargetReference, nil
	}
	return "", nil
}

func changeOwnerStringParam(params map[string]interface{}, name string) string {
	if value, ok := params[name].(string); ok {
		return value
	}
	return ""
}

func changeOwnerBoolParam(params map[string]interface{}, name string) bool {
	switch value := params[name].(type) {
	case bool:
		return value
	case string:
		return strings.EqualFold(strings.TrimSpace(value), "true")
	default:
		return false
	}
}

func stampConvertedAnchorsFlowActionCallHandlerImplementation() automation.FlowActionCallHandlerImplementation {
	return automation.FlowActionCallHandlerImplementation{
		HandlerRef: stampConvertedAnchorsHandlerRef,
		Build: func(descriptor automation.FlowActionCallRegistrationDescriptor) (automation.FlowActionCallRegistration, error) {
			return automation.FlowActionCallRegistration{
				ActionType:      descriptor.ActionType,
				HandlerRef:      descriptor.HandlerRef,
				AtomicPolicy:    automation.FlowActionCallAtomicPolicySafe,
				RequiresContext: automation.FlowActionCallContextStandardActionProtected,
				Handler:         automation.RequireStandardActionProtectedContext(descriptor.ActionType, executeStampConvertedAnchors),
			}, nil
		},
	}
}

// executeStampConvertedAnchors is the CRM app-pack action handler for
// convert-style standard actions. It updates the source record with dynamic
// converted-anchor lookup fields and stamps ConvertedDate when the source
// schema declares that Salesforce-compatible field.
func executeStampConvertedAnchors(ctx context.Context, e *automation.FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
	fault := func(err error) (string, error) {
		return e.RouteActionCallFault(ctx, interview, action, err)
	}

	params, err := e.ResolveActionCallInputParameters(ctx, interview, action, user)
	if err != nil {
		return fault(err)
	}
	sourceObject, _ := params["sourceObject"].(string)
	sourceObject = strings.TrimSpace(sourceObject)
	sourceID, _ := params["sourceId"].(string)
	sourceID = strings.TrimSpace(sourceID)
	if sourceObject == "" || sourceID == "" {
		return fault(fmt.Errorf("stampConvertedAnchors requires sourceObject and sourceId"))
	}
	if !e.ObjectSchemaLookupAvailable() {
		return fault(fmt.Errorf("metadata service unavailable for stampConvertedAnchors"))
	}

	anchors, _ := params["anchors"].(map[string]interface{})
	extra, _ := params["extraFields"].(map[string]interface{})

	prepared := e.PreparedRecordUpdate(ctx, sourceObject, sourceID)
	if prepared == nil {
		return fault(fmt.Errorf("stampConvertedAnchors requires prepared source preview for %s/%s", sourceObject, sourceID))
	}
	internalUpdates := cloneStampConvertedAnchorsSObject(prepared.InternalUpdates)

	for _, field := range convertedAnchorLookupFields(ctx, e, sourceObject) {
		for _, referenceTo := range field.ReferenceTo {
			if referenceTo == "" {
				continue
			}
			val, present := anchors[referenceTo]
			if !present {
				continue
			}
			str, _ := val.(string)
			if str == "" {
				continue
			}
			internalUpdates[field.APIName] = str
			break
		}
	}

	if f := e.LookupFieldMetadata(ctx, sourceObject, stampConvertedAnchorsConvertedDate); f != nil {
		t := constants.NormalizeFieldType(string(f.Type))
		if t == constants.FieldTypeDate || t == constants.FieldTypeDatetime {
			if _, overridden := internalUpdates[stampConvertedAnchorsConvertedDate]; !overridden {
				internalUpdates[stampConvertedAnchorsConvertedDate] = time.Now().UTC()
			}
		}
	}
	prepared.InternalUpdates = internalUpdates

	if len(prepared.EffectiveUpdates) == 0 && len(prepared.InternalUpdates) == 0 && len(extra) == 0 {
		if action.Connector != nil {
			return action.Connector.TargetReference, nil
		}
		return "", nil
	}

	if err := e.CommitPreparedRecordUpdate(ctx, sourceObject, sourceID, prepared, extra, user); err != nil {
		return fault(err)
	}

	snap := make(models.SObject)
	for k, v := range prepared.EffectiveUpdates {
		snap[k] = v
	}
	for k, v := range prepared.InternalUpdates {
		snap[k] = v
	}
	for k, v := range extra {
		snap[k] = v
	}
	snap[constants.FieldID] = sourceID
	e.PublishPostUpsertRecord(ctx, sourceObject, snap)

	if action.Connector != nil {
		return action.Connector.TargetReference, nil
	}
	return "", nil
}

func convertedAnchorLookupFields(ctx context.Context, e *automation.FlowExecutor, objectAPIName string) []models.FieldMetadata {
	schema := e.LookupObjectSchema(ctx, objectAPIName)
	if schema == nil {
		return nil
	}
	out := make([]models.FieldMetadata, 0)
	for _, field := range schema.Fields {
		if !isConvertedAnchorLookupAPIName(field.APIName) {
			continue
		}
		if len(field.ReferenceTo) == 0 {
			continue
		}
		out = append(out, field)
	}
	return out
}

func isConvertedAnchorLookupAPIName(name string) bool {
	if !strings.HasPrefix(name, stampConvertedAnchorsConvertedPrefix) {
		return false
	}
	for _, suffix := range []string{"Id", "__c"} {
		if len(name) > len(stampConvertedAnchorsConvertedPrefix)+len(suffix) && strings.HasSuffix(name, suffix) {
			return true
		}
	}
	return false
}

func cloneStampConvertedAnchorsSObject(in models.SObject) models.SObject {
	if in == nil {
		return models.SObject{}
	}
	out := make(models.SObject, len(in))
	for k, v := range in {
		out[k] = v
	}
	return out
}
