package automation

import (
	"context"
	"fmt"
	"strings"
	"time"

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

const (
	flowSendNotificationActionType     = flowactions.ActionTypeSendNotification
	flowSendNotificationRecipientParam = "recipientUserId"
	flowSendNotificationTitleParam     = "title"
	flowSendNotificationBodyParam      = "body"
	flowSendNotificationLinkParam      = "link"
	flowSendNotificationTypeParam      = "notificationType"
	flowSendNotificationDefaultType    = "flow"
	flowSendNotificationTitleMaxRunes  = 255
)

// sendNotificationRequest is the pure-builder output of
// buildSendNotificationRequest: the fully-resolved, validated set of
// sendNotification actionCall parameters ready for delivery.
type sendNotificationRequest struct {
	RecipientUserID  string
	Title            string
	Body             string
	Link             string
	NotificationType string
}

// executeSendNotificationAction inserts an in-app Notification row for a
// Flow-resolved recipient user. Structure mirrors executeSendEmailAction: a
// fault() closure for authoring errors, a pure request builder, an execute
// step, and the connector target. Like emailAlert it is fire-and-forget: it
// declares no output parameters, so a flow cannot branch on delivery outcome
// (a Tier-2 skip stays observable via the audit-fault box below).
//
// Two-tier error semantics (deliberate deviation from sibling actionCall
// handlers, documented per NX#930):
//
//   - Tier 1 (authoring errors) — a title param that is not declared at all
//     in inputParameters, or a non-string param value — routes to the
//     FaultConnector like every other actionCall. A broken flow definition
//     must not ship silently.
//
//   - Tier 2 (runtime data / delivery failures) — an unresolved/blank
//     recipientUserId, a declared title that resolves blank at runtime,
//     recipient not found, recipient inactive, a recipient-lookup infra
//     error, or a notification insert error — is logged and swallowed: a
//     notification must never crash its host Flow or block an approval
//     action. recipientUserId is deliberately Tier-2 rather than Tier-1:
//     deepResolveFlowResource collapses "param omitted" and "an elementReference
//     resolved to empty at runtime" (e.g. $Record.OwnerId blank on the
//     stamped record the approval-notification sandbox reads) into the same
//     empty string, and the latter is exactly the "invalid recipient" case
//     the issue's fail-safe rule requires to log-and-continue rather than
//     fault the flow / surface a ProcessActionFailure. A declared-but-blank
//     title gets the same treatment: the declaration proves the author wired
//     the param, so a blank can only be runtime data shape (a blank field or
//     formula on this record), which must not surface a ProcessActionFailure
//     in the approval-notification sandbox. Tier 2 skips never consume a
//     wired FaultConnector (the normal Connector target is always taken).
//     Tier 2 skips still stamp the ctx audit-fault box via recordAuditFault
//     so the failure is never invisible in _System_FlowAuditLog even though
//     the Flow continues.
func (e *FlowExecutor) executeSendNotificationAction(ctx context.Context, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
	fault := func(err error) (string, error) {
		if action.FaultConnector != nil {
			return e.routeFault(ctx, interview, action.FaultConnector.TargetReference, err)
		}
		return "", err
	}
	if e.persistence == nil {
		return fault(apperrors.NewInternalError("sendNotification action unavailable: PersistenceProvider not wired into FlowExecutor", nil))
	}

	req, err := e.buildSendNotificationRequest(ctx, interview, action, user)
	if err != nil {
		return fault(err)
	}

	skip := func(reason string, cause error) (string, error) {
		if cause == nil {
			// GetRaw's not-found contract is (nil, nil) — synthesize a cause so
			// the audit-fault box actually records something; recordAuditFault
			// no-ops on a nil error and the skip would otherwise be invisible
			// in _System_FlowAuditLog.
			cause = fmt.Errorf("sendNotification: %s", reason)
		}
		recordAuditFault(ctx, cause)
		if action.Connector != nil {
			return action.Connector.TargetReference, nil
		}
		return "", nil
	}

	if req.RecipientUserID == "" {
		// Blank/unresolved recipientUserId is a Tier-2 fail-safe skip, not a
		// Tier-1 fault: it is indistinguishable here from an elementReference
		// (e.g. $Record.OwnerId) that resolved to empty at runtime, which is
		// the issue's canonical "invalid recipient" case.
		e.logger.Warn(fmt.Sprintf("sendNotification actionCall %q: recipientUserId not resolved, skipping", action.Name))
		return skip(fmt.Sprintf("%s not resolved", flowSendNotificationRecipientParam), nil)
	}

	if req.Title == "" {
		// A declared title that resolved blank/whitespace at runtime is a
		// Tier-2 fail-safe skip, symmetric with the blank-recipient case: the
		// declaration proves the wiring exists, so the blank is data shape on
		// this record, not an authoring bug. An undeclared title param is
		// still a Tier-1 fault raised by buildSendNotificationRequest.
		e.logger.Warn(fmt.Sprintf("sendNotification actionCall %q: title resolved empty, skipping", action.Name))
		return skip(fmt.Sprintf("%s not resolved", flowSendNotificationTitleParam), nil)
	}

	recipient, err := e.persistence.GetRaw(ctx, constants.TableUser, req.RecipientUserID)
	if err != nil {
		e.logger.Error(fmt.Sprintf("sendNotification actionCall %q: recipient lookup failed for %s", action.Name, req.RecipientUserID), err)
		return skip(fmt.Sprintf("recipient lookup failed: %s", err.Error()), err)
	}
	if recipient == nil {
		e.logger.Warn(fmt.Sprintf("sendNotification actionCall %q: recipient %s not found, skipping", action.Name, req.RecipientUserID))
		return skip(fmt.Sprintf("recipient %s not found", req.RecipientUserID), nil)
	}
	// approvalBoolValue tolerates the shapes a TiDB boolean surfaces as
	// (bool / int / int64 / float64 / "1" / "true"); a strict == true would
	// read an active user as inactive on non-bool read paths.
	if !approvalBoolValue(recipient[constants.FieldUser_IsActive]) {
		e.logger.Warn(fmt.Sprintf("sendNotification actionCall %q: recipient %s is inactive, skipping", action.Name, req.RecipientUserID))
		return skip(fmt.Sprintf("recipient %s is inactive", req.RecipientUserID), nil)
	}

	notificationRowID := utils.GenerateID()
	notif := models.Record{
		constants.FieldNotification_ID:               notificationRowID,
		constants.FieldNotification_RecipientID:      req.RecipientUserID,
		constants.FieldNotification_Title:            req.Title,
		constants.FieldNotification_Body:             req.Body,
		constants.FieldNotification_Link:             req.Link,
		constants.FieldNotification_NotificationType: req.NotificationType,
		constants.FieldNotification_IsRead:           false,
		constants.FieldNotification_LastModifiedDate: time.Time{}.Format(time.RFC3339Nano),
		constants.FieldNotification_CreatedDate:      time.Now().Format(time.RFC3339Nano),
	}

	// Notification is a system_core table without FLS grants for ordinary
	// users; elevate ONLY this Insert call to SystemContext (same discipline
	// as approvalNotificationFlowVersion in approval_notifications.go). The
	// elevated ctx never escapes this call — the recipient lookup above and
	// the rest of Flow execution continue on the caller's un-elevated ctx.
	// The row is also a system-owned side effect: audit observers still see
	// its record events, but record-triggered automation must not dispatch
	// for it — otherwise a tenant Flow record-triggered on Notification
	// would run under this SystemContext (an approval-notification-sandbox
	// escape) and could recurse back into sendNotification. Suppression is
	// targeted by row id, same discipline as exclusiveSiblingResetFinalRecord;
	// the insert pipeline preserves a caller-provided Id, so the tuple
	// matches the emitted event payload. Public streaming exposure is handled
	// separately: the CRM distribution's record-topic filter drops Notification
	// rows before the CometD broadcast (streaming_topics.go).
	writeCtx := constants.WithSecurityContext(ctx, constants.SystemContext, "FlowExecutor: sendNotification system-table write")
	writeCtx = context.WithValue(writeCtx, constants.ContextKeySuppressRecordTriggeredAutomation, constants.TableNotification+"/"+notificationRowID)
	if _, err := e.persistence.Insert(writeCtx, constants.TableNotification, notif, user); err != nil {
		e.logger.Error(fmt.Sprintf("sendNotification actionCall %q: notification insert failed for recipient %s", action.Name, req.RecipientUserID), err)
		return skip(fmt.Sprintf("notification insert failed: %s", err.Error()), err)
	}

	if action.Connector != nil {
		return action.Connector.TargetReference, nil
	}
	return "", nil
}

// buildSendNotificationRequest is a pure param extractor: it resolves and
// validates every sendNotification input parameter without performing any
// I/O, so it is unit-testable independent of a live executor beyond
// deepResolveFlowResource's own inputs (interview.ContextData, $Record, etc.).
//
// title is Tier-1 required only in the declaration sense: an actionCall whose
// inputParameters carry no title param at all is an authoring bug (fault
// path). A DECLARED title that resolves to blank at runtime is deliberately
// NOT an error here — like recipientUserId, it is a Tier-2 fail-safe
// condition handled by the caller (executeSendNotificationAction), because a
// blank resolution of wired-up authoring can only be runtime data shape and
// must not surface a ProcessActionFailure in the approval-notification
// sandbox (see the Tier-2 doc comment above). body/link/notificationType are
// optional; notificationType defaults to "flow" when absent or blank.
func (e *FlowExecutor) buildSendNotificationRequest(ctx context.Context, interview *models.FlowInterview, action *models.FlowActionCall, user *models.UserSession) (sendNotificationRequest, error) {
	req := sendNotificationRequest{NotificationType: flowSendNotificationDefaultType}
	titleDeclared := false

	for _, param := range action.InputParameters {
		name := strings.TrimSpace(param.Name)
		if !strings.EqualFold(name, flowSendNotificationRecipientParam) &&
			!strings.EqualFold(name, flowSendNotificationTitleParam) &&
			!strings.EqualFold(name, flowSendNotificationBodyParam) &&
			!strings.EqualFold(name, flowSendNotificationLinkParam) &&
			!strings.EqualFold(name, flowSendNotificationTypeParam) {
			continue
		}
		resolved, err := e.deepResolveFlowResource(ctx, interview, user, param.Value)
		if err != nil {
			return req, err
		}
		resolved = unwrapCanonicalSendEmailTypedValue(resolved)
		switch {
		case strings.EqualFold(name, flowSendNotificationRecipientParam):
			value, err := sendNotificationStringParam(flowSendNotificationRecipientParam, resolved)
			if err != nil {
				return req, err
			}
			req.RecipientUserID = value
		case strings.EqualFold(name, flowSendNotificationTitleParam):
			titleDeclared = true
			value, err := sendNotificationStringParam(flowSendNotificationTitleParam, resolved)
			if err != nil {
				return req, err
			}
			req.Title = value
		case strings.EqualFold(name, flowSendNotificationBodyParam):
			value, err := sendNotificationStringParam(flowSendNotificationBodyParam, resolved)
			if err != nil {
				return req, err
			}
			req.Body = value
		case strings.EqualFold(name, flowSendNotificationLinkParam):
			value, err := sendNotificationStringParam(flowSendNotificationLinkParam, resolved)
			if err != nil {
				return req, err
			}
			req.Link = value
		case strings.EqualFold(name, flowSendNotificationTypeParam):
			value, err := sendNotificationStringParam(flowSendNotificationTypeParam, resolved)
			if err != nil {
				return req, err
			}
			if value != "" {
				req.NotificationType = value
			}
		}
		// Unknown parameter names are ignored (sendEmail parity).
	}

	// bytes >= runes, so a title within the byte cap can never exceed it in
	// runes; only the rare oversized title pays the []rune conversion.
	if len(req.Title) > flowSendNotificationTitleMaxRunes {
		if runes := []rune(req.Title); len(runes) > flowSendNotificationTitleMaxRunes {
			e.logger.Warn(fmt.Sprintf("sendNotification actionCall %q: title truncated from %d to %d runes", action.Name, len(runes), flowSendNotificationTitleMaxRunes))
			req.Title = string(runes[:flowSendNotificationTitleMaxRunes])
		}
	}

	if !titleDeclared {
		return req, apperrors.NewValidationError(flowSendNotificationTitleParam, fmt.Sprintf("%s is required", flowSendNotificationTitleParam))
	}
	return req, nil
}

// sendNotificationStringParam unwraps a resolved sendNotification param value
// into a trimmed string. A nil value (resolved to nothing) is not an error
// here — the only Tier-1 required-ness is title's declaration check after the
// param loop, and blank-after-resolution is a Tier-2 concern of the caller,
// per the boundary documented on buildSendNotificationRequest.
func sendNotificationStringParam(fieldName string, value interface{}) (string, error) {
	if value == nil {
		return "", nil
	}
	text, ok := value.(string)
	if !ok {
		return "", apperrors.NewValidationError(fieldName, fmt.Sprintf("%s must be a string", fieldName))
	}
	return strings.TrimSpace(text), nil
}
