package automation

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

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// Reassign changes the assigned approver on a pending work item.
// Only the current ActorId or a system admin can reassign.
func (s *ApprovalService) Reassign(ctx context.Context, workItemID, newActorID string, user *models.UserSession) error {
	ctx = constants.WithSecurityContext(ctx, constants.SystemContext, "ApprovalService: managing approval system tables")

	workItemFilter := fmt.Sprintf("Id == '%s'", escapeApprovalFilterValue(workItemID))
	wiResult, err := s.query.QueryWithFilter(ctx, constants.TableProcessInstanceWorkitem, workItemFilter, user, "", "", 1)
	if err != nil {
		return apperrors.NewInternalError("failed to resolve approval work item for Reassign", err)
	}
	if wiResult == nil || wiResult.TotalSize == 0 || len(wiResult.Records) == 0 {
		return ErrWorkItemNotFound
	}
	workItem := wiResult.Records[0]
	instanceId, _ := workItem["ProcessInstanceId"].(string)

	// Authorization: only current actor or system admin can reassign
	currentActorId, _ := workItem["ActorId"].(string)
	isSuperUser := s.hasSuperUserCapability(ctx, user)
	if currentActorId != user.ID && !isSuperUser {
		return ErrNotAuthorized
	}
	originalActorId := approvalStringValue(workItem["OriginalActorId"])
	if originalActorId == "" {
		originalActorId = currentActorId
	}

	// Check if the current approval step allows delegation (upstream #894):
	// Done before the transaction so an authorization failure doesn't touch any
	// state. Pure read path; no mutation.
	if instanceId != "" {
		// Find the current pending ProcessInstanceNode for this instance
		nodeFilter := fmt.Sprintf("ProcessInstanceId == '%s' && NodeStatus == '%s'", escapeApprovalFilterValue(instanceId), constants.ApprovalStatusPending)
		nodeResult, err := s.query.QueryWithFilter(ctx, constants.TableProcessInstanceNode, nodeFilter, user, "", "", 1)
		if err != nil {
			return apperrors.NewInternalError("failed to load current approval step for Reassign", err)
		}
		if nodeResult == nil {
			return apperrors.NewInternalError("current approval step query returned no result for Reassign", nil)
		}
		if len(nodeResult.Records) > 0 {
			instanceNode := nodeResult.Records[0]
			processNodeName, _ := instanceNode["ProcessNodeName"].(string)
			processNodeID := approvalStringValue(instanceNode["ProcessNodeId"])
			processNode := models.SObject{
				constants.FieldID: processNodeID,
				"Name":            processNodeName,
			}

			// Get the ProcessDefinition to check the AllowDelegate flag for this step
			instanceFilter := fmt.Sprintf("Id == '%s'", escapeApprovalFilterValue(instanceId))
			instResult, err := s.query.QueryWithFilter(ctx, constants.TableProcessInstance, instanceFilter, user, "", "", 1)
			if err != nil {
				return apperrors.NewInternalError("failed to load ProcessInstance for Reassign delegation authorization", err)
			}
			if instResult == nil {
				return apperrors.NewInternalError("ProcessInstance query returned no result for Reassign delegation authorization", nil)
			}
			if len(instResult.Records) > 0 {
				instance := instResult.Records[0]
				reassignProcessDefID, _ := instance["ProcessDefinitionId"].(string)

				if reassignProcessDefID != "" {
					pdFilter := fmt.Sprintf("Id == '%s'", escapeApprovalFilterValue(reassignProcessDefID))
					pdResult, pdErr := s.query.QueryWithFilter(ctx, constants.TableProcessDefinition, pdFilter, user, "", "", 1)
					if pdErr != nil {
						return apperrors.NewInternalError("failed to load ProcessDefinition for Reassign delegation authorization", pdErr)
					}
					if pdResult == nil {
						return apperrors.NewInternalError("ProcessDefinition query returned no result for Reassign delegation authorization", nil)
					}
					if len(pdResult.Records) > 0 {
						processDefinition := pdResult.Records[0]
						metadataStr := approvalStringValue(processDefinition["Metadata"])
						if strings.TrimSpace(metadataStr) != "" {
							parsedMetadata, parseErr := s.approvalMetadataForDefinition(processDefinition)
							if parseErr != nil {
								return parseErr
							}
							if parsedMetadata == nil {
								return apperrors.NewInternalError("approval delegation authorization decoded no ProcessDefinition metadata", nil)
							}
							if processNodeID != "" {
								pnFilter := fmt.Sprintf("Id == '%s'", escapeApprovalFilterValue(processNodeID))
								pnResult, pnErr := s.query.QueryWithFilter(ctx, constants.TableProcessNode, pnFilter, user, "", "", 1)
								if pnErr != nil {
									return apperrors.NewInternalError("failed to load ProcessNode for approval delegation authorization", pnErr)
								}
								if pnResult == nil || len(pnResult.Records) == 0 {
									return apperrors.NewInternalError("approval delegation authorization ProcessNode was not found", nil)
								}
								processNode = pnResult.Records[0]
							}
							step := metadataStepForProcessNode(parsedMetadata, processNode)
							// Salesforce parity: ModifyAllData holders may
							// reassign any approval request regardless of the
							// step's delegation setting — allowDelegate governs
							// an assigned approver's own ability to delegate.
							// The bypass covers the policy verdict including
							// the step==nil metadata-drift shape (logged, same
							// family as the NX#3997 WARN); the authority reads
							// above still fail closed for super users.
							if step == nil || !step.AllowDelegate {
								if !isSuperUser {
									return ErrDelegationNotAllowed
								}
								if step == nil {
									s.logger.Warn(fmt.Sprintf("Reassign super-user bypass without an authored step: %s", describeMetadataStepGap(processDefinition, processNode, parsedMetadata)))
								}
							}
						}
					}
				}
			}
		} else if _, runtimeBound := runtimeApprovalAuthorityFromContext(ctx); runtimeBound {
			return apperrors.NewInternalError("Runtime Approval work item has no pending step for Reassign", nil)
		}
	}

	var reassignNotifications *pendingNotifications
	var reassignSubmittedByID string

	// NX#4150 Class C. Immutable pre-transaction identity; see
	// approval_recall.go for why each attempt restores from this rather than
	// clearing resolved fields one by one.
	baseOutcomeReq := approvalActionOutcomeRequest{
		actionEvent:       ApprovalActionRequestReassign,
		workItemID:        workItemID,
		processInstanceID: instanceId,
		actor:             user,
		startedAt:         time.Now(),
	}
	outcomeReq := baseOutcomeReq
	// Hoisted so the completion hook can read them when it fires. Declaring
	// them inside the closure would put them out of scope at registration and
	// force a publish step, which is what left them NULL before.
	var reassignProcessDefID, reassignTargetObjectID string
	var actionCounter approvalActionCounter
	var reassignAttemptErr error

	txErr := s.persistence.RunInPessimisticTransaction(ctx, func(txCtx context.Context) (attemptOutcomeErr error) {
		// Record the attempt error for the completion hook, which fires after
		// this returns and otherwise sees only that something failed.
		defer func() { reassignAttemptErr = attemptOutcomeErr }()
		// Reset closure-scoped state so deadlock-retry attempts are clean.
		reassignNotifications = nil
		reassignSubmittedByID = ""
		reassignProcessDefID = ""
		reassignTargetObjectID = ""
		outcomeReq = baseOutcomeReq
		actionCounter.reset()
		reassignAttemptErr = nil
		if !s.registerApprovalActionOutcome(txCtx, &outcomeReq, actionCounter.value, func() error { return reassignAttemptErr },
			func() approvalActionIdentity {
				return approvalActionIdentity{
					processDefinitionID: reassignProcessDefID,
					targetObjectID:      reassignTargetObjectID,
				}
			}) {
			s.noteMissingApprovalBoundary(outcomeReq)
		}

		if instanceId == "" {
			return apperrors.NewInternalError("approval work item missing ProcessInstanceId for Reassign", nil)
		}

		instanceFilter := fmt.Sprintf("Id == '%s'", escapeApprovalFilterValue(instanceId))
		instResult, err := s.query.QueryWithFilter(txCtx, constants.TableProcessInstance, instanceFilter, user, "", "", 1)
		if err != nil {
			return apperrors.NewInternalError("failed to look up ProcessInstance "+instanceId+" for Reassign", err)
		}
		if instResult == nil || instResult.TotalSize == 0 || len(instResult.Records) == 0 {
			return apperrors.NewInternalError("failed to look up ProcessInstance "+instanceId+" for Reassign", nil)
		}
		instance := instResult.Records[0]
		reassignProcessDefID, _ = instance["ProcessDefinitionId"].(string)
		reassignTargetObjectID, _ = instance["TargetObjectId"].(string)
		reassignSubmittedByID, _ = instance["SubmittedById"].(string)
		if reassignProcessDefID == "" || reassignTargetObjectID == "" {
			return apperrors.NewInternalError("malformed ProcessInstance "+instanceId+" for Reassign", nil)
		}

		batch, fetchErr := s.prepareRequiredLifecycleActions(txCtx, reassignProcessDefID, "", reassignTargetObjectID, "", constants.ApprovalActionEventReassign, user)
		if fetchErr != nil {
			return fetchErr
		}
		actionCounter.add(len(batch.actions))

		now := time.Now().UTC().Format(time.RFC3339)
		// Update ActorId (leave OriginalActorId unchanged)
		updates := models.SObject{
			"ActorId": newActorID,
		}
		if err := s.persistence.Update(processTableWriteContext(txCtx, "ApprovalService: update ProcessInstanceWorkitem"), constants.TableProcessInstanceWorkitem, workItemID, updates, user); err != nil {
			return err
		}
		if err := s.updatePendingApprovalHistoryActor(txCtx, workItemID, instanceId, reassignTargetObjectID, batch.objectApiName, originalActorId, newActorID, now, user); err != nil {
			return err
		}

		reassignRuntime := approvalFlowNotificationContext{
			ProcessInstanceID: instanceId,
			WorkItemIDs:       []string{workItemID},
			SubmitterUserID:   reassignSubmittedByID,
			ActorUserID:       user.ID,
		}
		notifications, execErr := s.executeLifecycleActionBatch(txCtx, batch, []string{newActorID}, reassignRuntime, user)
		if execErr != nil {
			return execErr
		}
		reassignNotifications = notifications

		// ActorId moved from the old approver to the new one: revoke the old
		// approver's share and grant the new approver's.
		if err := s.reconcileApprovalShares(txCtx, instanceId, user); err != nil {
			return err
		}
		return nil
	})
	if txErr != nil {
		return txErr
	}

	if reassignNotifications != nil {
		// Reassign's exported signature carries no response payload, so
		// notification delivery failures (including #4397 R1 snapshot
		// fail-closed entries) are log-only on this path — a documented
		// asymmetry with submit/step/terminal/recall, which attach
		// ActionFailures to their responses. Widening Reassign's contract is
		// a separate change; do not silently rely on this discard.
		s.executeProcessActionNotifications(ctx, *reassignNotifications, user)
	}
	return nil
}
