// Bulk-path adapter (design.md §3.7).
//
// Accepts a batch of SObject mutations from the Bulk API and returns the
// corresponding []AuditEvent with Entrypoint=BULK_API. Pure builder; no DB
// writes. Wiring into the real Bulk path is a follow-up — this commit just
// publishes the adapter so callers can adopt with a one-line call.
//
// Contract shapes (BulkRecord / BulkBatch / BulkAuthContext) are defined
// minimally to what today's adapter needs. Production types will map through
// these at the wiring PR.

package audit

import (
	"errors"
	"fmt"
)

// BulkOp is the per-record mutation verb carried in a BulkBatch. Only the
// three Bulk API CRUD verbs are modeled here (Upsert is resolved to
// Create/Update by the upstream Bulk handler before adapter entry).
type BulkOp string

const (
	BulkOpCreate BulkOp = "CREATE"
	BulkOpUpdate BulkOp = "UPDATE"
	BulkOpDelete BulkOp = "DELETE"
)

// BulkAuthContext carries the actor + request-correlation ids shared by every
// event in a single Bulk job. Propagated identically into each AuditEvent.
type BulkAuthContext struct {
	ChangedById string
	RequestId   string
	TraceId     string
	SessionId   string
	ClientIp    string
	UserAgent   string
}

// BulkRecord is one SObject mutation inside a Bulk batch.
type BulkRecord struct {
	ObjectApiName string
	RecordId      string
	Op            BulkOp
	FieldDiffs    []FieldDiff // populated for Op=UPDATE; empty otherwise
}

// BulkBatch groups a set of BulkRecord entries under one Bulk API job id.
type BulkBatch struct {
	JobId   string
	Records []BulkRecord
}

// ErrUnknownBulkOp is returned when a BulkRecord.Op is not one of the three
// modeled verbs. Indicates a caller contract bug, fail-loud per §3.1.
var ErrUnknownBulkOp = errors.New("audit: unknown BulkOp")

// BuildBulkEvents translates a BulkBatch into []AuditEvent. Every event
// carries Entrypoint=BULK_API and EntrypointDetail=batch.JobId. Auth-context
// fields (ChangedById, RequestId, TraceId, ...) propagate to every event.
//
// Returns ErrUnknownBulkOp if any record.Op is unrecognized; returns an
// ErrMissingRequired-wrapping error if any generated event fails Validate()
// (e.g. empty RecordId). EmitBatch validation is all-or-nothing.
func BuildBulkEvents(auth BulkAuthContext, batch BulkBatch) ([]AuditEvent, error) {
	if auth.ChangedById == "" {
		return nil, fmt.Errorf("%w: ChangedById", ErrMissingRequired)
	}
	if len(batch.Records) == 0 {
		return nil, nil
	}

	events := make([]AuditEvent, 0, len(batch.Records))
	for i, r := range batch.Records {
		action, err := bulkOpToAction(r.Op)
		if err != nil {
			return nil, fmt.Errorf("record[%d]: %w", i, err)
		}
		ev := AuditEvent{
			ObjectApiName:    r.ObjectApiName,
			RecordId:         r.RecordId,
			Action:           action,
			ChangedById:      auth.ChangedById,
			Entrypoint:       EntrypointBulkAPI,
			EntrypointDetail: batch.JobId,
			Outcome:          OutcomeSuccess,
			FieldDiffs:       r.FieldDiffs,
			RequestId:        auth.RequestId,
			TraceId:          auth.TraceId,
			SessionId:        auth.SessionId,
			ClientIp:         auth.ClientIp,
			UserAgent:        auth.UserAgent,
		}
		if err := ev.Validate(); err != nil {
			return nil, fmt.Errorf("record[%d]: %w", i, err)
		}
		events = append(events, ev)
	}
	return events, nil
}

func bulkOpToAction(op BulkOp) (AuditAction, error) {
	switch op {
	case BulkOpCreate:
		return ActionCreate, nil
	case BulkOpUpdate:
		return ActionUpdate, nil
	case BulkOpDelete:
		return ActionDelete, nil
	default:
		return "", fmt.Errorf("%w: %q", ErrUnknownBulkOp, op)
	}
}
