package automation

import (
	"context"
	"fmt"
	"strings"

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

// ProcessActionFailure is a structured record of a ProcessAction that failed
// during best-effort notification execution (Alert, unknown action types).
// FieldUpdate failures are not surfaced here: synchronous lifecycle paths abort
// their enclosing approval transaction. Callers attach notification failures to
// the parent response payload so the UI can surface a warning banner.
type ProcessActionFailure struct {
	ActionName   string `json:"actionName"`
	ActionType   string `json:"actionType"`
	TargetObject string `json:"targetObject,omitempty"`
	TargetID     string `json:"targetId,omitempty"`
	Field        string `json:"field,omitempty"`
	Reason       string `json:"reason"`
}

// ApprovalActionError is returned when an approval FieldUpdate action fails
// inside the approval transaction. Callers type-assert to distinguish a
// linked-record update failure from an infrastructure error.
type ApprovalActionError struct {
	ActionName   string
	ActionType   string
	TargetObject string
	TargetID     string
	Field        string
	Reason       string
	Cause        error
}

func (e *ApprovalActionError) Error() string {
	return fmt.Sprintf("approval action %q (%s) failed on %s.%s id=%s: %s",
		e.ActionName, e.ActionType, e.TargetObject, e.Field, e.TargetID, e.Reason)
}

func (e *ApprovalActionError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Cause
}

// pendingNotifications carries ProcessAction records whose FieldUpdate
// mutations were already applied inside a transaction and whose notification
// actions still need to run best-effort after the tx commits.
type pendingNotifications struct {
	actions       []models.SObject
	objectApiName string
	targetID      string
	actorIDs      []string
	// flowTargetSnapshot is the $Record payload for notification Flows,
	// captured inside the approval transaction under the engine read
	// principal (approvalNotificationSnapshotReadContext, #4397 R1). When it
	// is nil while Flow actions are pending, flowTargetSnapshotErr carries
	// the user-safe capture failure (NotFound/Validation only — the capture
	// classifier aborts the transaction on anything else per the v63
	// contract) and the Flows fail closed as delivery failures instead of
	// running with an empty $Record.
	flowTargetSnapshot    models.SObject
	flowTargetSnapshotErr error
	approval              approvalFlowNotificationContext
}

type lifecycleActionBatch struct {
	actions       []models.SObject
	objectApiName string
	targetID      string
	processDefID  string
	processNodeID string
	actionEvent   string
}

type approvalFlowNotificationContext struct {
	ActionEvent         string
	ProcessDefinitionID string
	ProcessInstanceID   string
	ProcessNodeID       string
	WorkItemIDs         []string
	TargetObjectAPIName string
	TargetRecordID      string
	SubmitterUserID     string
	ActorUserID         string
	ApproverUserIDs     []string
	Comments            string
	RequestURL          string
}

func (b lifecycleActionBatch) approvalContext(actorIDs []string, runtime approvalFlowNotificationContext) approvalFlowNotificationContext {
	ctx := runtime
	ctx.ActionEvent = b.actionEvent
	ctx.ProcessDefinitionID = b.processDefID
	ctx.ProcessNodeID = b.processNodeID
	ctx.TargetObjectAPIName = b.objectApiName
	ctx.TargetRecordID = b.targetID
	if ctx.ApproverUserIDs == nil {
		ctx.ApproverUserIDs = copyApprovalStringSlice(actorIDs)
	} else {
		ctx.ApproverUserIDs = copyApprovalStringSlice(ctx.ApproverUserIDs)
	}
	ctx.WorkItemIDs = copyApprovalStringSlice(ctx.WorkItemIDs)
	return ctx
}

func copyApprovalStringSlice(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		out = append(out, value)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func (c approvalFlowNotificationContext) flowInputs(targetRecord models.SObject) map[string]interface{} {
	approval := map[string]interface{}{
		"ActionEvent":         c.ActionEvent,
		"ProcessDefinitionId": c.ProcessDefinitionID,
		"ProcessInstanceId":   c.ProcessInstanceID,
		"WorkItemIds":         copyApprovalStringSlice(c.WorkItemIDs),
		"TargetObjectApiName": c.TargetObjectAPIName,
		"TargetRecordId":      c.TargetRecordID,
		"SubmitterUserId":     c.SubmitterUserID,
		"ActorUserId":         c.ActorUserID,
		"ApproverUserIds":     copyApprovalStringSlice(c.ApproverUserIDs),
		"Comments":            c.Comments,
		"RequestUrl":          c.RequestURL,
	}
	return map[string]interface{}{
		"$Record":  targetRecord,
		"record":   targetRecord,
		"Approval": approval,
	}
}

// fetchProcessActions queries ProcessAction records for a given lifecycle
// event and filters by ProcessNodeId scoping: when processNodeId is set, only
// actions attached to that node are returned; otherwise only process-level
// actions (no ProcessNodeId) are returned.
func (s *ApprovalService) fetchProcessActions(ctx context.Context, processDefId, processNodeId, actionEvent string, user *models.UserSession) ([]models.SObject, error) {
	if processDefId == "" {
		return nil, nil
	}
	filter := fmt.Sprintf("ProcessDefinitionId == '%s' && ActionEvent == '%s'", escapeApprovalFilterValue(processDefId), escapeApprovalFilterValue(actionEvent))
	// Sort by ProcessAction.Order (numeric sequence column) using the
	// generated metadata constant. Issue #230: a bare "Order" string literal
	// here is a coding red-line under the L0 strong reading because it
	// visually collides with the CRM L1 SObject name "Order" — referring to
	// the column through constants.FieldProcessAction_Order makes the intent
	// unambiguous and survives any future SObject-name scan.
	result, err := s.query.QueryWithFilter(ctx, constants.TableProcessAction, filter, user, constants.FieldProcessAction_Order, "ASC", 100)
	if err != nil {
		return nil, err
	}
	if result == nil || result.TotalSize == 0 {
		return nil, nil
	}
	matched := make([]models.SObject, 0, len(result.Records))
	for _, action := range result.Records {
		actionProcessNodeID, _ := action["ProcessNodeId"].(string)
		if processNodeId != "" {
			if actionProcessNodeID != processNodeId {
				continue
			}
		} else if actionProcessNodeID != "" {
			continue
		}
		matched = append(matched, action)
	}
	return matched, nil
}

// fetchRequiredProcessActions is used by lifecycle mutation paths whose
// FieldUpdate actions must be known before approval runtime state commits.
// Lifecycle paths execute them in the transaction. A failed query must abort the
// transaction; otherwise the engine could commit approval runtime state without
// knowing whether configured FieldUpdates needed to run.
func (s *ApprovalService) fetchRequiredProcessActions(ctx context.Context, processDefId, processNodeId, actionEvent string, user *models.UserSession) ([]models.SObject, error) {
	actions, err := s.fetchProcessActions(ctx, processDefId, processNodeId, actionEvent, user)
	if err == nil {
		return actions, nil
	}

	message := fmt.Sprintf("failed to query %s ProcessActions for processDefId=%s", actionEvent, processDefId)
	if processNodeId != "" {
		message = fmt.Sprintf("%s node=%s", message, processNodeId)
	}
	s.logger.Warn(fmt.Sprintf("%s: %v", message, err))
	return nil, apperrors.NewInternalError(message, err)
}

// resolveRequiredProcessObjectAPIName is used by lifecycle mutation paths whose
// FieldUpdate actions must be executed or durably enqueued before approval
// runtime state commits. A missing or failed ProcessDefinition lookup must
// abort the transaction; otherwise the engine could commit approval runtime
// state while skipping configured FieldUpdates because it could not resolve the
// target object.
func (s *ApprovalService) resolveRequiredProcessObjectAPIName(ctx context.Context, processDefId, cached, actionEvent string, user *models.UserSession) (string, error) {
	objectAPIName, err := s.resolveProcessObjectAPIName(ctx, processDefId, cached, user)
	if err != nil {
		message := fmt.Sprintf("failed to resolve object for %s ProcessActions processDefId=%s", actionEvent, processDefId)
		s.logger.Warn(fmt.Sprintf("%s: %v", message, err))
		return "", apperrors.NewInternalError(message, err)
	}
	if objectAPIName == "" {
		message := fmt.Sprintf("failed to resolve object for %s ProcessActions processDefId=%s", actionEvent, processDefId)
		s.logger.Warn(message)
		return "", apperrors.NewInternalError(message, nil)
	}
	return objectAPIName, nil
}

// prepareRequiredLifecycleActions validates and fetches the complete
// ProcessAction context for lifecycle paths whose FieldUpdates must either run
// in the caller's transaction or be durably enqueued there. A failure here means
// the caller cannot safely know whether linked-record mutations must run, so it
// must abort before committing approval runtime state.
func (s *ApprovalService) prepareRequiredLifecycleActions(ctx context.Context, processDefId, cachedObjectAPIName, targetObjectId, processNodeId, actionEvent string, user *models.UserSession) (lifecycleActionBatch, error) {
	processDefId = strings.TrimSpace(processDefId)
	targetObjectId = strings.TrimSpace(targetObjectId)
	if processDefId == "" {
		return lifecycleActionBatch{}, apperrors.NewInternalError(fmt.Sprintf("approval lifecycle %s missing ProcessDefinitionId", actionEvent), nil)
	}
	if targetObjectId == "" {
		return lifecycleActionBatch{}, apperrors.NewInternalError(fmt.Sprintf("approval lifecycle %s missing TargetObjectId", actionEvent), nil)
	}

	objectAPIName, resolveErr := s.resolveRequiredProcessObjectAPIName(ctx, processDefId, cachedObjectAPIName, actionEvent, user)
	if resolveErr != nil {
		return lifecycleActionBatch{}, resolveErr
	}
	actions, fetchErr := s.fetchRequiredProcessActions(ctx, processDefId, processNodeId, actionEvent, user)
	if fetchErr != nil {
		return lifecycleActionBatch{}, fetchErr
	}
	return lifecycleActionBatch{
		actions:       actions,
		objectApiName: objectAPIName,
		targetID:      targetObjectId,
		processDefID:  processDefId,
		processNodeID: strings.TrimSpace(processNodeId),
		actionEvent:   strings.TrimSpace(actionEvent),
	}, nil
}

func (s *ApprovalService) executeLifecycleActionBatch(ctx context.Context, batch lifecycleActionBatch, actorIDs []string, runtime approvalFlowNotificationContext, user *models.UserSession) (*pendingNotifications, error) {
	if len(batch.actions) == 0 {
		return nil, nil
	}
	if err := s.executeLiveFieldUpdateMutations(ctx, batch.actions, batch.objectApiName, batch.targetID, user); err != nil {
		return nil, err
	}
	capture, err := s.captureApprovalFlowNotificationTargetSnapshot(ctx, batch, user)
	if err != nil {
		return nil, err
	}
	return &pendingNotifications{
		actions:               batch.actions,
		objectApiName:         batch.objectApiName,
		targetID:              batch.targetID,
		actorIDs:              actorIDs,
		flowTargetSnapshot:    capture.record,
		flowTargetSnapshotErr: capture.warn,
		approval:              batch.approvalContext(actorIDs, runtime),
	}, nil
}

func (s *ApprovalService) pendingNotificationsForLifecycleBatch(ctx context.Context, batch lifecycleActionBatch, actorIDs []string, runtime approvalFlowNotificationContext, user *models.UserSession) (pendingNotifications, error) {
	capture, err := s.captureApprovalFlowNotificationTargetSnapshot(ctx, batch, user)
	if err != nil {
		return pendingNotifications{}, err
	}
	return pendingNotifications{
		actions:               batch.actions,
		objectApiName:         batch.objectApiName,
		targetID:              batch.targetID,
		actorIDs:              actorIDs,
		flowTargetSnapshot:    capture.record,
		flowTargetSnapshotErr: capture.warn,
		approval:              batch.approvalContext(actorIDs, runtime),
	}, nil
}

func (s *ApprovalService) executeRecallLifecycleActionBatch(ctx context.Context, batch lifecycleActionBatch, actorIDs []string, runtime approvalFlowNotificationContext, user *models.UserSession) (*pendingNotifications, error) {
	if len(batch.actions) == 0 {
		return nil, nil
	}

	// Recall used to persist FieldUpdates through the durable replay path,
	// which froze all lookup targets before any action mutation ran. Preserve
	// that ordering guarantee while making the updates visible before Recall
	// returns.
	fieldUpdates, buildErr := s.replayFieldUpdatesFromActions(ctx, batch.actions, batch.objectApiName, batch.targetID)
	if buildErr != nil {
		return nil, buildErr
	}
	if err := s.executeReplayFieldUpdateMutations(ctx, fieldUpdates, user); err != nil {
		return nil, err
	}

	capture, err := s.captureApprovalFlowNotificationTargetSnapshot(ctx, batch, user)
	if err != nil {
		return nil, err
	}
	return &pendingNotifications{
		actions:               batch.actions,
		objectApiName:         batch.objectApiName,
		targetID:              batch.targetID,
		actorIDs:              actorIDs,
		flowTargetSnapshot:    capture.record,
		flowTargetSnapshotErr: capture.warn,
		approval:              batch.approvalContext(actorIDs, runtime),
	}, nil
}

// approvalSnapshotCapture is the classified outcome of a notification target
// snapshot read. Exactly one of record/warn is set on a capture that ran:
// warn carries only user-safe, non-fatal causes (NotFound / Validation) that
// the v63 contract allows to surface as successful-response actionFailures.
type approvalSnapshotCapture struct {
	record models.SObject
	warn   error
}

// captureApprovalFlowNotificationTargetSnapshot reads the notification target
// under the engine read principal and classifies failures per the checked-in
// v63 contract (docs/design/salesforce-v63-api-contract.md, "Process
// approvals: non-fatal action failure warnings"): NotFound/Validation are
// user-safe non-fatal warnings and are carried to the delivery gate; every
// other failure (infrastructure, cancellation, internal) must NOT be converted
// into a successful-response warning — it is returned as an error so the
// enclosing approval transaction aborts, mirroring the fail-closed precedent
// of fetchRequiredProcessActions above. This classification is the single
// point that decides what may enter pendingNotifications.flowTargetSnapshotErr;
// the delivery gate renders that stored cause verbatim on the strength of this
// invariant.
func (s *ApprovalService) captureApprovalFlowNotificationTargetSnapshot(ctx context.Context, batch lifecycleActionBatch, user *models.UserSession) (approvalSnapshotCapture, error) {
	if !processActionsContainFlow(batch.actions) {
		return approvalSnapshotCapture{}, nil
	}
	snapshotCtx := approvalNotificationSnapshotReadContext(ctx, batch.objectApiName)
	targetRecord, err := s.loadApprovalFlowNotificationTargetRecord(snapshotCtx, batch.objectApiName, batch.targetID, user)
	if err != nil {
		if apperrors.IsInternal(err) || !(apperrors.IsNotFound(err) || apperrors.IsValidation(err)) {
			message := fmt.Sprintf("approval notification Flow target snapshot read failed for %s %s event=%s", batch.objectApiName, batch.targetID, batch.actionEvent)
			s.logger.Error(message, err)
			return approvalSnapshotCapture{}, apperrors.NewInternalError(message, err)
		}
		s.logger.Error(fmt.Sprintf("Approval notification Flow target snapshot unreadable for %s %s event=%s; pending Flow notifications will fail closed", batch.objectApiName, batch.targetID, batch.actionEvent), err)
		return approvalSnapshotCapture{warn: err}, nil
	}
	return approvalSnapshotCapture{record: targetRecord}, nil
}

func processActionsContainFlow(actions []models.SObject) bool {
	for _, action := range actions {
		actionType, _ := action["ActionType"].(string)
		if actionType == constants.ProcessActionTypeFlow {
			return true
		}
	}
	return false
}
