package automation

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"

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

// FlowRecordCreatePreview is one captured RecordCreate intent from a preview
// run: the field map a real run would prepare for Object before the physical
// insert. Field values are resolved through the same recordCreate defaults and
// no-DML persistence prepare path as live execution, including schema defaults,
// system fields, before-create flows, validation rules, and uniqueness checks.
type FlowRecordCreatePreview struct {
	Object                    string
	PreviewID                 string `json:"-"`
	Fields                    models.SObject
	SiblingMaxDefaultedFields []string
}

// FlowRecordCreatePreviewResult is the return shape of PreviewRecordCreateFields.
// IsSuccess matches the live-execution return contract: true when the flow ran
// to completion (or skipped via entry conditions), false when a flow-level
// error or a recordUpdate / recordDelete attempt aborted the preview.
type FlowRecordCreatePreviewResult struct {
	ActionName string
	IsSuccess  bool
	Creates    []FlowRecordCreatePreview
	Errors     []models.FlowExecutionError
}

type flowPreviewOptions struct{}

type flowPreviewOptionsContextKey struct{}

func withFlowPreviewOptions(ctx context.Context, opts flowPreviewOptions) context.Context {
	return context.WithValue(ctx, flowPreviewOptionsContextKey{}, opts)
}

func withoutFlowPreviewOptions(ctx context.Context) context.Context {
	return withFlowPreviewOptions(ctx, flowPreviewOptions{})
}

// errPreviewRecordUpdateAttempt and errPreviewRecordDeleteAttempt surface when
// the previewed flow tries to mutate other records — preview is strictly
// no-DML and these elements have no preview contract today.
//
// errPreviewActionCallAttempt surfaces when a non-"flow" actionCall is hit
// without a registry-owned preview policy that explicitly allows it.
// PreviewBeforeInsert can also reach a live FlowExecutor through the event
// bus, so executor-side gates must read preview mode from ctx too. Letting
// these actions execute would mutate real records, send real email, and run
// real webhooks from a preview path — breaking the no-DML / no-side-effect
// preview contract. Subflow invocations (actionType="flow", or top-level
// Subflow elements) recurse through the preview-marked execution path and
// remain allowed only when before-save validation proves them safe.
var (
	errPreviewRecordUpdateAttempt  = errors.New("flow preview: recordUpdate is not allowed in a no-DML preview run")
	errPreviewBeforeSaveCreate     = errors.New("flow preview: recordCreate is not allowed inside before-save insert preparation")
	errPreviewRecordDeleteAttempt  = errors.New("flow preview: recordDelete is not allowed in a no-DML preview run")
	errPreviewLockAttempt          = errors.New("flow preview: GetLock is not allowed in a no-DML preview run")
	errPreviewActionCallAttempt    = errors.New("flow preview: actionCall is not allowed in a no-DML preview run (subflow actionType=\"flow\" is allowed)")
	errPreviewWaitAttempt          = errors.New("flow preview: Wait is not allowed in a no-DML preview run (Wait pause persists FlowInterview rows)")
	errPreviewEngineInternalInsert = errors.New("flow preview: only RecordCreate-element Inserts are captured; engine-internal inserts (e.g. FlowInterview from Wait) are not allowed")
	// errPreviewRecordCreateAttempt fires when a RecordCreate element is
	// reached on a FlowExecutor whose persistence is NOT the preview wrapper
	// while ctx is still marked WithPreviewMode. This happens when
	// previewPersistence.PreviewBeforeUpdate delegates into the live
	// persistence service, which publishes RecordBeforeUpdate and reruns
	// matching before-save flows on the shared trigger-listener executor.
	// That executor is the live, unwrapped one — if its $Record-driven
	// branch hits an executeRecordCreate, the in-context Insert would write
	// a real row from a no-DML preview path. Reject loudly instead.
	errPreviewRecordCreateAttempt = errors.New("flow preview: recordCreate is not allowed in a no-DML preview run (triggered before-save flow attempted Insert under preview ctx)")
	// errPreviewOutputFlowRecordCreateAttempt fires when an output-only
	// preview producer (PreviewFlowOutputs) encounters a RecordCreate
	// element. Unlike previewPersistence which captures RecordCreate as
	// success for create-form previews, output-only producers have no
	// side-effect contract and must reject create attempts outright.
	errPreviewOutputFlowRecordCreateAttempt = errors.New("flow preview: recordCreate is not allowed in an output-only preview producer (use PreviewRecordCreateFields if create-form capture is the intent)")
)

// recordCreateInsertContextKey marks a persistence.Insert call as originating
// from executeRecordCreate's user-facing path. previewPersistence.Insert
// checks this marker so engine-internal Insert calls (FlowInterview write
// from pauseForWait, future runtime-internal persistence) are rejected
// instead of being silently captured as user-facing RecordCreate intents.
//
// Without this marker, a future Wait + transient-promote path would write a
// FlowInterview row through previewPersistence and the captured Creates[]
// slice would surface that internal row to callers (PR #273 round 7,
// IANTHEREAL P1 Blocker 3 — second layer of defense).
type recordCreateInsertContextKey struct{}

// withRecordCreateInsertMarker tags ctx so the immediately-following call to
// PersistenceProvider.Insert is recognised by previewPersistence.Insert as
// the user-facing RecordCreate path. Used only in executeRecordCreate.
func withRecordCreateInsertMarker(ctx context.Context) context.Context {
	return context.WithValue(ctx, recordCreateInsertContextKey{}, true)
}

// isRecordCreateInsert reports whether ctx was tagged via
// withRecordCreateInsertMarker. previewPersistence.Insert calls this to
// decide whether to capture the call (true) or reject it (false).
func isRecordCreateInsert(ctx context.Context) bool {
	v, _ := ctx.Value(recordCreateInsertContextKey{}).(bool)
	return v
}

// PreviewRecordCreateFields runs the named flow exactly like ExecuteFlow,
// except every persistence write is intercepted: RecordCreate intents are
// captured into the result instead of being inserted, and recordUpdate /
// recordDelete elements abort the preview with a clear error. The preview
// uses the same metadata, formula engine, and ResolveFlowResource semantics as
// live execution, so create-form panels see byte-identical defaulted fields
// to what a subsequent real run would write.
//
// Permissions: ExecuteFlow's authorizeProtectedStandardFlow gate runs
// unchanged on the preview path, so a caller without permission to invoke a
// protected/standard flow gets the same FLOW_EXECUTION_ERROR denial they
// would on a real run.
//
// Errors: a recordUpdate / recordDelete attempt — or any other flow-runtime
// fault — surfaces as Errors[] on the returned result with IsSuccess=false.
// Callers should treat this exactly like a failed ExecuteFlow.
func (e *FlowExecutor) PreviewRecordCreateFields(ctx context.Context, flowApiName string, inputs map[string]interface{}, user *models.UserSession) (*FlowRecordCreatePreviewResult, error) {
	return e.previewRecordCreateFields(ctx, flowApiName, inputs, user, flowPreviewOptions{})
}

// PreviewStandardActionRecordCreateFields is the standard-action create preview
// variant. It keeps the same no-DML create capture engine as
// PreviewRecordCreateFields. Protected update/finalizer steps remain
// fail-closed until update preview has full post-before-save validation parity.
func (e *FlowExecutor) PreviewStandardActionRecordCreateFields(ctx context.Context, flowApiName string, inputs map[string]interface{}, user *models.UserSession) (*FlowRecordCreatePreviewResult, error) {
	return e.previewRecordCreateFields(ctx, flowApiName, inputs, user, flowPreviewOptions{})
}

// PreviewPreparedStandardActionRecordCreateFields binds the no-DML preview to
// the exact Flow/subflow closure selected from one immutable Catalog revision.
// The existing preview engine remains the execution owner; this adapter only
// prevents it from reopening live active-version metadata by name.
func (e *FlowExecutor) PreviewPreparedStandardActionRecordCreateFields(
	ctx context.Context,
	flowAPIName string,
	versions map[string]*models.FlowVersion,
	inputs map[string]interface{},
	user *models.UserSession,
) (*FlowRecordCreatePreviewResult, error) {
	prepared, err := withPreparedFlowClosure(ctx, versions)
	if err != nil {
		return nil, err
	}
	prepared = withPreparedFlowRootAuthorizationRequired(prepared)
	return e.PreviewStandardActionRecordCreateFields(
		prepared, flowAPIName, inputs, user,
	)
}

func (e *FlowExecutor) previewRecordCreateFields(ctx context.Context, flowApiName string, inputs map[string]interface{}, user *models.UserSession, opts flowPreviewOptions) (*FlowRecordCreatePreviewResult, error) {
	if e == nil {
		return nil, apperrors.NewInternalError("flow preview: nil FlowExecutor", nil)
	}
	if e.persistence == nil {
		return nil, apperrors.NewInternalError("flow preview: FlowExecutor has no persistence wired", nil)
	}
	if e.interviews == nil {
		return nil, apperrors.NewInternalError("flow preview: FlowExecutor has no FlowInterviewService wired", nil)
	}

	captured := []FlowRecordCreatePreview{}
	pp := &previewPersistence{
		inner:    e.persistence,
		captured: &captured,
	}

	// Build a sibling executor sharing the schema / expression / metadata
	// stack, including the pointer-owned lookup coordinator, but with the two
	// persistence-touching services swapped to no-DML wrappers.
	prev := *e
	prev.persistence = pp
	prev.interviews = NewFlowInterviewService(pp, e.query)

	// Skip FlowInterview row writes entirely (executeFlowVersion checks
	// shouldPersistFlowInterview before calling CreateInterview), so the
	// preview path has zero DB writes even before the wrapped persistence
	// would have a chance to no-op them.
	runCtx := WithTransientFlowInterviews(ctx)

	// Tag the ctx with preview mode so all side-effect channels that
	// previewPersistence cannot intercept on its own self-suppress:
	//   - emitFlowAudit drops audit-row writes to _System_FlowAuditLog
	//   - executeRecordUpdate's in-place $Record path early-rejects
	//   - executeWait early-rejects (pause would persist FlowInterview)
	// These three call sites read the marker via isPreviewMode(ctx).
	runCtx = WithPreviewMode(runCtx)
	runCtx = autonumber.WithPreviewCounterState(runCtx)
	runCtx = constants.WithPreviewSyntheticRecordRegistry(runCtx)
	runCtx = constants.WithPreviewSiblingMaxDefaultedFieldRegistry(runCtx)
	runCtx = withFlowPreviewOptions(runCtx, opts)

	runResult, runErr := prev.ExecuteFlow(runCtx, flowApiName, inputs, user)
	if runErr != nil {
		return nil, runErr
	}
	out := &FlowRecordCreatePreviewResult{
		Creates: captured,
	}
	if runResult != nil {
		out.ActionName = runResult.ActionName
		out.IsSuccess = runResult.IsSuccess
		out.Errors = runResult.Errors
	}
	return out, nil
}

// previewPersistence implements PersistenceProvider with no DML: RecordCreate
// intents are captured, live RecordUpdate / RecordDelete writes are rejected,
// and read/prepare-side calls pass through to the inner provider so real
// metadata-driven resolvers and no-DML save preparation share live runtime
// semantics without committing rows.
type previewPersistence struct {
	inner    PersistenceProvider
	captured *[]FlowRecordCreatePreview
}

// isFlowPreviewPersistence marks the no-DML persistence capability used by
// registry-owned preview action policies. The unexported marker is promoted by
// stricter wrappers such as outputOnlyPreviewPersistence without treating live
// PersistenceProvider implementations as preview-safe.
func (*previewPersistence) isFlowPreviewPersistence() {}

// Insert captures the intended write into the running result and returns a
// synthetic preview row so downstream AssignRecordIdToReference /
// StoreOutputAutomatically wiring keeps working without any DB write. The
// synthetic Id is request-local wiring, not proof that a durable parent row
// exists for generic records validators. The captured field map is a copy:
// downstream flow steps that mutate ContextData cannot retroactively edit a
// previously captured intent.
//
// Engine-internal Inserts that go through the same PersistenceProvider
// surface (e.g. pauseForWait writing a FlowInterview row) are rejected: they
// are NOT user RecordCreate intents and must not appear in Creates[]. The
// caller (executeRecordCreate) tags the ctx via withRecordCreateInsertMarker
// to opt into the capture path; everything else fails closed.
func (p *previewPersistence) Insert(ctx context.Context, tableName string, record models.SObject, user *models.UserSession) (models.SObject, error) {
	if !isRecordCreateInsert(ctx) {
		return nil, errPreviewEngineInternalInsert
	}
	previewID, err := previewRecordId()
	if err != nil {
		return nil, err
	}
	previewCtx := constants.WithPreviewSiblingMaxDefaultedFieldToken(ctx, previewID)
	prepared, err := p.inner.PreviewBeforeInsert(previewCtx, tableName, record, user)
	if err != nil {
		return nil, err
	}
	siblingMaxDefaultedFields := constants.ConsumePreviewSiblingMaxDefaultedFields(previewCtx, previewID)
	fields := clonePreviewSObject(prepared)
	delete(fields, constants.FieldID)
	*p.captured = append(*p.captured, FlowRecordCreatePreview{
		Object:                    tableName,
		PreviewID:                 previewID,
		Fields:                    fields,
		SiblingMaxDefaultedFields: siblingMaxDefaultedFields,
	})
	out := clonePreviewSObject(prepared)
	out[constants.FieldID] = previewID
	constants.RegisterPreviewSyntheticRecord(previewCtx, previewID, tableName, out)
	return out, nil
}

func (p *previewPersistence) PreviewBeforeInsert(ctx context.Context, tableName string, record models.SObject, user *models.UserSession) (models.SObject, error) {
	return nil, errPreviewEngineInternalInsert
}

func (p *previewPersistence) PrelockUpdateParents(ctx context.Context, tableName string, id string, updates models.SObject, user *models.UserSession) error {
	return errPreviewRecordUpdateAttempt
}

func clonePreviewSObject(in models.SObject) models.SObject {
	if in == nil {
		return models.SObject{}
	}
	out := make(models.SObject, len(in))
	for k, v := range in {
		out[k] = v
	}
	return out
}

func (p *previewPersistence) Update(ctx context.Context, tableName string, id string, updates models.SObject, user *models.UserSession) error {
	return errPreviewRecordUpdateAttempt
}

func (p *previewPersistence) PreviewBeforeUpdate(ctx context.Context, tableName string, id string, updates models.SObject, internalStamps models.SObject, user *models.UserSession) (*models.PreparedRecordUpdate, error) {
	if p.inner == nil {
		return nil, errPreviewRecordUpdateAttempt
	}
	return p.inner.PreviewBeforeUpdate(ctx, tableName, id, updates, internalStamps, user)
}

func (p *previewPersistence) CommitPreparedUpdate(ctx context.Context, tableName string, id string, prepared *models.PreparedRecordUpdate, extraUpdates models.SObject, user *models.UserSession) error {
	return errPreviewRecordUpdateAttempt
}

func (p *previewPersistence) Delete(ctx context.Context, tableName string, id string, user *models.UserSession) error {
	return errPreviewRecordDeleteAttempt
}

func (p *previewPersistence) GetRaw(ctx context.Context, objectName string, id string) (models.SObject, error) {
	if record, ok := constants.PreviewSyntheticRecordForObject(ctx, objectName, id); ok {
		return models.SObject(record.Fields), nil
	}
	return p.inner.GetRaw(ctx, objectName, id)
}

func (p *previewPersistence) GetRecordType(ctx context.Context, id string) (string, error) {
	return p.inner.GetRecordType(ctx, id)
}

// GetLock requires a real transaction, which the preview path does not own;
// rejecting it loud beats silently returning nil and corrupting the
// caller's expectation of "I just locked this row".
func (p *previewPersistence) GetLock(ctx context.Context, tableName string, id string) (models.SObject, error) {
	return nil, errPreviewLockAttempt
}

// RunInTransaction calls fn directly: preview has no real DML, so a real DB
// transaction would be a no-op wrapping zero writes. Calling fn inline
// preserves the executor's transactional control-flow expectations
// (atomic-flow boundary marker is still set by runPossiblyAtomic via
// withAtomicFlowBoundary on top of the ctx we return) without claiming a
// connection or savepoint.
func (p *previewPersistence) RunInTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}

func (p *previewPersistence) IsTransactionActive(context.Context) bool {
	return false
}

func (p *previewPersistence) RunInPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}

// DetachTx is a no-op: preview never carries a real tx.
func (p *previewPersistence) DetachTx(ctx context.Context) context.Context {
	return ctx
}

// RecomputeActiveFormulaProjection is a no-op in preview mode: active-formula
// reprojection after in-flow RecordUpdate requires a committed row in the view,
// which preview never writes. Return (nil, nil) so the caller skips the overlay
// gracefully rather than fail-closing a preview run.
func (p *previewPersistence) RecomputeActiveFormulaProjection(_ context.Context, _ *models.UserSession, _, _ string, _ []string) (models.SObject, error) {
	return nil, nil
}

// previewRecordId returns a synthetic Id for a captured RecordCreate. The
// "preview-" prefix makes it impossible for downstream code (including
// future REST contract tests) to confuse a preview Id with a real one.
//
// The trailing token is 16 bytes of crypto/rand hex (128 bits) so callers
// cannot guess or hand-craft a "preview-<object>-<n>"-style literal that
// would match a same-run synthetic in the request-local registry. The
// registry's provenance contract (Codex round-4 finding on
// nonpoly_lookup_validator.go: synthetic-ID shortcut trusted registry
// membership alone) only holds when the only way to land an ID in the
// registry is to mint it here.
//
// crypto/rand.Read on Linux/Darwin is backed by getrandom(2) / /dev/urandom
// and only returns an error in degenerate cases. On failure, preview fails
// closed instead of falling back to weaker entropy or panicking the request.
func previewRecordId() (string, error) {
	var buf [16]byte
	if _, err := rand.Read(buf[:]); err != nil {
		return "", apperrors.NewInternalError("flow preview: failed to mint preview id", err)
	}
	return "preview-" + hex.EncodeToString(buf[:]), nil
}

// FlowPreviewOutputsResult is the return shape of PreviewFlowOutputs.
// Unlike FlowRecordCreatePreviewResult which captures RecordCreate intents,
// this exposes the Flow's OutputValues directly for output-only preview
// producers (e.g. campaign attribution before lead convert).
type FlowPreviewOutputsResult struct {
	ActionName   string
	IsSuccess    bool
	OutputValues map[string]interface{}
	Errors       []models.FlowExecutionError
}

// outputOnlyPreviewPersistence is a no-DML PersistenceProvider wrapper for
// output-only preview producers. Unlike previewPersistence which captures
// RecordCreate as success for create-form previews, this wrapper rejects
// RecordCreate outright because output-only producers have no side-effect
// contract. All other methods delegate to the embedded previewPersistence.
type outputOnlyPreviewPersistence struct {
	*previewPersistence
}

func (p *outputOnlyPreviewPersistence) Insert(ctx context.Context, tableName string, record models.SObject, user *models.UserSession) (models.SObject, error) {
	return nil, errPreviewOutputFlowRecordCreateAttempt
}

// PreviewFlowOutputs runs the named flow exactly like ExecuteFlow, except
// every persistence write is intercepted and rejected (including RecordCreate,
// unlike PreviewRecordCreateFields which captures it). The Flow's OutputValues
// are returned directly so callers can observe computed values (e.g. campaign
// attribution) without any DML side effects.
//
// Permissions: ExecuteFlow's authorizeProtectedStandardFlow gate runs
// unchanged on the preview path, so a caller without permission to invoke a
// protected/standard flow gets the same FLOW_EXECUTION_ERROR denial.
func (e *FlowExecutor) PreviewFlowOutputs(ctx context.Context, flowApiName string, inputs map[string]interface{}, user *models.UserSession) (*FlowPreviewOutputsResult, error) {
	if e == nil {
		return nil, apperrors.NewInternalError("flow preview: nil FlowExecutor", nil)
	}
	if e.persistence == nil {
		return nil, apperrors.NewInternalError("flow preview: FlowExecutor has no persistence wired", nil)
	}
	if e.interviews == nil {
		return nil, apperrors.NewInternalError("flow preview: FlowExecutor has no FlowInterviewService wired", nil)
	}

	pp := &previewPersistence{
		inner:    e.persistence,
		captured: &[]FlowRecordCreatePreview{},
	}
	prevPersistence := &outputOnlyPreviewPersistence{
		previewPersistence: pp,
	}

	prev := *e
	prev.persistence = prevPersistence
	prev.interviews = NewFlowInterviewService(prevPersistence, e.query)

	runCtx := WithTransientFlowInterviews(ctx)
	runCtx = WithPreviewMode(runCtx)

	runResult, runErr := prev.ExecuteFlow(runCtx, flowApiName, inputs, user)
	if runErr != nil {
		return nil, runErr
	}
	out := &FlowPreviewOutputsResult{}
	if runResult != nil {
		out.ActionName = runResult.ActionName
		out.IsSuccess = runResult.IsSuccess
		out.OutputValues = runResult.OutputValues
		out.Errors = runResult.Errors
	}
	return out, nil
}

// PreviewPreparedFlowOutputs is the exact-closure variant used by the private
// Platform boundary.  It preserves the existing output-only/no-DML behavior
// while failing closed if the Flow attempts to leave the selected closure.
func (e *FlowExecutor) PreviewPreparedFlowOutputs(
	ctx context.Context,
	flowAPIName string,
	versions map[string]*models.FlowVersion,
	inputs map[string]interface{},
	user *models.UserSession,
) (*FlowPreviewOutputsResult, error) {
	prepared, err := withPreparedFlowClosure(ctx, versions)
	if err != nil {
		return nil, err
	}
	prepared = withPreparedFlowRootAuthorizationRequired(prepared)
	return e.PreviewFlowOutputs(prepared, flowAPIName, inputs, user)
}
