package automation

import (
	"context"
	"errors"
	"slices"
	"time"

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

type approvalSubmitRuntimeRequest struct {
	processDefinitionID string
	objectName          string
	targetRecordID      string
	comments            string
	firstNodeID         string
	firstNodeName       string
	actorIDs            []string
	initialStatus       string
	instanceRecord      models.SObject
	now                 string
	chainID             string
	skipEntryCriteria   bool
	runtimeEligibility  *SubmitEligibility
}

type approvalSubmitRuntimeResult struct {
	instanceID          string
	workItemIDs         []string
	submitNotifications pendingNotifications
	finalNotifications  pendingNotifications
}

func (s *ApprovalService) createSubmitRuntimeRows(ctx context.Context, req approvalSubmitRuntimeRequest, user *models.UserSession) (approvalSubmitRuntimeResult, error) {
	var result approvalSubmitRuntimeResult

	// NX#4150 Class C. Submit has no work item yet - it creates one - so the
	// row is keyed on the process definition and target record instead. That is
	// the identity a submitter would report if the submission silently did
	// nothing.
	outcomeReq := approvalActionOutcomeRequest{
		actionEvent:         ApprovalActionRequestSubmit,
		processDefinitionID: req.processDefinitionID,
		targetObjectID:      req.targetRecordID,
		requestedStatus:     req.initialStatus,
		actor:               user,
		startedAt:           time.Now(),
	}
	var actionCounter approvalActionCounter
	var submitAttemptErr error

	txErr := s.persistence.RunInPessimisticTransaction(ctx, func(txCtx context.Context) error {
		submitAttempt := func(txCtx context.Context) error {
			result = approvalSubmitRuntimeResult{}
			actionCounter.reset()
			submitAttemptErr = nil
			if !s.registerApprovalActionOutcome(txCtx, &outcomeReq, actionCounter.value, func() error { return submitAttemptErr },
				func() approvalActionIdentity {
					return approvalActionIdentity{processInstanceID: result.instanceID}
				}) {
				s.noteMissingApprovalBoundary(outcomeReq)
			}

			submitBatch, fetchErr := s.prepareRequiredLifecycleActions(txCtx, req.processDefinitionID, req.objectName, req.targetRecordID, "", constants.ApprovalActionEventSubmit, user)
			if fetchErr != nil {
				return fetchErr
			}
			actionCounter.add(len(submitBatch.actions))
			startPendingApproval := req.initialStatus == constants.ApprovalStatusPending
			var finalBatch lifecycleActionBatch
			if !startPendingApproval {
				finalEvent := constants.ApprovalActionEventFinalApproval
				if req.initialStatus == constants.ApprovalStatusRejected {
					finalEvent = constants.ApprovalActionEventFinalRejection
				}
				finalBatch, fetchErr = s.prepareRequiredLifecycleActions(txCtx, req.processDefinitionID, req.objectName, req.targetRecordID, "", finalEvent, user)
				if fetchErr != nil {
					return fetchErr
				}
				actionCounter.add(len(finalBatch.actions))
			}

			// Lifecycle FieldUpdates can fan out into lookup and rollup parents.
			// Derive those intents before the first approval row lock so this
			// transaction acquires one global lock set instead of discovering a
			// lower-sorting parent after Process/User rows and replaying the whole
			// approval request. The prepared preview is discarded; the write path
			// re-plans and revalidates against the locked target below.
			plannedBatches := []lifecycleActionBatch{submitBatch}
			if !startPendingApproval {
				plannedBatches = append(plannedBatches, finalBatch)
			}
			var planErr error
			txCtx, planErr = s.withPlannedLifecycleParentPrelocks(txCtx, user, plannedBatches...)
			if planErr != nil {
				return planErr
			}

			// The target row is the serialization key for the one-pending-
			// approval invariant. Acquire it in the same globally ordered lock
			// set as the approval parents, then recheck pending state in this
			// transaction before any lineage, business or approval-state write.
			if err := s.prelockSubmitRuntimeParents(txCtx, req.objectName, req.targetRecordID, req.processDefinitionID, req.firstNodeID, req.actorIDs, user.ID, startPendingApproval); err != nil {
				return err
			}
			if err := s.requireNoPendingApproval(txCtx, req.objectName, req.targetRecordID, user); err != nil {
				return err
			}
			if req.runtimeEligibility != nil && req.runtimeEligibility.runtimeRevalidate {
				if err := s.revalidateRuntimeSubmitPlan(txCtx, req, user); err != nil {
					return err
				}
			}

			// Record the (chain, record, process) submit fact inside the same
			// transaction as approval state. The unique key deduplicates one
			// causal chain; the target-row lock above serializes distinct chains.
			if s.submitLineage == nil {
				return apperrors.NewInternalError("approval submit lineage store is not wired", nil)
			}
			if err := s.submitLineage.RecordSubmitted(txCtx, req.chainID, req.targetRecordID, req.processDefinitionID); err != nil {
				if errors.Is(err, ErrApprovalSubmitLineageDuplicate) {
					return ErrRecordAlreadyPending
				}
				return apperrors.NewInternalError("failed to record approval submit lineage", err)
			}

			if err := s.executeSubmitFieldUpdateMutationsBeforeRuntimeRows(txCtx, submitBatch.actions, submitBatch.objectApiName, submitBatch.targetID, user); err != nil {
				return err
			}

			createdInstance, err := s.persistence.Insert(processTableWriteContext(txCtx, "ApprovalService: create ProcessInstance"), constants.TableProcessInstance, req.instanceRecord, user)
			if err != nil {
				s.logger.Error("Failed to create ProcessInstance", err)
				return apperrors.NewInternalError("failed to create approval instance", err)
			}
			result.instanceID, _ = createdInstance[constants.FieldID].(string)

			if err := s.appendApprovalHistoryRow(txCtx, approvalHistoryRow{
				ProcessInstanceID: result.instanceID,
				TargetObjectID:    req.targetRecordID,
				TargetObjectType:  req.objectName,
				StepStatus:        "Started",
				ActorID:           user.ID,
				Comments:          req.comments,
				IsPending:         false,
				CreatedDate:       req.now,
			}, user); err != nil {
				return err
			}

			firstStepNodeID := ""
			if startPendingApproval && req.firstNodeID != "" {
				nodeInstanceRecord := models.SObject{
					"ProcessInstanceId": result.instanceID,
					"ProcessNodeId":     req.firstNodeID,
					"ProcessNodeName":   req.firstNodeName,
					"NodeStatus":        constants.ApprovalStatusPending,
					"CreatedDate":       req.now,
				}
				createdNode, insertErr := s.persistence.Insert(processTableWriteContext(txCtx, "ApprovalService: create ProcessInstanceNode"), constants.TableProcessInstanceNode, nodeInstanceRecord, user)
				if insertErr != nil {
					s.logger.Error("Failed to create ProcessInstanceNode", insertErr)
					return apperrors.NewInternalError("failed to create approval step instance", insertErr)
				}
				firstStepNodeID, _ = createdNode[constants.FieldID].(string)
			}

			if startPendingApproval {
				workItemIDs, err := s.createWorkitems(txCtx, result.instanceID, req.actorIDs, req.comments, req.now, user)
				if err != nil {
					s.logger.Error("Failed to create ProcessInstanceWorkitem", err)
					return apperrors.NewInternalError("failed to create approval work item", err)
				}
				result.workItemIDs = workItemIDs
				for i, actorID := range req.actorIDs {
					historyID := ""
					if i < len(result.workItemIDs) {
						historyID = result.workItemIDs[i]
					}
					if err := s.appendApprovalHistoryRow(txCtx, approvalHistoryRow{
						ID:                historyID,
						ProcessInstanceID: result.instanceID,
						StepNodeID:        firstStepNodeID,
						ProcessNodeID:     req.firstNodeID,
						TargetObjectID:    req.targetRecordID,
						TargetObjectType:  req.objectName,
						StepStatus:        constants.ApprovalStatusPending,
						ActorID:           actorID,
						OriginalActorID:   actorID,
						Comments:          req.comments,
						IsPending:         true,
						CreatedDate:       req.now,
					}, user); err != nil {
						return err
					}
				}
			}

			submitRuntime := approvalFlowNotificationContext{
				ProcessInstanceID: result.instanceID,
				WorkItemIDs:       result.workItemIDs,
				SubmitterUserID:   user.ID,
				ActorUserID:       user.ID,
				Comments:          req.comments,
			}
			submitNotifications, notifErr := s.pendingNotificationsForLifecycleBatch(txCtx, submitBatch, req.actorIDs, submitRuntime, user)
			if notifErr != nil {
				return notifErr
			}
			result.submitNotifications = submitNotifications

			if !startPendingApproval {
				if err := s.appendApprovalHistoryRow(txCtx, approvalHistoryRow{
					ProcessInstanceID: result.instanceID,
					TargetObjectID:    req.targetRecordID,
					TargetObjectType:  req.objectName,
					StepStatus:        req.initialStatus,
					ActorID:           user.ID,
					Comments:          req.comments,
					IsPending:         false,
					CreatedDate:       req.now,
				}, user); err != nil {
					return err
				}
				finalRuntime := approvalFlowNotificationContext{
					ProcessInstanceID: result.instanceID,
					SubmitterUserID:   user.ID,
					ActorUserID:       user.ID,
					Comments:          req.comments,
				}
				if notifications, execErr := s.executeLifecycleActionBatch(txCtx, finalBatch, nil, finalRuntime, user); execErr != nil {
					return execErr
				} else if notifications != nil {
					result.finalNotifications = *notifications
				}
			}

			return s.reconcileApprovalShares(txCtx, result.instanceID, user)
		}
		// Capture the attempt error for the completion hook, which fires after
		// this returns and would otherwise record the failure without its cause.
		attemptOutcomeErr := s.runWithParentLockCoordinatorIfSupported(txCtx, submitAttempt)
		submitAttemptErr = attemptOutcomeErr
		return attemptOutcomeErr
	})
	if txErr != nil {
		return approvalSubmitRuntimeResult{}, txErr
	}
	return result, nil
}

// revalidateRuntimeSubmitPlan current-reads the target only after the complete
// canonical parent lock set is held. It rejects a pre-lock plan that no longer
// matches current entry criteria or initial routing; the outer Runtime
// transaction then rolls back its invocation acceptance and the caller may
// retry the same idempotency key against the new state.
func (s *ApprovalService) revalidateRuntimeSubmitPlan(
	ctx context.Context,
	req approvalSubmitRuntimeRequest,
	user *models.UserSession,
) error {
	lockedRecord, err := s.persistence.GetLock(ctx, req.objectName, req.targetRecordID)
	if err != nil {
		return apperrors.NewInternalError("failed to current-read Runtime Approval submit target", err)
	}
	if lockedRecord == nil || approvalStringValue(lockedRecord[constants.FieldID]) != req.targetRecordID {
		return apperrors.NewNotFoundError(req.objectName, req.targetRecordID)
	}
	currentRecord := cloneAutomationSObject(lockedRecord)
	models.StampObjectType(currentRecord, req.objectName)
	lockedCtx := withRuntimeApprovalLockedCriteriaTarget(ctx, req.objectName, req.targetRecordID, currentRecord)
	current, err := s.resolveSubmitEligibilityAgainstRecord(
		lockedCtx,
		SubmitRequest{
			ObjectAPIName: req.objectName, RecordID: req.targetRecordID,
			SkipEntryCriteria: req.skipEntryCriteria,
		},
		req.targetRecordID,
		req.objectName,
		req.runtimeEligibility.TargetRecord,
		user,
	)
	if err != nil {
		return err
	}
	if current.ProcessDefinitionID != req.processDefinitionID {
		return apperrors.NewConflictError("approval", "submit plan", "definition changed during admission")
	}
	if s.permissions != nil {
		var schema *models.ObjectMetadata
		if s.schema != nil {
			schema = s.schema.GetSchema(lockedCtx, req.objectName)
		}
		if s.permissions.IsRecordLockedForGate(lockedCtx, schema, currentRecord, user) {
			return apperrors.NewPermissionError("submit", "record is locked and cannot be submitted for approval")
		}
	}
	canSubmit, err := s.canUserSubmitForProcess(lockedCtx, current.ProcessDefinition, req.objectName, req.targetRecordID, user)
	if err != nil {
		return apperrors.NewInternalError("failed to revalidate approval submitter permissions", err)
	}
	if !canSubmit {
		return ErrSubmitPermissionDenied
	}
	nodes, err := s.ensureProcessNodesForDefinition(lockedCtx, current.ProcessDefinition, user)
	if err != nil {
		return err
	}
	firstNode, actorIDs, initialStatus, err := s.selectInitialApprovalStep(
		lockedCtx, current.ProcessDefinition, nodes, req.objectName, req.targetRecordID, user.ID, user,
	)
	if err != nil {
		return err
	}
	firstNodeID := approvalStringValue(firstNode[constants.FieldID])
	firstNodeName := approvalStringValue(firstNode["Name"])
	if firstNodeID != req.firstNodeID || firstNodeName != req.firstNodeName ||
		initialStatus != req.initialStatus || !slices.Equal(actorIDs, req.actorIDs) {
		return apperrors.NewConflictError("approval", "submit plan", "routing changed during admission")
	}
	return nil
}
