// Package rest — EXPORT_AUDIT meta-audit emission (design §4.3 CH-8).
//
// When an admin bulk-reads the audit log via GET /admin/audit/log, we record
// a single EXPORT_AUDIT row so that silent exfiltration of the audit trail
// leaves a trace. Per §4.3, emission happens exactly once at the API
// boundary (not inside the query layer) to prevent recursion:
//
//	AuditQueryService → reads _System_AuditLog → emits → reads → ...
//
// The emission rules implemented here:
//
//  1. Gate: AUDIT_EXPORT_META_ENABLED env flag MUST be "true". Default off
//     per loop-prompt §3 C12 "off-default toggle".
//  2. Threshold: result row count MUST be >= AUDIT_EXPORT_META_THRESHOLD
//     (default 100). Below that, an export is treated as an incident-
//     forensics lookup, not a bulk exfil, and is not recorded.
//  3. Best-effort: emission failure is WARN-logged and SWALLOWED — the
//     export response was already returned to the admin; compliance
//     observability MUST NOT break a legitimate admin action.
//  4. Isolation: meta-audit opens its own short transaction against the
//     shared *sql.DB. It is NOT part of any caller business txn.
//  5. Self-reference: ObjectApiName = "_System_AuditLog", RecordId is a
//     SHA-256(filter-params)[:16] synthetic handle so repeated identical
//     exports by the same admin are correlatable but the raw filter
//     values do not leak into RecordId.
//
// FieldDiffs is intentionally empty — this is a read-event, nothing
// changed. The ChangesJSON-equivalent (filter params + row count) is
// encoded in EntrypointDetail as a compact JSON blob so existing
// _System_AuditLog columns carry it without a schema change.
//
// FLS note: the filter params (object, recordId) may reference field
// names a regular user could not read. Admin API callers hold the
// ViewFieldAuditTrail privilege which is the documented grant to
// bypass FLS for audit investigation; see runbook "EXPORT_AUDIT
// meta-audit" tradeoff paragraph.
package rest

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/nexuscrm/backend/internal/features/audit"
	"github.com/rs/zerolog/log"
)

// exportAuditEmitTimeout bounds the goroutine-dispatched export-audit emit
// so that a hung DB can't leak a long-lived goroutine per admin export.
const exportAuditEmitTimeout = 10 * time.Second

const (
	envExportMetaEnabled   = "AUDIT_EXPORT_META_ENABLED"
	envExportMetaThreshold = "AUDIT_EXPORT_META_THRESHOLD"
	defaultExportThreshold = 100
	// auditLogSelfObject is the self-reference ObjectApiName for meta-audit
	// rows. It points at the platform infrastructure table, not an SObject.
	auditLogSelfObject = "_System_AuditLog"
)

// exportMetaFilters is the payload-equivalent blob recorded for one export.
// Kept flat + small so it fits comfortably in EntrypointDetail.
type exportMetaFilters struct {
	Object   string `json:"object"`
	RecordID string `json:"recordId"`
	Limit    int    `json:"limit"`
	Offset   int    `json:"offset"`
	Rows     int    `json:"rows"`
}

// isExportMetaEnabled returns true only when the env flag is explicitly
// set to a truthy value. Default off.
func isExportMetaEnabled() bool {
	v := strings.TrimSpace(os.Getenv(envExportMetaEnabled))
	return strings.EqualFold(v, "true") || v == "1"
}

// exportMetaThreshold returns the configured row-count threshold at or
// above which an export qualifies as bulk. Default 100 when unset or
// malformed.
func exportMetaThreshold() int {
	raw := strings.TrimSpace(os.Getenv(envExportMetaThreshold))
	if raw == "" {
		return defaultExportThreshold
	}
	n, err := strconv.Atoi(raw)
	if err != nil || n <= 0 {
		return defaultExportThreshold
	}
	return n
}

// filterHash produces a stable short identifier for a set of export
// filters. Used as RecordId so repeat exports by the same admin share
// a handle without leaking raw filter literals.
func filterHash(f exportMetaFilters) string {
	raw := fmt.Sprintf("%s|%s|%d|%d", f.Object, f.RecordID, f.Limit, f.Offset)
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:8]) // 16 hex chars
}

// emitExportAudit best-effort writes one EXPORT_AUDIT row when the flag is
// on AND rowCount >= threshold. It runs after the handler has already
// returned its response; all errors are swallowed with WARN logs.
//
// emitter is injected so tests can exercise the gate + threshold logic
// without a real *sql.DB. A nil db or nil emitter is treated as "not
// configured" and skipped silently (DEBUG log), same as C11's defensive
// path for svcMgr.DB() == nil.
func emitExportAudit(
	ctx context.Context,
	db *sql.DB,
	emitter *audit.DBEmitter,
	userID string,
	filters exportMetaFilters,
) {
	if !isExportMetaEnabled() {
		return
	}
	if filters.Rows < exportMetaThreshold() {
		return
	}
	if db == nil || emitter == nil {
		log.Debug().Msg("EXPORT_AUDIT meta-audit skipped: db or emitter not configured")
		return
	}
	if strings.TrimSpace(userID) == "" {
		// Admin API is always behind requireAuth in production. A missing
		// userID here means middleware was bypassed (test harness or a
		// misconfigured route). Skip silently to avoid WARN spam — the
		// missing auth is a much louder signal elsewhere.
		log.Debug().Msg("EXPORT_AUDIT meta-audit skipped: no userID in context")
		return
	}

	detail, err := json.Marshal(filters)
	if err != nil {
		log.Warn().Err(err).Msg("EXPORT_AUDIT meta-audit: marshal filters failed; skipping")
		return
	}

	event := audit.AuditEvent{
		ObjectApiName:    auditLogSelfObject,
		RecordId:         filterHash(filters),
		Action:           audit.ActionExportAudit,
		ChangedById:      userID,
		Entrypoint:       audit.EntrypointRestAPI,
		EntrypointDetail: string(detail),
		Outcome:          audit.OutcomeSuccess,
	}

	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		log.Warn().Err(err).Msg("EXPORT_AUDIT meta-audit: begin tx failed; export unaffected")
		return
	}
	if err := emitter.EmitTx(ctx, tx, event); err != nil {
		_ = tx.Rollback()
		log.Warn().Err(err).Msg("EXPORT_AUDIT meta-audit: emit failed; export unaffected")
		return
	}
	if err := tx.Commit(); err != nil {
		log.Warn().Err(err).Msg("EXPORT_AUDIT meta-audit: commit failed; export unaffected")
		return
	}
}

// dispatchExportAudit runs emitExportAudit in a goroutine detached from the
// handler's request context (Gin cancels the request context once the
// handler returns — which races this best-effort emit). Mirrors the pattern
// used by writeCaptureRow in the request-capture middleware: preserve trace
// values via context.WithoutCancel, cap wall time with a 10s timeout.
func dispatchExportAudit(
	ctx context.Context,
	db *sql.DB,
	emitter *audit.DBEmitter,
	userID string,
	filters exportMetaFilters,
) {
	go func() {
		emitCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), exportAuditEmitTimeout)
		defer cancel()
		emitExportAudit(emitCtx, db, emitter, userID, filters)
	}()
}
