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"
)

// Recall allows the submitter to withdraw a pending approval.
// 1. Finds the ProcessInstanceWorkitem by ID
// 2. Gets the ProcessInstance
// 3. Verifies the current user is the submitter
// 4. Updates ProcessInstance status to Removed
// 5. Deletes the pending WorkItem
func (s *ApprovalService) Recall(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error) {
	ctx = constants.WithSecurityContext(ctx, constants.SystemContext, "ApprovalService: managing approval system tables")

	// Step 1: Find the work item
	workItemFilter := fmt.Sprintf("Id == '%s'", escapeApprovalFilterValue(workItemID))
	wiResult, err := s.query.QueryWithFilter(ctx, constants.TableProcessInstanceWorkitem, workItemFilter, user, "", "", 1)
	if err != nil {
		return nil, apperrors.NewInternalError("failed to resolve approval work item for Recall", err)
	}
	if wiResult == nil || wiResult.TotalSize == 0 || len(wiResult.Records) == 0 {
		return nil, ErrWorkItemNotFound
	}
	workItem := wiResult.Records[0]
	instanceId, _ := workItem["ProcessInstanceId"].(string)
	if instanceId == "" {
		return nil, ErrWorkItemNotFound
	}
	workItemActorID := approvalStringValue(workItem["ActorId"])
	workItemOriginalActorID := approvalStringValue(workItem["OriginalActorId"])
	if workItemOriginalActorID == "" {
		workItemOriginalActorID = workItemActorID
	}
	// Capability read stays outside the transaction: it does not race with
	// approval state, and the submitter identity check it relaxes remains
	// inside (Step 4) with the status check.
	isSuperUser := s.hasSuperUserCapability(ctx, user)

	now := time.Now().UTC().Format(time.RFC3339)
	var targetObjectId string
	var recallProcessDefId string
	var recallActorIDs []string
	var recallWorkItemIDs []string
	var recallNotifications *pendingNotifications

	// NX#4150 Class C. Only workItemID and instanceId are known before the
	// transaction; the rest is resolved inside it, so on a rollback the row
	// carries whatever the deciding attempt reached and nothing is invented.
	// Immutable pre-transaction identity. Each attempt restores outcomeReq from
	// this rather than clearing fields one by one: the fields resolved inside
	// the transaction have already grown once, and a reset list that must be
	// extended by hand is how a retry came to emit the previous attempt's
	// target record.
	baseOutcomeReq := approvalActionOutcomeRequest{
		actionEvent:       ApprovalActionRequestRecall,
		workItemID:        workItemID,
		processInstanceID: instanceId,
		actor:             user,
		startedAt:         time.Now(),
	}
	outcomeReq := baseOutcomeReq
	var actionCounter approvalActionCounter
	var recallAttemptErr error

	// Steps 2-7 wrapped in a transaction for atomicity (prevents TOCTOU race
	// between concurrent Recall and Approve, and ensures no partial state).
	// The status and submitter checks MUST be inside the transaction to avoid
	// a time-of-check-time-of-use race where a concurrent Approve could change
	// the status between the check and the update.
	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() { recallAttemptErr = attemptOutcomeErr }()
		// Reset closure-scoped state so deadlock-retry attempts are clean.
		targetObjectId = ""
		recallProcessDefId = ""
		recallActorIDs = nil
		recallWorkItemIDs = nil
		recallNotifications = nil
		outcomeReq = baseOutcomeReq
		actionCounter.reset()
		recallAttemptErr = nil
		if !s.registerApprovalActionOutcome(txCtx, &outcomeReq, actionCounter.value, func() error { return recallAttemptErr },
			func() approvalActionIdentity {
				return approvalActionIdentity{
					processDefinitionID: recallProcessDefId,
					targetObjectID:      targetObjectId,
				}
			}) {
			s.noteMissingApprovalBoundary(outcomeReq)
		}

		// Step 2: Get the ProcessInstance (inside transaction)
		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, err)
		}
		if instResult == nil || instResult.TotalSize == 0 || len(instResult.Records) == 0 {
			return apperrors.NewInternalError("failed to look up ProcessInstance "+instanceId, nil)
		}
		instance := instResult.Records[0]
		recallProcessDefId, _ = instance["ProcessDefinitionId"].(string)

		// Step 3: Verify the instance is still Pending (inside transaction)
		status, _ := instance["Status"].(string)
		if status != constants.ApprovalStatusPending {
			return ErrInstanceNotPending
		}

		// Step 4: Verify the current user is the submitter (inside transaction).
		// Callers with the platform super-user capability may recall any pending
		// approval request on behalf of the submitter; the recall history row
		// records the acting user as ActorID either way.
		submittedById, _ := instance["SubmittedById"].(string)
		if submittedById != user.ID && !isSuperUser {
			return ErrNotSubmitter
		}

		targetObjectId, _ = instance["TargetObjectId"].(string)
		if strings.TrimSpace(recallProcessDefId) == "" || strings.TrimSpace(targetObjectId) == "" {
			return apperrors.NewInternalError("malformed ProcessInstance "+instanceId+" for Recall", nil)
		}

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

		// Step 5: Update ProcessInstance status to Removed
		instanceUpdates := models.SObject{
			"Status":        constants.ApprovalStatusRemoved,
			"CompletedDate": now,
		}
		if updateErr := s.persistence.Update(processTableWriteContext(txCtx, "ApprovalService: recall ProcessInstance"), constants.TableProcessInstance, instanceId, instanceUpdates, user); updateErr != nil {
			return fmt.Errorf("failed to recall approval instance: %w", updateErr)
		}

		// Step 6: Update any pending ProcessInstanceNode to Removed
		nodeFilter := fmt.Sprintf("ProcessInstanceId == '%s' && NodeStatus == '%s'", escapeApprovalFilterValue(instanceId), constants.ApprovalStatusPending)
		nodeResult, lookupErr := s.query.QueryWithFilter(txCtx, constants.TableProcessInstanceNode, nodeFilter, user, "", "", 1)
		recalledStepNodeID := ""
		recalledProcessNodeID := ""
		if lookupErr != nil {
			return apperrors.NewInternalError("failed to query pending ProcessInstanceNode for recall", lookupErr)
		}
		if nodeResult == nil {
			return apperrors.NewInternalError("pending ProcessInstanceNode query returned no result for recall", nil)
		}
		if len(nodeResult.Records) > 0 {
			nodeId, _ := nodeResult.Records[0][constants.FieldID].(string)
			recalledStepNodeID = nodeId
			recalledProcessNodeID, _ = nodeResult.Records[0]["ProcessNodeId"].(string)
			if nodeId != "" {
				nodeUpdates := models.SObject{
					"NodeStatus":    constants.ApprovalStatusRemoved,
					"CompletedDate": now,
					"LastActorId":   user.ID,
					"Comments":      comments,
				}
				if updateErr := s.persistence.Update(processTableWriteContext(txCtx, "ApprovalService: recall ProcessInstanceNode"), constants.TableProcessInstanceNode, nodeId, nodeUpdates, user); updateErr != nil {
					return apperrors.NewInternalError("failed to update ProcessInstanceNode "+nodeId+" during recall", updateErr)
				}
			}
		} else if _, runtimeBound := runtimeApprovalAuthorityFromContext(txCtx); runtimeBound {
			return apperrors.NewInternalError("Runtime Approval instance has no pending ProcessInstanceNode for recall", nil)
		}
		// Step 7: Delete every pending work item. This must read to exhaustion
		// and fail closed: reconcileApprovalShares (called later in this same
		// transaction) derives approval-owned record shares from the surviving
		// workitems, so a truncated read or a swallowed delete failure would mark
		// the instance Removed while a former approver kept read access. Abort the
		// transaction instead of best-effort logging.
		workItemsFilter := fmt.Sprintf("ProcessInstanceId == '%s'", escapeApprovalFilterValue(instanceId))
		workItems, lookupErr := s.queryAllPaged(txCtx, constants.TableProcessInstanceWorkitem, workItemsFilter, user)
		if lookupErr != nil {
			return apperrors.NewInternalError("failed to query ProcessInstanceWorkitem for recall", lookupErr)
		}
		pendingHistoryWorkItemIDs := []string{}
		seenActorIDs := make(map[string]struct{})
		for _, pendingItem := range workItems {
			pendingItemID, _ := pendingItem[constants.FieldID].(string)
			if pendingItemID == "" {
				continue
			}
			pendingHistoryWorkItemIDs = append(pendingHistoryWorkItemIDs, pendingItemID)
			recallWorkItemIDs = append(recallWorkItemIDs, pendingItemID)
			actorID, _ := pendingItem["ActorId"].(string)
			if actorID != "" {
				if _, exists := seenActorIDs[actorID]; !exists {
					seenActorIDs[actorID] = struct{}{}
					recallActorIDs = append(recallActorIDs, actorID)
				}
			}
			if delErr := s.persistence.Delete(processTableWriteContext(txCtx, "ApprovalService: recall ProcessInstanceWorkitem"), constants.TableProcessInstanceWorkitem, pendingItemID, user); delErr != nil {
				return apperrors.NewInternalError("failed to delete ProcessInstanceWorkitem "+pendingItemID+" during recall", delErr)
			}
		}
		if err := s.deletePendingApprovalHistoryRows(txCtx, pendingHistoryWorkItemIDs, user); err != nil {
			return err
		}
		if err := s.appendApprovalHistoryRow(txCtx, approvalHistoryRow{
			ProcessInstanceID: instanceId,
			StepNodeID:        recalledStepNodeID,
			ProcessNodeID:     recalledProcessNodeID,
			TargetObjectID:    targetObjectId,
			TargetObjectType:  batch.objectApiName,
			StepStatus:        constants.ApprovalStatusRemoved,
			ActorID:           user.ID,
			OriginalActorID:   workItemOriginalActorID,
			Comments:          comments,
			IsPending:         false,
			CreatedDate:       now,
		}, user); err != nil {
			return err
		}

		// Step 8: Recall lifecycle actions. FieldUpdates must be visible before
		// the API returns so the recalled record is immediately editable and can
		// be resubmitted; notification actions are stashed for post-commit
		// best-effort delivery.
		// SubmitterUserID stays the ProcessInstance submitter: on an
		// on-behalf recall the acting admin is only the actor, and recall
		// Flow actions reading Approval.SubmitterUserId must see the
		// original submitter.
		recallRuntime := approvalFlowNotificationContext{
			ProcessInstanceID: instanceId,
			WorkItemIDs:       recallWorkItemIDs,
			SubmitterUserID:   submittedById,
			ActorUserID:       user.ID,
			Comments:          comments,
		}
		notifications, execErr := s.executeRecallLifecycleActionBatch(txCtx, batch, recallActorIDs, recallRuntime, user)
		if execErr != nil {
			return execErr
		}
		recallNotifications = notifications

		// Instance is Removed and all pending workitems are deleted, so every
		// approval share for this record is revoked.
		if err := s.reconcileApprovalShares(txCtx, instanceId, user); err != nil {
			return err
		}
		return nil
	})

	if txErr != nil {
		return nil, txErr
	}

	// Post-commit: notification actions run best-effort.
	var recallFailures []ProcessActionFailure
	if recallNotifications != nil {
		recallFailures = s.executeProcessActionNotifications(ctx, *recallNotifications, user)
	}

	responseObj := models.SObject{
		constants.FieldID:   workItemID,
		"ProcessInstanceId": instanceId,
		"TargetObjectId":    targetObjectId,
		"ActorId":           user.ID,
	}
	if len(recallFailures) > 0 {
		responseObj["ActionFailures"] = recallFailures
	}

	return responseObj, nil
}
