package automation

import (
	"context"
	"fmt"
	"time"

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

// processAction is the shared logic for Approve and Reject actions.
// For Approve: checks if there is a next step in the process definition.
//   - If a next step exists, advances the chain (creates new node + workitem) without finalising.
//   - If no next step, marks the ProcessInstance as Approved.
//
// For Reject: always finalises the ProcessInstance as Rejected.
func (s *ApprovalService) processAction(ctx context.Context, workItemID, newStatus, comments string, user *models.UserSession) (models.SObject, error) {
	ctx = constants.WithSecurityContext(ctx, constants.SystemContext, "ApprovalService: managing approval system tables")

	actionCtx, err := s.loadApprovalActionContext(ctx, workItemID, user)
	if err != nil {
		return nil, err
	}
	instanceId := actionCtx.instanceID
	workItemActorId := actionCtx.workItemActorID
	workItemOriginalActorID := actionCtx.workItemOriginalActorID
	processDefId := actionCtx.processDefinitionID
	targetObjectId := actionCtx.targetObjectID
	submittedByID := actionCtx.submittedByID

	now := time.Now().UTC().Format(time.RFC3339)

	// NX#4150 Class C. Request identity is captured before the transaction so a
	// rollback cannot take it with it, and startedAt measures the whole request
	// including retried attempts - that is the latency the approver felt.
	outcomeReq := approvalActionOutcomeRequest{
		actionEvent:         approvalActionRequestVerb(newStatus),
		workItemID:          workItemID,
		processInstanceID:   instanceId,
		processDefinitionID: processDefId,
		targetObjectID:      targetObjectId,
		requestedStatus:     newStatus,
		actor:               user,
		startedAt:           time.Now(),
	}
	var actionCounter approvalActionCounter
	// Captured just before the closure returns so the completion hook, which
	// fires afterwards, can record WHY this attempt failed and not merely that
	// it did.
	var attemptErr error

	// Run steps 3-6 in a transaction to ensure atomicity.
	// If next-step resolution or creation fails, the node update and workitem
	// deletion are rolled back, preventing premature finalization.
	var nextActorIDs []string
	var newWorkItemIDs []string
	advancedToNext := false
	stepStillPending := false
	terminalStatus := newStatus
	currentProcessNodeDefID := ""
	processObjectAPIName := ""
	var pendingNotificationBatches []pendingNotifications

	attempt := func(txCtx context.Context) error {
		processDef, loadDefErr := s.loadProcessDefinitionForAction(txCtx, processDefId, user)
		if loadDefErr != nil {
			return loadDefErr
		}
		processObjectAPIName, _ = processDef["TableEnumOrId"].(string)

		// Step 3: Find the current pending ProcessInstanceNode and its backing ProcessNode.
		var currentProcessNode models.SObject
		var currentNodeID string
		nodeFilter := fmt.Sprintf("ProcessInstanceId == '%s' && NodeStatus == '%s'", escapeApprovalFilterValue(instanceId), constants.ApprovalStatusPending)
		nodeResult, err := s.query.QueryWithFilter(txCtx, constants.TableProcessInstanceNode, nodeFilter, user, "", "", 1)
		if err != nil {
			return apperrors.NewInternalError("failed to load current approval step", err)
		}
		if nodeResult == nil || len(nodeResult.Records) == 0 {
			return apperrors.NewInternalError("approval work item has no pending approval step", nil)
		}
		currentNodeID, _ = nodeResult.Records[0][constants.FieldID].(string)
		if currentNodeID == "" {
			return apperrors.NewInternalError("pending approval step missing Id", nil)
		}
		processNodeID, _ := nodeResult.Records[0]["ProcessNodeId"].(string)
		currentProcessNodeDefID = processNodeID
		processNodeName, _ := nodeResult.Records[0]["ProcessNodeName"].(string)
		if processNodeID == "" {
			return apperrors.NewInternalError("pending approval step missing ProcessNodeId", nil)
		}
		processNodeFilter := fmt.Sprintf("Id == '%s'", escapeApprovalFilterValue(processNodeID))
		processNodeResult, nodeErr := s.query.QueryWithFilter(txCtx, constants.TableProcessNode, processNodeFilter, user, "", "", 1)
		if nodeErr != nil {
			return apperrors.NewInternalError("failed to load current ProcessNode "+processNodeID, nodeErr)
		}
		if processNodeResult != nil && len(processNodeResult.Records) > 0 {
			currentProcessNode = processNodeResult.Records[0]
		} else {
			if _, runtimeBound := runtimeApprovalAuthorityFromContext(txCtx); runtimeBound {
				return apperrors.NewInternalError("current ProcessNode "+processNodeID+" is absent from Runtime Approval authority", nil)
			}
			currentProcessNode = models.SObject{
				constants.FieldID: processNodeID,
				"Name":            processNodeName,
			}
		}

		// Step 4: Gather pending work items for the current step so we can
		// enforce FirstResponse vs Unanimous behavior transactionally. Read to
		// exhaustion: this set drives both the sibling deletes and the
		// remaining/next-actor computation, and reconcileApprovalShares later
		// derives approval-owned record shares from whatever workitems survive —
		// a truncated read would strand shares for the dropped approvers.
		workItemsFilter := fmt.Sprintf("ProcessInstanceId == '%s'", escapeApprovalFilterValue(instanceId))
		workItems, err := s.queryAllPaged(txCtx, constants.TableProcessInstanceWorkitem, workItemsFilter, user)
		if err != nil {
			return apperrors.NewInternalError("failed to query ProcessInstanceWorkitem siblings", err)
		}

		pendingWorkItemIDs := make([]string, 0, len(workItems))
		remainingActorIDs := make([]string, 0, len(workItems))
		remainingWorkItemIDs := make([]string, 0, len(workItems))
		actorByWorkItemID := make(map[string]string, len(workItems))
		for _, item := range workItems {
			itemID, _ := item[constants.FieldID].(string)
			if itemID == "" {
				continue
			}
			pendingWorkItemIDs = append(pendingWorkItemIDs, itemID)
			if actorID, _ := item["ActorId"].(string); actorID != "" {
				actorByWorkItemID[itemID] = actorID
			}
			if itemID == workItemID {
				continue
			}
			remainingWorkItemIDs = append(remainingWorkItemIDs, itemID)
			if actorID := actorByWorkItemID[itemID]; actorID != "" {
				remainingActorIDs = append(remainingActorIDs, actorID)
			}
		}

		approvalMode := constants.ApprovalModeFirstResponse
		if processDef != nil && currentProcessNode != nil {
			var modeErr error
			approvalMode, modeErr = s.approvalModeForNode(processDef, currentProcessNode)
			if modeErr != nil {
				return modeErr
			}
		}

		// In Unanimous mode, a single approval only clears that approver's work
		// item while the step remains pending until every approver has responded.
		finalizeCurrentStep := true
		deleteIDs := pendingWorkItemIDs
		if newStatus == constants.ApprovalStatusApproved && constants.NormalizeApprovalMode(approvalMode) == constants.ApprovalModeUnanimous && len(remainingWorkItemIDs) > 0 {
			finalizeCurrentStep = false
			stepStillPending = true
			nextActorIDs = remainingActorIDs
			newWorkItemIDs = remainingWorkItemIDs
			deleteIDs = []string{workItemID}
		}

		stepActionEvent := ""
		if newStatus == constants.ApprovalStatusApproved {
			stepActionEvent = constants.ApprovalActionEventStepApproval
		} else if newStatus == constants.ApprovalStatusRejected {
			stepActionEvent = constants.ApprovalActionEventStepRejection
		}
		var stepBatch lifecycleActionBatch
		if stepActionEvent != "" {
			var fetchErr error
			stepBatch, fetchErr = s.prepareRequiredLifecycleActions(txCtx, processDefId, processObjectAPIName, targetObjectId, currentProcessNodeDefID, stepActionEvent, user)
			if fetchErr != nil {
				return fetchErr
			}
			actionCounter.add(len(stepBatch.actions))
		}
		var stepPlanErr error
		txCtx, stepPlanErr = s.withPlannedLifecycleParentPrelocks(txCtx, user, stepBatch)
		if stepPlanErr != nil {
			return stepPlanErr
		}

		// Current-step FieldUpdates are part of the transition input: downstream
		// step criteria and related approver resolution must see these
		// in-transaction target-record changes before selecting the next step.
		stepRuntime := approvalFlowNotificationContext{
			ProcessInstanceID: instanceId,
			WorkItemIDs:       []string{workItemID},
			SubmitterUserID:   submittedByID,
			ActorUserID:       user.ID,
			ApproverUserIDs:   []string{workItemActorId},
			Comments:          comments,
		}
		if notifications, execErr := s.executeLifecycleActionBatch(txCtx, stepBatch, nil, stepRuntime, user); execErr != nil {
			return execErr
		} else if notifications != nil {
			pendingNotificationBatches = append(pendingNotificationBatches, *notifications)
		}

		transition := approvalStepTransition{}
		if finalizeCurrentStep && newStatus == constants.ApprovalStatusApproved && processDefId != "" {
			var advanceErr error
			transition, advanceErr = s.determineNextApprovalStep(txCtx, processDef, currentProcessNode, targetObjectId, submittedByID, user)
			if advanceErr != nil {
				return advanceErr
			}
		}
		if finalizeCurrentStep && newStatus == constants.ApprovalStatusRejected && processDefId != "" {
			var rejectErr error
			transition, rejectErr = s.determineRejectBackToPrevious(txCtx, processDef, instanceId, currentProcessNode, targetObjectId, submittedByID, user)
			if rejectErr != nil {
				return rejectErr
			}
		}
		if transition.terminalStatus != "" {
			terminalStatus = transition.terminalStatus
		}

		var terminalBatch lifecycleActionBatch
		var nextStepBatch lifecycleActionBatch
		if finalizeCurrentStep {
			if transition.advanced {
				if newStatus == constants.ApprovalStatusApproved && len(transition.actorIDs) > 0 {
					var fetchErr error
					nextStepBatch, fetchErr = s.prepareRequiredLifecycleActions(txCtx, processDefId, processObjectAPIName, targetObjectId, "", constants.ApprovalActionEventStepApproval, user)
					if fetchErr != nil {
						return fetchErr
					}
					actionCounter.add(len(nextStepBatch.actions))
				}
			} else {
				terminalEvent := ""
				if terminalStatus == constants.ApprovalStatusApproved {
					terminalEvent = constants.ApprovalActionEventFinalApproval
				} else if terminalStatus == constants.ApprovalStatusRejected {
					terminalEvent = constants.ApprovalActionEventFinalRejection
				}
				if terminalEvent != "" {
					var fetchErr error
					terminalBatch, fetchErr = s.prepareRequiredLifecycleActions(txCtx, processDefId, processObjectAPIName, targetObjectId, "", terminalEvent, user)
					if fetchErr != nil {
						return fetchErr
					}
					actionCounter.add(len(terminalBatch.actions))
				}
			}
		}

		// The selected downstream lifecycle mutation is now known, while no
		// ProcessInstanceHistory/Node/Instance row for this transition has been
		// written yet. Carry its parent-lock intent into the first runtime-row
		// mutation so Records acquires one ordered set. Without this boundary the
		// runtime insert can establish a ProcessInstanceHistory high-water mark
		// before a lower-sorting business rollup parent is discovered.
		if transition.advanced {
			txCtx, err = s.withPlannedLifecycleParentPrelocks(txCtx, user, nextStepBatch)
		} else {
			txCtx, err = s.withPlannedLifecycleParentPrelocks(txCtx, user, terminalBatch)
		}
		if err != nil {
			return err
		}

		for _, deleteID := range deleteIDs {
			if delErr := s.persistence.Delete(processTableWriteContext(txCtx, "ApprovalService: delete ProcessInstanceWorkitem"), constants.TableProcessInstanceWorkitem, deleteID, user); delErr != nil {
				return fmt.Errorf("failed to delete ProcessInstanceWorkitem %s: %w", deleteID, delErr)
			}
		}
		if err := s.deletePendingApprovalHistoryRows(txCtx, deleteIDs, user); err != nil {
			return err
		}

		if err := s.appendApprovalHistoryRow(txCtx, approvalHistoryRow{
			ProcessInstanceID: instanceId,
			StepNodeID:        currentNodeID,
			ProcessNodeID:     currentProcessNodeDefID,
			TargetObjectID:    targetObjectId,
			TargetObjectType:  processObjectAPIName,
			StepStatus:        newStatus,
			ActorID:           user.ID,
			OriginalActorID:   workItemOriginalActorID,
			Comments:          comments,
			IsPending:         false,
			CreatedDate:       now,
		}, user); err != nil {
			return err
		}

		if !finalizeCurrentStep {
			// Unanimous step with approvers still pending: only the acting
			// approver's workitem was deleted, so reconcile drops just their
			// share and leaves the remaining approvers' shares in place.
			return s.reconcileApprovalShares(txCtx, instanceId, user)
		}

		if currentNodeID != "" {
			nodeUpdates := models.SObject{
				"NodeStatus":    newStatus,
				"CompletedDate": now,
				"LastActorId":   user.ID,
				"Comments":      comments,
			}
			if updateErr := s.persistence.Update(processTableWriteContext(txCtx, "ApprovalService: update ProcessInstanceNode"), constants.TableProcessInstanceNode, currentNodeID, nodeUpdates, user); updateErr != nil {
				return fmt.Errorf("failed to update ProcessInstanceNode: %w", updateErr)
			}
		}

		if transition.advanced {
			var createErr error
			advancedToNext, nextActorIDs, newWorkItemIDs, createErr = s.createNextStepRecords(txCtx, instanceId, targetObjectId, processObjectAPIName, transition.nextNodeID, transition.nextNodeName, transition.actorIDs, now, user)
			if createErr != nil {
				return createErr
			}
		}

		// Step 6: If we did NOT advance/rollback to another step, finalise the ProcessInstance
		if !advancedToNext {
			instanceUpdates := models.SObject{
				"Status":        terminalStatus,
				"CompletedDate": now,
			}
			if updateErr := s.persistence.Update(processTableWriteContext(txCtx, "ApprovalService: update ProcessInstance"), constants.TableProcessInstance, instanceId, instanceUpdates, user); updateErr != nil {
				return fmt.Errorf("failed to finalize ProcessInstance: %w", updateErr)
			}
		}

		// Step 7: Terminal and next-step lifecycle actions — run FieldUpdates
		// inside the tx so they share atomicity with the state transition.
		// Notification actions are stashed for post-commit best-effort delivery.
		if !advancedToNext && !stepStillPending {
			terminalRuntime := approvalFlowNotificationContext{
				ProcessInstanceID: instanceId,
				WorkItemIDs:       []string{workItemID},
				SubmitterUserID:   submittedByID,
				ActorUserID:       user.ID,
				Comments:          comments,
			}
			if notifications, execErr := s.executeLifecycleActionBatch(txCtx, terminalBatch, nil, terminalRuntime, user); execErr != nil {
				return execErr
			} else if notifications != nil {
				pendingNotificationBatches = append(pendingNotificationBatches, *notifications)
			}
		}

		if newStatus == constants.ApprovalStatusApproved && advancedToNext && len(nextActorIDs) > 0 {
			nextStepRuntime := approvalFlowNotificationContext{
				ProcessInstanceID: instanceId,
				WorkItemIDs:       newWorkItemIDs,
				SubmitterUserID:   submittedByID,
				ActorUserID:       user.ID,
				Comments:          comments,
			}
			if notifications, execErr := s.executeLifecycleActionBatch(txCtx, nextStepBatch, nextActorIDs, nextStepRuntime, user); execErr != nil {
				return execErr
			} else if notifications != nil {
				pendingNotificationBatches = append(pendingNotificationBatches, *notifications)
			}
		}

		// Advance: revoke shares for the resolved step's approvers and grant the
		// next step's. Finalize (Approved/Rejected): all workitems are deleted, so
		// every approval share is revoked.
		if err := s.reconcileApprovalShares(txCtx, instanceId, user); err != nil {
			return err
		}
		return nil
	}
	txErr := s.persistence.RunInPessimisticTransaction(ctx, func(txCtx context.Context) error {
		// Reset closure variables on each attempt (safe across deadlock retries).
		advancedToNext = false
		stepStillPending = false
		terminalStatus = newStatus
		nextActorIDs = nil
		newWorkItemIDs = nil
		currentProcessNodeDefID = ""
		processObjectAPIName = ""
		pendingNotificationBatches = nil
		actionCounter.reset()
		attemptErr = nil

		// Register on the actual transaction attempt. The completion hook runs
		// after this closure returns and reads the error captured below, including
		// failures raised while installing or running the parent-lock coordinator.
		if !s.registerApprovalActionOutcome(txCtx, &outcomeReq, actionCounter.value, func() error { return attemptErr }, nil) {
			s.noteMissingApprovalBoundary(outcomeReq)
		}
		attemptErr = s.runWithParentLockCoordinatorIfSupported(txCtx, attempt)
		return attemptErr
	})

	if txErr != nil {
		return nil, txErr
	}

	// Determine actual instance status for the response
	actualStatus := terminalStatus
	if advancedToNext || stepStillPending {
		actualStatus = constants.ApprovalStatusPending
	}

	// Post-commit: notification actions run best-effort. Terminal-state
	// (FinalApproval / FinalRejection) and next-step (StepApproval) mutations
	// are already dispatched inside the transaction above; only notifications
	// remain here.
	var actionFailures []ProcessActionFailure
	for _, batch := range pendingNotificationBatches {
		if fs := s.executeProcessActionNotifications(ctx, batch, user); len(fs) > 0 {
			actionFailures = append(actionFailures, fs...)
		}
	}

	responseObj := models.SObject{
		"ProcessInstanceId": instanceId,
		"TargetObjectId":    targetObjectId,
		"InstanceStatus":    actualStatus,
		"ActorIds":          nextActorIDs,
		"NewWorkitemIds":    newWorkItemIDs,
	}
	if len(actionFailures) > 0 {
		responseObj["ActionFailures"] = actionFailures
	}
	if len(newWorkItemIDs) > 0 {
		responseObj[constants.FieldID] = newWorkItemIDs[0]
	} else {
		responseObj[constants.FieldID] = workItemID
	}
	if len(nextActorIDs) > 0 {
		responseObj["ActorId"] = nextActorIDs[0]
	} else {
		responseObj["ActorId"] = user.ID
	}

	return responseObj, nil
}
