// Package audit defines the unified audit event contract that platform
// services use to record business-observable changes into the _System_AuditLog
// pipeline (and, via the outbox, its field-level expansion).
//
// Contract (design.md §3.1, §3.5):
//
//   - Entrypoint is a REQUIRED field on AuditEvent. It is NOT implicitly sourced
//     from context.Context. Every caller MUST set it explicitly; a goroutine
//     that has lost its ctx MUST NOT silently degrade to REST_API.
//   - Emit() / EmitBatch() reject AuditEvents whose REQUIRED fields are zero
//     (ObjectApiName, RecordId, Action, ChangedById, Entrypoint, Outcome).
//     Rejection surfaces as a returned error and a WARN log — never a silent
//     fallback.
//   - Auto-populated context fields (RequestId, TraceId, SessionId, ClientIp,
//     UserAgent, ParentRequestId) are best-effort: missing values log DEBUG and
//     proceed with empty strings; they do NOT fail validation.
//
// Field-name strategy (Strategy A per loop-prompt decision):
// Go struct field names match DB column names in design §2.1 exactly
// (Action, SessionId, RequestId, TraceId, ...). No translation layer at DB
// write time; this keeps mapping trivial and avoids ambiguity between
// "struct label" and "column label". Note: this file ships only the contract;
// no DB writer is implemented here (that lands in C5+).
//
// Scope of this file: types + interface only. No implementation, no wiring
// to PersistenceService, no consumer. The interface is un-referenced by any
// other package at the time of introduction (C4).
package audit

import (
	"errors"
	"fmt"
	"strings"
)

// AuditAction enumerates the business verbs that the audit pipeline recognizes.
// Values map 1:1 to the _System_AuditLog.Action column (design §2.1).
type AuditAction string

// Valid AuditAction values (design §2.1).
const (
	ActionCreate      AuditAction = "CREATE"
	ActionUpdate      AuditAction = "UPDATE"
	ActionDelete      AuditAction = "DELETE"
	ActionUndelete    AuditAction = "UNDELETE"
	ActionOwnerChange AuditAction = "OWNER_CHANGE"
	ActionLock        AuditAction = "LOCK"
	ActionUnlock      AuditAction = "UNLOCK"
	// ActionExportAudit is the "audit-the-audit" meta-event emitted when an
	// admin reads the audit log itself in bulk (design §4.3 CH-8). Emission
	// is gated by the AUDIT_EXPORT_META_ENABLED env flag (default off) and
	// a result-size threshold. The self-reference ObjectApiName is
	// "_System_AuditLog"; RecordId is a synthetic filter-hash stand-in.
	ActionExportAudit AuditAction = "EXPORT_AUDIT"
)

// AuditEntrypoint enumerates the execution surface that originated an audit event.
// REQUIRED on every AuditEvent (design §3.1, §3.5). Callers MUST pass this
// explicitly; emitter MUST NOT derive it from ctx.
type AuditEntrypoint string

// Valid AuditEntrypoint values (design §2.1, §3.5).
// EntrypointUnknown is the documented fallback an emitter uses when a caller
// somehow reaches Emit() without setting Entrypoint — it is still a real enum
// value (not "" zero) so downstream queries can distinguish "caller bug" from
// "pre-v2 row". Passing AuditEntrypoint("") to Validate() is rejected.
const (
	EntrypointRestAPI AuditEntrypoint = "REST_API"
	EntrypointFlow    AuditEntrypoint = "FLOW"
	EntrypointTrigger AuditEntrypoint = "TRIGGER"
	EntrypointBulkAPI AuditEntrypoint = "BULK_API"
	EntrypointUnknown AuditEntrypoint = "UNKNOWN"
)

// Outcome records whether the business operation that produced the audit event
// succeeded or failed. REQUIRED.
type Outcome string

const (
	OutcomeSuccess Outcome = "SUCCESS"
	OutcomeFailure Outcome = "FAILURE"
)

// FieldDiff is one field-level change; used for UPDATE. Optional on AuditEvent.
type FieldDiff struct {
	FieldName            string
	OldValue             string
	NewValue             string
	FieldHistoryEligible *bool  `json:",omitempty"`
	HistorySequence      *int64 `json:",omitempty"`
}

// AuditEvent is the payload passed to audit emitters. Field groupings
// mirror design §3.1. See Validate() for the REQUIRED contract.
type AuditEvent struct {
	// --- REQUIRED. Zero value rejected by Validate(). ---
	ObjectApiName string
	RecordId      string
	Action        AuditAction
	ChangedById   string
	// Entrypoint is REQUIRED and MUST be passed explicitly by the caller.
	// The emitter MUST NOT infer it from context.Context (design §3.1 CH-4).
	Entrypoint AuditEntrypoint
	Outcome    Outcome

	// --- Optional. ---
	FieldDiffs       []FieldDiff // for UPDATE
	EntrypointDetail string
	ActualUserId     string
	ErrorCode        string

	// ChangesJSON is the optional payload pointer written onto the head
	// row (design §2.1). For UPDATE, carries the set of changed field
	// names so forensic replay can reason about the scope of the change
	// without re-joining the outbox payload row. Set explicitly by
	// MaybeEmitUpdateV2 from FieldDiffs; NULL for CREATE/DELETE head rows
	// (the sentinel FieldName value is the load-bearing signal there).
	ChangesJSON string

	// --- Auto-populated from ctx (best-effort; missing values log DEBUG). ---
	RequestId       string
	TraceId         string
	SessionId       string
	ClientIp        string
	UserAgent       string
	ParentRequestId string
}

// ErrMissingRequired is returned by Validate() / Emit() when any REQUIRED
// field is zero. Callers SHOULD treat this as a programming error (fail-loud).
var ErrMissingRequired = errors.New("audit: required AuditEvent field missing")

// Validate enforces the REQUIRED contract on AuditEvent. Returns an error
// that wraps ErrMissingRequired naming the offending field(s).
//
// This is the single chokepoint for the CH-4 "no silent fallback" invariant:
// a zero Entrypoint (AuditEntrypoint("")) is rejected here, never rewritten
// to REST_API or UNKNOWN on the caller's behalf. Callers that genuinely have
// no information MUST explicitly set EntrypointUnknown, so the loss of
// signal is visible in the record (and in alerting counters).
func (e AuditEvent) Validate() error {
	var missing []string
	if strings.TrimSpace(e.ObjectApiName) == "" {
		missing = append(missing, "ObjectApiName")
	}
	if strings.TrimSpace(e.RecordId) == "" {
		missing = append(missing, "RecordId")
	}
	if string(e.Action) == "" {
		missing = append(missing, "Action")
	}
	if strings.TrimSpace(e.ChangedById) == "" {
		missing = append(missing, "ChangedById")
	}
	if string(e.Entrypoint) == "" {
		missing = append(missing, "Entrypoint")
	}
	if string(e.Outcome) == "" {
		missing = append(missing, "Outcome")
	}
	if len(missing) == 0 {
		return nil
	}
	return fmt.Errorf("%w: %s", ErrMissingRequired, strings.Join(missing, ", "))
}
