// Package audit — DBEmitter: concrete AuditEmitter implementation that writes
// the synchronous head row + outbox enqueue inside the caller's business
// transaction (design §3.2, §3.3 CH-3).
//
// CH-3 invariant: the head row ("FieldName=__record__" summary) and the outbox
// row (payload for later field-level fan-out by the C6 worker) MUST be inserted
// in the SAME transaction as the SObject write. Any failure on either insert
// propagates so the caller's txn rolls back — never a silent half-write.
//
// Wiring into PersistenceService is explicitly NOT done here; that is C8 scope.
// At this commit DBEmitter is un-referenced outside unit tests.
package audit

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"time"

	"github.com/google/uuid"
)

// headFieldNameSentinel is the pseudo-FieldName used on the record-level head
// row written synchronously. Field-level diffs are fanned out later by the
// outbox worker and use their real FieldName values. Design §2.1 / §3.2.
const headFieldNameSentinel = "__record__"

// txExecer is the narrow transactional interface DBEmitter needs. `*sql.Tx`
// satisfies it; sqlmock also produces an object that satisfies it. Keeping the
// surface minimal makes unit tests trivial.
type txExecer interface {
	ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}

// DBEmitter is the default AuditEmitter — writes AuditLog head row + Outbox
// payload in a single caller-provided transaction.
//
// DBEmitter does NOT start its own transaction. The caller's business txn is
// passed explicitly to EmitTx / EmitBatchTx; the Emit / EmitBatch methods
// (satisfying AuditEmitter) are intentionally not usable until a tx is
// attached via WithTx, because a non-transactional head emit would violate
// §3.3. See EmitTx below.
type DBEmitter struct {
	// clock is injected for deterministic tests. Production uses time.Now.
	clock func() time.Time
	// idGen generates AuditLog.Id values. Production uses uuid.NewString.
	idGen func() string
}

// NewDBEmitter returns a DBEmitter with production defaults.
func NewDBEmitter() *DBEmitter {
	return &DBEmitter{
		clock: time.Now,
		idGen: uuid.NewString,
	}
}

// Emit on the bare DBEmitter is not supported — writing a head row outside
// the caller's business transaction violates §3.3 CH-3 (atomicity with the
// SObject write). Callers MUST obtain a tx-bound emitter via WithTx or call
// EmitTx directly. Returning an error (not panicking) keeps the contract
// visible to static analysis and tests.
func (e *DBEmitter) Emit(ctx context.Context, event AuditEvent) error {
	return fmt.Errorf("audit: DBEmitter.Emit requires a caller-provided transaction; use EmitTx or WithTx")
}

// EmitBatch mirrors Emit's restriction.
func (e *DBEmitter) EmitBatch(ctx context.Context, events []AuditEvent) error {
	return fmt.Errorf("audit: DBEmitter.EmitBatch requires a caller-provided transaction; use EmitBatchTx or WithTx")
}

// EmitTx writes one head row + one outbox payload row inside the caller's
// transaction. Both inserts share the same tx; propagating an error commits
// nothing — the caller is responsible for Rollback on error.
//
// Order of operations (§3.3):
//  1. Validate the event (zero-Entrypoint rejected).
//  2. Insert into _System_AuditOutbox; read back auto-increment Id.
//  3. Insert into _System_AuditLog with FieldName=__record__, OutboxRowId
//     pointing at step 2's row.
//
// Outbox-first ordering means: if step 3 fails, the outbox row is orphaned
// for one txn but rolled back with the business txn, so no stranded payload
// outlives the rollback. If step 2 fails, no head row is written — caller
// observes the failure and aborts.
func (e *DBEmitter) EmitTx(ctx context.Context, tx txExecer, event AuditEvent) error {
	if err := event.Validate(); err != nil {
		return err
	}
	return e.emitTxNoValidate(ctx, tx, event)
}

// emitTxNoValidate performs the EmitTx work without running Validate(). It is
// the shared body used by EmitTx (which validates) and EmitBatchTx (which
// validates all events upfront, so per-event re-validation would be wasted
// work). Keeping the nil-tx guard here means both entry points remain safe.
func (e *DBEmitter) emitTxNoValidate(ctx context.Context, tx txExecer, event AuditEvent) error {
	if tx == nil {
		return fmt.Errorf("audit: EmitTx requires a non-nil transaction")
	}

	now := e.clock().UTC()

	// 1. Enqueue full payload to _System_AuditOutbox. The C6 worker will
	//    consume this and fan out field-level diffs into _System_AuditLog.
	payload, err := json.Marshal(event)
	if err != nil {
		return fmt.Errorf("audit: marshal event payload: %w", err)
	}
	res, err := tx.ExecContext(ctx,
		`INSERT INTO _System_AuditOutbox (Payload, EnqueuedAt, Attempts) VALUES (?, ?, 0)`,
		payload, now,
	)
	if err != nil {
		return fmt.Errorf("audit: insert outbox: %w", err)
	}
	outboxRowID, err := res.LastInsertId()
	if err != nil {
		return fmt.Errorf("audit: read outbox LastInsertId: %w", err)
	}

	// 2. Insert head row — one summary row per Emit call.
	//    FieldName = __record__ sentinel (§2.1, §3.2).
	headID := e.idGen()
	_, err = tx.ExecContext(ctx,
		`INSERT INTO _System_AuditLog (
			Id, ObjectApiName, RecordId, FieldName, OldValue, NewValue,
			ChangedById, ChangedAt, CreatedDate, LastModifiedDate,
			Action, ChangesJSON, Entrypoint, EntrypointDetail, RequestId, TraceId,
			SessionId, ParentRequestId, ActualUserId, ClientIp, UserAgent,
			Outcome, ErrorCode, OutboxRowId
		) VALUES (?, ?, ?, ?, '', '',
			?, ?, ?, ?,
			?, ?, ?, ?, ?, ?,
			?, ?, ?, ?, ?,
			?, ?, ?)`,
		headID, event.ObjectApiName, event.RecordId, headFieldNameSentinel,
		event.ChangedById, now, now, now,
		string(event.Action), nullableString(event.ChangesJSON),
		string(event.Entrypoint), nullableString(event.EntrypointDetail),
		nullableString(event.RequestId), nullableString(event.TraceId),
		nullableString(event.SessionId), nullableString(event.ParentRequestId),
		nullableString(event.ActualUserId), nullableString(event.ClientIp),
		nullableString(event.UserAgent),
		string(event.Outcome), nullableString(event.ErrorCode), outboxRowID,
	)
	if err != nil {
		return fmt.Errorf("audit: insert head row: %w", err)
	}
	return nil
}

// EmitBatchTx validates every event then writes them one-by-one in the same
// tx — all-or-nothing per §3.1 / §3.4 (single outbox row per event).
func (e *DBEmitter) EmitBatchTx(ctx context.Context, tx txExecer, events []AuditEvent) error {
	if tx == nil {
		return fmt.Errorf("audit: EmitBatchTx requires a non-nil transaction")
	}
	for i := range events {
		if err := events[i].Validate(); err != nil {
			return fmt.Errorf("audit: batch event %d invalid: %w", i, err)
		}
	}
	for i := range events {
		if err := e.emitTxNoValidate(ctx, tx, events[i]); err != nil {
			return fmt.Errorf("audit: batch event %d emit: %w", i, err)
		}
	}
	return nil
}

// nullableString returns sql.NullString{Valid: false} for empty strings,
// ensuring NULL is stored (not ""). Matches design §2.1 NULL-by-default columns.
func nullableString(s string) interface{} {
	if s == "" {
		return nil
	}
	return s
}
