// Package audit — `_System_RequestCapture` row shape + SQL store.
//
// Split out from request_capture_middleware.go to keep each file focused
// (middleware logic vs. DB persistence). The types here are the narrow
// data-plane surface RequestCaptureMiddleware depends on; tests substitute
// an in-memory implementation of RequestCaptureStore to avoid a real DB.
package audit

import (
	"context"
	"database/sql"
	"time"
)

// RequestCaptureStore is the narrow DB surface the middleware needs. An
// interface (rather than *sql.DB directly) keeps tests on an in-memory fake
// without pulling the full persistence layer into every call site.
type RequestCaptureStore interface {
	InsertRequestCapture(ctx context.Context, row RequestCaptureRow) error
}

// RequestCaptureRow is the plain-old-struct projection of a single
// `_System_RequestCapture` row (see system_tables.json C13 seed). Field
// order intentionally matches the SQL column order used by sqlStore below.
type RequestCaptureRow struct {
	RequestID      string
	ReceivedAt     time.Time
	Method         string
	Path           string
	Query          string
	Headers        string // JSON
	Body           string
	BodySize       int
	BodyTruncated  bool
	ResponseStatus int
	ResponseBody   sql.NullString
	UserID         sql.NullString
	SessionID      sql.NullString
	ClientIP       sql.NullString
	Entrypoint     string
	LatencyMs      int
}

// sqlStore is the production RequestCaptureStore. Reuses *sql.DB; the write
// is a single INSERT and does not need txn coupling (§14.6 — independent
// post-commit best-effort write).
type sqlStore struct {
	db *sql.DB
}

// NewSQLRequestCaptureStore returns a production RequestCaptureStore backed
// by *sql.DB. Returns nil when db is nil so callers can keep construction
// unconditional — a nil store is treated as "capture disabled" by the
// middleware's config path.
func NewSQLRequestCaptureStore(db *sql.DB) RequestCaptureStore {
	if db == nil {
		return nil
	}
	return &sqlStore{db: db}
}

const insertRequestCaptureSQL = `
INSERT INTO _System_RequestCapture (
  RequestId, ReceivedAt, Method, Path, Query, Headers, Body, BodySize,
  BodyTruncated, ResponseStatus, ResponseBody, UserId, SessionId, ClientIp,
  Entrypoint, LatencyMs
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`

// InsertRequestCapture writes a single row. No retry / no backoff — the
// caller (middleware goroutine) treats failures as best-effort WARN and
// moves on (§14.6 failure policy).
func (s *sqlStore) InsertRequestCapture(ctx context.Context, row RequestCaptureRow) error {
	_, err := s.db.ExecContext(ctx, insertRequestCaptureSQL,
		row.RequestID, row.ReceivedAt, row.Method, row.Path, row.Query,
		nullIfEmpty(row.Headers), nullIfEmpty(row.Body), row.BodySize,
		row.BodyTruncated, row.ResponseStatus, row.ResponseBody,
		row.UserID, row.SessionID, row.ClientIP, row.Entrypoint, row.LatencyMs,
	)
	return err
}

// nullIfEmpty converts empty strings to SQL NULL. Used for columns where
// NULL and "" are semantically distinct (Headers/Body NULL = not captured;
// empty string would imply "captured and empty" which misrepresents the
// state).
func nullIfEmpty(s string) sql.NullString {
	if s == "" {
		return sql.NullString{}
	}
	return sql.NullString{String: s, Valid: true}
}
