// Package rest — admin audit log read API (§5.2).
//
// GET /admin/audit/log?object=<Object>&recordId=<Id>&limit=<N>&offset=<N>
//
// Returns the full _System_AuditLog row payload for callers with the
// ViewFieldAuditTrail system permission (Salesforce-native name). This
// endpoint is deliberately DISTINCT from the <Object>History SOQL view:
//
//   - <Object>History is the read-by-anyone view with FLS redaction.
//   - /admin/audit/log is the privileged view that returns Entrypoint,
//     TraceId, RequestId, SessionId, ClientIp, Action, etc — the full
//     audit metadata needed for incident forensics.
//
// Because callers must have ViewFieldAuditTrail, we do NOT apply FLS
// redaction here: that permission is the documented grant for "see all
// field history including fields you otherwise cannot read" (§5.2).
//
// Error surface never echoes filter values to avoid leaking filter
// literals into logs (same rule as fls_plan.go).
package rest

import (
	"database/sql"
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/application/services"
	"github.com/nexuscrm/backend/internal/features/audit"
)

// AdminAuditHandler exposes the privileged audit-log read API.
type AdminAuditHandler struct {
	svcMgr      *services.ServiceManager
	metaEmitter *audit.DBEmitter // for EXPORT_AUDIT meta-audit (§4.3 CH-8)
}

// NewAdminAuditHandler constructs the handler.
func NewAdminAuditHandler(svcMgr *services.ServiceManager) *AdminAuditHandler {
	return &AdminAuditHandler{
		svcMgr:      svcMgr,
		metaEmitter: audit.NewDBEmitter(),
	}
}

// adminAuditLogRow is the JSON shape returned for each row. Field names
// match _System_AuditLog columns (Strategy A from emitter.go docs) so
// ops tooling can map 1:1 without a translation layer.
type adminAuditLogRow struct {
	ID               string  `json:"Id"`
	ObjectAPIName    string  `json:"ObjectApiName"`
	RecordID         string  `json:"RecordId"`
	FieldName        string  `json:"FieldName"`
	OldValue         *string `json:"OldValue,omitempty"`
	NewValue         *string `json:"NewValue,omitempty"`
	ChangedByID      string  `json:"ChangedById"`
	ChangedAt        string  `json:"ChangedAt"`
	Action           string  `json:"Action,omitempty"`
	ChangesJSON      string  `json:"ChangesJSON,omitempty"`
	Entrypoint       string  `json:"Entrypoint,omitempty"`
	EntrypointDetail string  `json:"EntrypointDetail,omitempty"`
	RequestID        string  `json:"RequestId,omitempty"`
	TraceID          string  `json:"TraceId,omitempty"`
	SessionID        string  `json:"SessionId,omitempty"`
	ClientIP         string  `json:"ClientIp,omitempty"`
	ActualUserID     string  `json:"ActualUserId,omitempty"`
	Outcome          string  `json:"Outcome,omitempty"`
	ErrorCode        string  `json:"ErrorCode,omitempty"`
}

// GetAuditLog handles GET /admin/audit/log.
//
// Required query params:
//   - object  (SObject API name; filters ObjectApiName)
//   - recordId (filters RecordId)
//
// Optional:
//   - limit (default 100, max 500)
//   - offset
func (h *AdminAuditHandler) GetAuditLog(c *gin.Context) {
	object := c.Query("object")
	recordID := c.Query("recordId")
	if object == "" || recordID == "" {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "object and recordId are required",
		})
		return
	}

	limit := 100
	if s := c.Query("limit"); s != "" {
		if n, err := strconv.Atoi(s); err == nil && n > 0 {
			limit = n
		}
	}
	if limit > 500 {
		limit = 500
	}
	offset := 0
	if s := c.Query("offset"); s != "" {
		if n, err := strconv.Atoi(s); err == nil && n >= 0 {
			offset = n
		}
	}

	db := h.svcMgr.DB()
	if db == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "audit log backend not initialized"})
		return
	}

	rows, err := db.QueryContext(c.Request.Context(),
		`SELECT Id, ObjectApiName, RecordId, FieldName, OldValue, NewValue,
		        ChangedById, ChangedAt, Action, ChangesJSON, Entrypoint, EntrypointDetail,
		        RequestId, TraceId, SessionId, ClientIp, ActualUserId,
		        Outcome, ErrorCode
		 FROM _System_AuditLog
		 WHERE ObjectApiName = ? AND RecordId = ?
		 ORDER BY ChangedAt DESC
		 LIMIT ? OFFSET ?`,
		object, recordID, limit, offset,
	)
	if err != nil {
		// Intentionally generic — see file header re: never echoing args.
		c.JSON(http.StatusInternalServerError, gin.H{"error": "audit log query failed"})
		return
	}
	defer rows.Close()

	out := make([]adminAuditLogRow, 0, 32)
	for rows.Next() {
		var r adminAuditLogRow
		var (
			old, nw                                  sql.NullString
			changedAt                                sql.NullTime
			action, changesJSON, entry, entryDetail  sql.NullString
			reqID, traceID, sessID, clientIP, actual sql.NullString
			outcome, errCode                         sql.NullString
		)
		if err := rows.Scan(
			&r.ID, &r.ObjectAPIName, &r.RecordID, &r.FieldName,
			&old, &nw, &r.ChangedByID, &changedAt,
			&action, &changesJSON, &entry, &entryDetail,
			&reqID, &traceID, &sessID, &clientIP, &actual,
			&outcome, &errCode,
		); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "audit log scan failed"})
			return
		}
		if old.Valid {
			v := old.String
			r.OldValue = &v
		}
		if nw.Valid {
			v := nw.String
			r.NewValue = &v
		}
		if changedAt.Valid {
			r.ChangedAt = changedAt.Time.UTC().Format(time.RFC3339Nano)
		}
		r.Action = action.String
		r.ChangesJSON = changesJSON.String
		r.Entrypoint = entry.String
		r.EntrypointDetail = entryDetail.String
		r.RequestID = reqID.String
		r.TraceID = traceID.String
		r.SessionID = sessID.String
		r.ClientIP = clientIP.String
		r.ActualUserID = actual.String
		r.Outcome = outcome.String
		r.ErrorCode = errCode.String
		out = append(out, r)
	}
	if err := rows.Err(); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "audit log iterate failed"})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"records": out,
		"count":   len(out),
		"limit":   limit,
		"offset":  offset,
		"object":  object,
	})

	// EXPORT_AUDIT meta-audit (§4.3 CH-8). Emission runs AFTER the response
	// and failures are swallowed inside emitExportAudit — compliance
	// observability MUST NOT break a legitimate admin export. Gate + threshold
	// checks live in the helper so this handler stays focused on the read.
	var userID string
	if u := GetUserFromContext(c); u != nil {
		userID = u.ID
	}
	dispatchExportAudit(
		c.Request.Context(),
		db,
		h.metaEmitter,
		userID,
		exportMetaFilters{
			Object:   object,
			RecordID: recordID,
			Limit:    limit,
			Offset:   offset,
			Rows:     len(out),
		},
	)
}
