// Package audit — DBFailureEmitter: the out-of-band writer for rejected
// saves (issue #4261).
//
// # Why this is not EmitTx
//
// Every other producer in this package records something that COMMITTED. A
// rejected save is the opposite: the business transaction is being rolled
// back, and the whole point of the row is to outlive that rollback. Writing
// it through the caller's *sql.Tx would roll it back too, leaving exactly the
// blind spot #4261 is about. So this emitter takes its own *sql.DB handle and
// writes outside any caller transaction — the same shape
// _System_FlowAuditLog's DBauditStore uses for automation errors.
//
// # DELIBERATE CARVE-OUT FROM THE CH-3 INVARIANT
//
// emitter_db.go documents CH-3: a head row (FieldName='__record__') is always
// paired with a _System_AuditOutbox payload row, inserted in the same
// transaction, so the outbox worker can fan the head row out into field-level
// rows.
//
// Rejected saves write a head row with **OutboxRowId = NULL and no outbox
// row at all**. This is intentional, not an oversight:
//
//   - The outbox exists to expand a COMMITTED field-level diff. A rejected
//     save committed nothing, so there is no diff to expand. An outbox row
//     here would enqueue work that can only ever produce zero field rows.
//   - <Object>History surfaces are built from the fanned-out rows. A rejected
//     save must NOT appear in a record's field history — nothing changed.
//     Skipping the outbox is what keeps it out.
//
// The pairing therefore reads: **every SUCCESS head row has an outbox row;
// FAILURE head rows never do.** Consumers that join head rows to the outbox
// must filter on Outcome rather than assuming the join always matches.
//
// # Best-effort by contract
//
// A failure here must never turn a validation rejection into a 500. This
// emitter still returns its error — swallowing it here would hide audit
// breakage from tests and operators — but every caller on the request path is
// required to log-and-continue. See records.emitRejectedSaveAudit, which is
// the only production caller and which owns that guarantee.
package audit

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/google/uuid"
)

// RecordIdUnassigned is the RecordId sentinel used when a save is rejected
// before the platform assigned the record an Id.
//
// AuditEvent.Validate() requires a non-empty RecordId and the column is NOT
// NULL, so a rejected create that never reached Id assignment (for example an
// FLS denial during createable-field pruning, which runs before
// generateSystemFields) needs a stand-in. Using an explicit sentinel — rather
// than a blank or a fabricated id — keeps "no id was ever minted" queryable
// and distinguishable from a real record. It follows the existing
// '__record__' / '__created__' sentinel convention in this package.
const RecordIdUnassigned = "__unassigned__"

// FailureSink is the narrow port record-mutation code depends on to record a
// rejected save. Kept to one method so tests can substitute a fake that fails
// on demand (the best-effort mutation test) without a database.
type FailureSink interface {
	EmitFailure(ctx context.Context, event AuditEvent) error
}

// DBFailureEmitter writes FAILURE head rows directly against a *sql.DB,
// outside any caller transaction. See the package-level comment above for the
// CH-3 carve-out this implements.
type DBFailureEmitter struct {
	exec  txExecer
	idGen func() string
}

// NewDBFailureEmitter returns a DBFailureEmitter bound to db with production
// defaults. db MUST be a handle that is NOT enlisted in the caller's business
// transaction — that independence is the whole mechanism.
//
// A nil db yields an emitter that ERRORS on every EmitFailure rather than a
// nil return. Returning nil here would be worse than useless: callers store
// the result in a FailureSink interface, and a nil *DBFailureEmitter in an
// interface is NOT equal to nil, so every "is recording enabled?" check would
// pass and every emission would hit a nil receiver. Failing loudly on each
// call surfaces the misconfiguration in the logs instead.
func NewDBFailureEmitter(db *sql.DB) *DBFailureEmitter {
	if db == nil {
		return &DBFailureEmitter{idGen: uuid.NewString}
	}
	return &DBFailureEmitter{exec: db, idGen: uuid.NewString}
}

// ErrNotFailureOutcome is returned when EmitFailure is handed an event whose
// Outcome is not OutcomeFailure. This emitter is the failure-only path; a
// SUCCESS event routed here would silently skip its outbox row and lose its
// field-level fan-out, so it is rejected loudly instead.
var ErrNotFailureOutcome = fmt.Errorf("audit: EmitFailure requires Outcome=%s", OutcomeFailure)

// EmitFailure writes exactly one head row recording a rejected save.
//
// One row per rejected save, never one per violated constraint: the caller
// classifies the rejection once and passes a single event.
func (e *DBFailureEmitter) EmitFailure(ctx context.Context, event AuditEvent) error {
	if e == nil || e.exec == nil {
		return fmt.Errorf("audit: EmitFailure requires a database handle")
	}
	if event.Outcome != OutcomeFailure {
		return ErrNotFailureOutcome
	}
	if err := event.Validate(); err != nil {
		return err
	}
	// The ordering token comes from the database for the same reason the
	// transactional path reads it there: audit facts must be comparable
	// against DB-side history boundaries, not against a host clock.
	eventAt, err := CurrentEventTime(ctx, e.exec)
	if err != nil {
		return err
	}
	// nil outboxRowID → OutboxRowId IS NULL. This is the carve-out.
	return insertHeadRow(ctx, e.exec, e.idGen(), event, eventAt, nil)
}
