// Package rest — history_query.go: SOQL dispatch for `<Object>History`.
//
// See backend/internal/features/audit/history_soql.go for the translator
// core. This file glues the SOQL parser's api.QueryRequest shape onto the
// audit.HistorySOQLInput / HistoryQueryPlan pipeline, applies FLS
// redaction using the caller's field-read permissions, and renders rows
// in the standard SF SOQL response envelope (totalSize / done / records
// with attributes).
package rest

import (
	"errors"
	"fmt"
	"net/http"
	"regexp"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/features/audit"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/pkg/apierr"
	"github.com/nexuscrm/shared/pkg/models"
)

// userPermsAdapter wraps PermissionService into the minimal
// audit.UserPermissions contract RedactFieldHistoryPlan expects.
type userPermsAdapter struct {
	svc  *permissions.PermissionService
	ctx  *gin.Context
	user *models.UserSession
}

// HasFieldRead reports whether the session user can read (object, field).
// Non-superuser with no explicit FLS row falls back to "visible" to match
// the repo-wide default behavior — see CheckFieldVisibilityWithUser.
func (a *userPermsAdapter) HasFieldRead(object, field string) bool {
	if a == nil || a.svc == nil {
		return false
	}
	return a.svc.CheckFieldVisibilityWithUser(a.ctx.Request.Context(), object, field, a.user)
}

// parentIDFromFilter picks the ParentId literal out of a SOQL WHERE.
// Phase 2A only supports `ParentId = 'xxx'` exclusively — additional
// predicates (e.g. `AND Field = 'Description'`) are rejected upstream by
// validateHistoryFilter rather than silently widened here. Returns empty
// string if no ParentId literal is present.
var parentIDPattern = regexp.MustCompile(`(?i)ParentId\s*=\s*'([^']*)'`)

func parentIDFromFilter(filter string) string {
	m := parentIDPattern.FindStringSubmatch(filter)
	if len(m) != 2 {
		return ""
	}
	return m[1]
}

// residualPredicatePattern captures the *field name* of any predicate
// that survives after stripping `ParentId = 'literal'`. It matches a
// bare identifier followed by a comparison operator — enough to surface
// "what the user tried to filter on" WITHOUT echoing the user's literal
// value (§5.1 FLS error-format rule: never leak user-supplied values in
// error messages).
var residualPredicatePattern = regexp.MustCompile(`(?i)([A-Za-z_][A-Za-z0-9_.]*)\s*(?:=|<>|!=|<|>|<=|>=|\bLIKE\b|\bIN\b)`)

// validateHistoryFilter returns the unsupported predicate's field name
// (for the error message) if `filter` contains anything other than an
// optional `ParentId = 'literal'` clause. Phase 2A supports exactly one
// predicate; Phase 2B will broaden this.
func validateHistoryFilter(filter string) (unsupportedField string, ok bool) {
	residual := strings.TrimSpace(parentIDPattern.ReplaceAllString(filter, ""))
	// Strip leading/trailing boolean connectives left over by the
	// replacement (e.g. "AND Field = 'Description'" → "Field = '...'").
	residual = strings.TrimSpace(trimBooleanConnectives(residual))
	if residual == "" {
		return "", true
	}
	if m := residualPredicatePattern.FindStringSubmatch(residual); len(m) == 2 {
		return m[1], false
	}
	// Residual is non-empty but we could not isolate a field name —
	// still reject, but with a generic label (never echo the raw text).
	return "predicate", false
}

// trimBooleanConnectives removes leading / trailing AND/OR tokens so the
// residue check after stripping `ParentId = '...'` does not false-flag
// on pure glue. It is intentionally narrow — only the keyword itself is
// stripped, never any surrounding identifiers.
func trimBooleanConnectives(s string) string {
	for {
		t := strings.TrimSpace(s)
		upper := strings.ToUpper(t)
		switch {
		case strings.HasPrefix(upper, "AND "):
			s = t[4:]
		case strings.HasPrefix(upper, "OR "):
			s = t[3:]
		case strings.HasSuffix(upper, " AND"):
			s = t[:len(t)-4]
		case strings.HasSuffix(upper, " OR"):
			s = t[:len(t)-3]
		default:
			return t
		}
	}
}

// executeHistoryQuery handles GET /query when the FROM object ends in
// "History". Called from query_handler.executeQueryInternal before the
// normal QueryService dispatch.
func (h *QueryHandler) executeHistoryQuery(c *gin.Context, req *models.QueryRequest) {
	user := MustGetUser(c)

	// Phase 2A only supports `ParentId = '<literal>'`. Any additional
	// predicate (e.g. `Field = 'Description'`, `CreatedDate > ...`) must
	// be rejected with HTTP 400 — silently widening the result set is
	// worse than a clean failure for an audit surface. Echo only the
	// field name, never the user-supplied value.
	if fieldName, okFilter := validateHistoryFilter(req.FilterExpr); !okFilter {
		c.JSON(http.StatusBadRequest, gin.H{
			"errorCode": "UNSUPPORTED_HISTORY_PREDICATE",
			"message": fmt.Sprintf(
				"<Object>History supports only 'ParentId = <literal>' in Phase 2A; unsupported predicate on %q",
				fieldName,
			),
		})
		return
	}

	// Build the plan from SOQL input. Defaults: DESC on CreatedDate.
	in := audit.HistorySOQLInput{
		ObjectAPIName: req.ObjectAPIName,
		SelectFields:  req.SelectFields,
		ParentID:      parentIDFromFilter(req.FilterExpr),
		Limit:         req.Limit,
		Offset:        req.Offset,
		SortDesc:      !strings.EqualFold(req.SortDirection, "ASC"),
	}
	plan, err := audit.BuildHistoryPlan(in)
	if err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeMalformedQuery, err.Error()))
		return
	}

	// FLS redaction (§4.1 CH-6). If a predicate on a denied value column
	// slipped through the plan (Phase 2A only fills ParentId so this is
	// a defense-in-depth hook for later phases), surface it as HTTP 400
	// via the typed sentinel.
	adapter := &userPermsAdapter{svc: h.svcMgr.Permissions, ctx: c, user: user}
	redacted, err := audit.RedactFieldHistoryPlan(plan, adapter)
	if err != nil {
		var denied *audit.FieldDeniedByFLSError
		if errors.As(err, &denied) {
			c.JSON(http.StatusBadRequest, gin.H{
				"errorCode": denied.Code(),
				"message":   denied.Error(), // field name only; no filter value
			})
			return
		}
		c.JSON(http.StatusBadRequest, gin.H{"message": "history plan invalid"})
		return
	}

	// Execute against _System_AuditLog.
	db := h.svcMgr.DB()
	if db == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"message": "audit backend not initialized"})
		return
	}
	limit := in.Limit
	if limit == 0 {
		limit = 200
	}
	rows, err := audit.ExecuteHistoryPlan(c.Request.Context(), db, redacted, in.ParentID, limit, in.Offset, in.SortDesc)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"message": "audit history query failed"})
		return
	}

	// SF SOQL response shape.
	records := make([]map[string]interface{}, len(rows))
	for i, r := range rows {
		rec := map[string]interface{}{
			"Id":          r.ID,
			"ParentId":    r.ParentID,
			"Field":       r.Field,
			"OldValue":    stringOrNil(r.OldValue),
			"NewValue":    stringOrNil(r.NewValue),
			"DataType":    r.DataType,
			"CreatedDate": r.CreatedDate,
			"CreatedById": r.CreatedByID,
			"attributes": map[string]interface{}{
				"type": req.ObjectAPIName,
				"url":  fmt.Sprintf("/services/data/v63.0/sobjects/%s/%s", req.ObjectAPIName, r.ID),
			},
		}
		records[i] = rec
	}
	c.JSON(http.StatusOK, gin.H{
		"totalSize": len(records),
		"done":      true,
		"records":   records,
	})
}

// stringOrNil returns *string's value or nil (for JSON `null`).
func stringOrNil(s *string) interface{} {
	if s == nil {
		return nil
	}
	return *s
}
