package automation

import (
	"context"
	"fmt"
	"strings"

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

// executeProcessActionNotifications runs legacy Alert, native Flow, and
// unknown-type actions from the given ProcessAction slice. Best-effort:
// failures are collected and returned so the caller can attach them to the
// response payload. Never called inside an approval transaction — notification
// actions should not block state.
func (s *ApprovalService) executeProcessActionNotifications(ctx context.Context, pending pendingNotifications, user *models.UserSession) []ProcessActionFailure {
	if len(pending.actions) == 0 {
		return nil
	}
	var failures []ProcessActionFailure
	for _, action := range pending.actions {
		actionType, _ := action["ActionType"].(string)
		actionName, _ := action["Name"].(string)
		switch actionType {
		case constants.ProcessActionTypeFieldUpdate:
			// Mutations ran inside the tx; nothing to do here.
			continue
		case constants.ProcessActionTypeAlert:
			failure := legacyWorkflowAlertRuntimeDisabledFailure(actionName, actionType, "ProcessAction.Alert")
			s.logger.Error(fmt.Sprintf("ProcessAction Alert '%s' failed closed after WorkflowAlert cutover: %s", actionName, failure.Reason), nil)
			failures = append(failures, failure)
		case constants.ProcessActionTypeFlow:
			if failure := s.executeApprovalFlowNotification(ctx, action, pending, user); failure != nil {
				failures = append(failures, *failure)
			}
		default:
			reason := fmt.Sprintf("unknown ActionType %q", actionType)
			s.logger.Warn(fmt.Sprintf("ProcessAction '%s' skipped: %s", actionName, reason))
			failures = append(failures, ProcessActionFailure{ActionName: actionName, ActionType: actionType, Reason: reason})
		}
	}
	if len(failures) > 0 {
		s.logger.Error(fmt.Sprintf("APPROVAL_NOTIFICATION_FAILURE_SUMMARY objectApiName=%s targetId=%s failures=%d", pending.objectApiName, pending.targetID, len(failures)), nil)
	}
	return failures
}

func cloneApprovalNotificationRecord(source models.SObject) models.SObject {
	if source == nil {
		return nil
	}
	cloned := make(models.SObject, len(source))
	for k, v := range source {
		cloned[k] = v
	}
	return cloned
}

func approvalFlowNotificationExecutionContext(ctx context.Context) context.Context {
	ctx = constants.WithoutStandardActionInternalAccessScopes(ctx)
	ctx = constants.WithoutStandardActionProtectedFlowExecution(ctx)
	return constants.WithSecurityContext(ctx, constants.UserContext, "ApprovalService: approval notification Flow")
}

// approvalNotificationSnapshotActionID identifies the approval engine's
// notification-target snapshot read in the standard-action scoped-access
// mechanism. It is an engine-synthesized scope (not metadata-authored): the
// approval lifecycle is the only producer and the snapshot read is the only
// consumer.
const approvalNotificationSnapshotActionID = "approval.notification-target-snapshot"

// approvalNotificationSnapshotReadContext is the read principal for the
// approval notification target snapshot (#4397 R1) — the first named
// read-principal of the #4150 Class A consolidation. Callsites that need
// these semantics must use this constructor, not assemble their own context.
//
// Ruling: the snapshot exists to render a message about a decision the engine
// has already committed, so it is read with the engine's authority for BOTH
// rows and fields — the acting approver's grants must not shape it.
//
// Rows: the actor at a terminal event is an approver whose approval-derived
// share is revoked in the same transaction (#4148) and may never have been
// effective at all on a parent-controlled child (#4397).
//
// Fields, established by runtime probe during the #4397 diagnosis:
// CheckFieldVisibilityWithUser falls back to the actor's object-level read
// when there is no FieldPermissions row for a field. An actor denied object
// read therefore loses every field to that fallback — the probe showed such a
// read selecting `[Id]` alone — so the notification Flow's $Record guards see
// absent fields and the delivery dies silently even though the row itself came
// back. Bypassing rows alone does not repair that; this is why the incident
// survived the first cut of this fix.
//
// The widening is deliberately narrow: FLS stays enforced, but only EXPLICIT
// FieldPermissions rows count. A field the actor is explicitly denied
// (PermissionsRead=false) is still stripped from the snapshot and can never
// reach a notification; only the implicit denial — the fallback to the actor's
// object-level read, which this read has already been granted an exception to —
// is ignored. fieldReadBlocked (credential/token-class fields) remains
// unconditional, and the notification Flow sandbox is untouched: no lookups, no
// record mutation, no subflows, screens or unregistered action types. The
// scoped read requires a context below SystemContext, so the
// ambient approval-transaction elevation is explicitly lowered; blocked-object
// policy remains in force on the query path. The inherited
// protected-flow-execution stamp is stripped (our own scope stamp below
// overwrites any inherited internal-access scope set wholesale) — the read's
// authority is exactly this named principal and nothing inherited.
func approvalNotificationSnapshotReadContext(ctx context.Context, objectAPIName string) context.Context {
	ctx = constants.WithoutStandardActionProtectedFlowExecution(ctx)
	ctx = constants.WithSecurityContext(ctx, constants.UserContext, "ApprovalService: approval notification target snapshot")
	return constants.WithStandardActionInternalAccessScopes(ctx, approvalNotificationSnapshotActionID, []constants.StandardActionInternalAccessScope{{
		StandardActionID: approvalNotificationSnapshotActionID,
		ObjectAPIName:    objectAPIName,
		Operation:        constants.PermRead,
		// Keep field-level security on, but count only explicit denials —
		// see the note above on the object-read fallback.
		EnforceFLS:                 true,
		IgnoreImplicitFieldDenials: true,
	}})
}

func (s *ApprovalService) loadApprovalFlowNotificationTargetRecord(ctx context.Context, objectApiName, targetObjectId string, user *models.UserSession) (models.SObject, error) {
	if strings.TrimSpace(objectApiName) == "" {
		return nil, apperrors.NewValidationError("TargetObjectApiName", "approval Flow notification requires TargetObjectApiName")
	}
	if strings.TrimSpace(targetObjectId) == "" {
		return nil, apperrors.NewValidationError("TargetRecordId", "approval Flow notification requires TargetRecordId")
	}
	if s.query == nil {
		return nil, apperrors.NewInternalError("approval Flow notification target read unavailable: QueryService is not configured", nil)
	}
	result, err := s.query.QueryWithSpec(ctx, queryspec.QuerySpec{
		ObjectAPIName: objectApiName,
		Filter:        queryspec.FieldPredicate(constants.FieldID, "=", targetObjectId),
		Limit:         1,
	}, user)
	if err != nil {
		return nil, err
	}
	if result == nil || result.TotalSize == 0 || len(result.Records) == 0 {
		return nil, apperrors.NewNotFoundError(objectApiName, targetObjectId)
	}
	targetRecord := cloneApprovalNotificationRecord(result.Records[0])
	if targetRecord == nil {
		targetRecord = models.SObject{}
	}
	if _, ok := targetRecord[constants.FieldID]; !ok {
		targetRecord[constants.FieldID] = targetObjectId
	}
	models.StampObjectType(targetRecord, objectApiName)
	return targetRecord, nil
}

func (e *FlowExecutor) approvalNotificationFlowVersion(ctx context.Context, flowAPIName string, user *models.UserSession) (*models.FlowVersion, error) {
	// Runtime Approval already carries its exact immutable Approval view. Read
	// notification-only Flow roots from that authority, not from the Mutation
	// resolver installed on the shared Records graph. The Mutation projection
	// intentionally omits these roots.
	if view, bound := metadata.RuntimeMetadataViewFromContext(ctx); bound {
		if view.Profile() != catalogsnapshot.ProjectionApprovalExecutionV1 {
			return nil, apperrors.NewInternalError("approval notification Flow metadata authority is unavailable", nil)
		}
		version, _ := view.ActiveFlowVersion(flowAPIName)
		return version, nil
	}

	// Approval notification Flow metadata (FlowDefinition + Flow) must resolve
	// regardless of the acting approver's object FLS. A non-admin final approver
	// (for example a Legal profile) lacks ObjectPermissions READ on
	// FlowDefinition/Flow, so getActiveFlowVersion's FLS-gated QueryWithSpec would
	// filter the version to nil and surface as "approval notification Flow %q is
	// not found or not active" (pingcap-inc/nexuscrm-pingcap-pack#3661).
	//
	// Legacy non-Runtime composition has no bound immutable view. Elevate ONLY
	// that metadata lookup to SystemContext: the QueryService read
	// gate keys on GetSecurityContext(ctx) >= SystemContext, so ElevatedContext
	// would not bypass it. Scope stays tight -- this wrapper has a single
	// production caller (executeApprovalFlowNotification); the elevated ctx never
	// reaches flow execution (the notification body runs on the un-elevated
	// flowCtx via executeApprovalNotificationFlowVersion, whose reused flowVersion
	// is already DefinitionRuntimeLoaded so no second metadata read occurs); and
	// the target business record is still read as the user in
	// loadApprovalFlowNotificationTargetRecord. Flow/FlowDefinition are metadata,
	// not record-scoped, so SystemContext also bypassing RLS is harmless here.
	elevatedCtx := constants.WithSecurityContext(ctx, constants.SystemContext, "ApprovalService: approval notification Flow metadata resolution")
	return e.getActiveFlowVersion(elevatedCtx, flowAPIName, user)
}

func (e *FlowExecutor) validateApprovalNotificationFlowMetadata(flowAPIName string, meta *models.FlowMetadata) error {
	return validateApprovalNotificationFlowMetadataWithRegistry(flowAPIName, meta, e.flowActionCallRegistry())
}

func ValidateApprovalNotificationFlowMetadata(flowAPIName string, meta *models.FlowMetadata) error {
	return validateApprovalNotificationFlowMetadataWithRegistry(flowAPIName, meta, nil)
}

func validateApprovalNotificationFlowMetadataWithRegistry(flowAPIName string, meta *models.FlowMetadata, actionRegistry *flowActionCallRegistry) error {
	if meta == nil {
		return apperrors.NewValidationError("Flow", fmt.Sprintf("approval notification Flow %q is not found or not active", flowAPIName))
	}
	if actionRegistry == nil {
		var err error
		actionRegistry, err = platformFlowActionCallRegistry(DefaultAppFlowActionCallRegistrations()...)
		if err != nil {
			return apperrors.NewValidationError("Flow", fmt.Sprintf("approval notification Flow %q runtime action registry is invalid: %v", flowAPIName, err))
		}
	}
	if meta.Start != nil {
		if strings.TrimSpace(meta.Start.Object) != "" ||
			strings.TrimSpace(meta.Start.TriggerType) != "" ||
			strings.TrimSpace(meta.Start.RecordTriggerType) != "" ||
			len(meta.Start.ScheduledPaths) > 0 ||
			meta.Start.Schedule != nil {
			return approvalNotificationUnsafeFlowError(flowAPIName, "Start", "start", "approval notification Flows must be invoked only from the approval lifecycle bridge")
		}
	}
	for _, element := range meta.RecordCreates {
		return approvalNotificationUnsafeFlowError(flowAPIName, "RecordCreate", element.Name, "record mutation is outside notification scope")
	}
	for _, element := range meta.RecordUpdates {
		return approvalNotificationUnsafeFlowError(flowAPIName, "RecordUpdate", element.Name, "record mutation is outside notification scope")
	}
	for _, element := range meta.RecordDeletes {
		return approvalNotificationUnsafeFlowError(flowAPIName, "RecordDelete", element.Name, "record mutation is outside notification scope")
	}
	for _, element := range meta.RecordLookups {
		return approvalNotificationUnsafeFlowError(flowAPIName, "RecordLookup", element.Name, "post-commit lookups can depend on approval-shared visibility that was already reconciled")
	}
	for _, element := range meta.Subflows {
		return approvalNotificationUnsafeFlowError(flowAPIName, "Subflow", element.Name, "subflows can hide non-notification side effects")
	}
	for _, element := range meta.Screens {
		return approvalNotificationUnsafeFlowError(flowAPIName, "Screen", element.Name, "approval notification Flows run without an interactive screen")
	}
	for _, element := range meta.Waits {
		return approvalNotificationUnsafeFlowError(flowAPIName, "Wait", element.Name, "paused work is outside notification scope")
	}
	for _, element := range meta.CustomErrors {
		return approvalNotificationUnsafeFlowError(flowAPIName, "CustomError", element.Name, "post-commit notification Flows must not gate approval state")
	}
	for _, action := range meta.ActionCalls {
		actionType := strings.TrimSpace(action.ActionType)
		if actionRegistry.actionCallAllowedInApprovalNotification(actionType) {
			continue
		}
		if actionType == "" {
			actionType = "<empty>"
		}
		return approvalNotificationUnsafeFlowError(flowAPIName, "ActionCall", action.Name, fmt.Sprintf("actionType %q is outside notification scope", actionType))
	}
	return nil
}

func approvalNotificationUnsafeFlowError(flowAPIName, elementType, elementName, reason string) error {
	elementName = strings.TrimSpace(elementName)
	if elementName == "" {
		elementName = "<unnamed>"
	}
	return apperrors.NewValidationError("Flow", fmt.Sprintf("approval notification Flow %q is not notification-safe: %s element %q %s", flowAPIName, elementType, elementName, reason))
}

func (s *ApprovalService) executeApprovalFlowNotification(ctx context.Context, action models.SObject, pending pendingNotifications, user *models.UserSession) *ProcessActionFailure {
	approval := pending.approval
	actionName, _ := action["Name"].(string)
	flowAPIName := strings.TrimSpace(approvalStringValue(action[constants.FieldProcessAction_APIName]))
	if flowAPIName == "" {
		return &ProcessActionFailure{
			ActionName: actionName,
			ActionType: constants.ProcessActionTypeFlow,
			Reason:     "ApiName is required for approval Flow notification binding",
		}
	}
	if s.flowExecutor == nil {
		return &ProcessActionFailure{
			ActionName: actionName,
			ActionType: constants.ProcessActionTypeFlow,
			Reason:     "FlowExecutor is not configured",
		}
	}

	flowCtx := approvalFlowNotificationExecutionContext(ctx)
	flowVersion, err := s.flowExecutor.approvalNotificationFlowVersion(flowCtx, flowAPIName, user)
	if err != nil {
		s.logger.Error(fmt.Sprintf("ProcessAction Flow '%s' failed to inspect Flow %s", actionName, flowAPIName), err)
		return &ProcessActionFailure{
			ActionName: actionName,
			ActionType: constants.ProcessActionTypeFlow,
			Reason:     err.Error(),
		}
	}
	var metadata *models.FlowMetadata
	if flowVersion != nil {
		metadata = &flowVersion.Metadata
	}
	if err := s.flowExecutor.validateApprovalNotificationFlowMetadata(flowAPIName, metadata); err != nil {
		s.logger.Error(fmt.Sprintf("ProcessAction Flow '%s' rejected unsafe approval notification Flow %s", actionName, flowAPIName), err)
		return &ProcessActionFailure{
			ActionName: actionName,
			ActionType: constants.ProcessActionTypeFlow,
			Reason:     err.Error(),
		}
	}
	// #4397 R1 ruling 3: an unreadable snapshot is a visible delivery failure,
	// not an empty $Record. A Flow launched with a fabricated empty record
	// reports Finished while every $Record merge token and $Record-guarded
	// Decision silently degrades — a notification that was never sent becomes
	// indistinguishable from one that was never configured. Fail this delivery
	// (the approval itself already committed; Tier-2 isolation is unchanged).
	if pending.flowTargetSnapshot == nil {
		reason := "notification target snapshot was not captured; Flow not launched"
		if err := pending.flowTargetSnapshotErr; err != nil {
			// Reason renders in the acting user's warning banner. The v63
			// contract limits successful-response actionFailures to user-safe
			// non-fatal causes; captureApprovalFlowNotificationTargetSnapshot
			// is the single point that enforces it (NotFound/Validation are
			// stored here, everything else aborted the transaction), so the
			// stored cause renders verbatim.
			reason = fmt.Sprintf("notification target snapshot unreadable, Flow not launched: %v", err)
		}
		s.logger.Error(fmt.Sprintf("ProcessAction Flow '%s' failed closed for %s %s: %s", actionName, pending.objectApiName, pending.targetID, reason), pending.flowTargetSnapshotErr)
		return &ProcessActionFailure{
			ActionName:   actionName,
			ActionType:   constants.ProcessActionTypeFlow,
			TargetObject: pending.objectApiName,
			TargetID:     pending.targetID,
			Reason:       reason,
		}
	}
	targetRecord := cloneApprovalNotificationRecord(pending.flowTargetSnapshot)
	if approval.TargetRecordID != "" {
		if _, ok := targetRecord[constants.FieldID]; !ok {
			targetRecord[constants.FieldID] = approval.TargetRecordID
		}
	}
	if approval.TargetObjectAPIName != "" && models.RecordObjectType(targetRecord) == "" {
		models.StampObjectType(targetRecord, approval.TargetObjectAPIName)
	}
	inputs := approval.flowInputs(targetRecord)
	result, err := s.flowExecutor.executeApprovalNotificationFlowVersion(flowCtx, flowAPIName, flowVersion, inputs, user)
	if err != nil {
		s.logger.Error(fmt.Sprintf("ProcessAction Flow '%s' failed to launch Flow %s", actionName, flowAPIName), err)
		return &ProcessActionFailure{
			ActionName: actionName,
			ActionType: constants.ProcessActionTypeFlow,
			Reason:     err.Error(),
		}
	}
	if result != nil && !result.IsSuccess {
		reason := approvalFlowExecutionFailureReason(result)
		s.logger.Error(fmt.Sprintf("ProcessAction Flow '%s' Flow %s returned failure: %s", actionName, flowAPIName, reason), nil)
		return &ProcessActionFailure{
			ActionName: actionName,
			ActionType: constants.ProcessActionTypeFlow,
			Reason:     reason,
		}
	}
	s.logger.Info(fmt.Sprintf("ProcessAction Flow '%s': launched approval notification Flow %s", actionName, flowAPIName))
	return nil
}

func approvalFlowExecutionFailureReason(result *models.FlowExecutionResult) string {
	if result == nil {
		return "Flow execution returned no result"
	}
	if len(result.Errors) == 0 {
		return "Flow execution failed"
	}
	parts := make([]string, 0, len(result.Errors))
	for _, flowErr := range result.Errors {
		message := strings.TrimSpace(flowErr.Message)
		status := strings.TrimSpace(flowErr.StatusCode)
		switch {
		case status != "" && message != "":
			parts = append(parts, fmt.Sprintf("%s: %s", status, message))
		case message != "":
			parts = append(parts, message)
		case status != "":
			parts = append(parts, status)
		}
	}
	if len(parts) == 0 {
		return "Flow execution failed"
	}
	return strings.Join(parts, "; ")
}
