// Package audit — fls_plan.go implements the §4.1 CH-6 FLS redaction
// invariant for <Object>History queries.
//
// The threat model: an attacker with access to History rows but without FLS
// Read on field F must not be able to probe F's Old/NewValue via a SOQL
// predicate (LIKE, equality, comparison). If the predicate were passed to
// the DB it would leak the value one bit at a time via binary search on
// response size / timing, even though the value never appears in the
// response body.
//
// The redaction has two modes (design §4.1):
//
//  1. Projection on a denied field  -> silently dropped. A suppression
//     predicate `FieldName != F` is added so NO history rows for F
//     surface. The caller never learns F exists in this object's history.
//
//  2. Predicate on a denied field's OldValue / NewValue -> REJECTED at the
//     planner stage with *FieldDeniedByFLSError (HTTP 400,
//     public wire code INVALID_FIELD). Never 500, never echoing the filter value
//     (echoing would itself be a leak channel).
//
// Metadata predicates (FieldName, ChangedAt, ChangedById) are always allowed
// and do not count as value probes.
//
// The L1 dialect lowers any Salesforce-specific history query into a neutral
// HistoryQueryPlan before this redactor runs. This file is parse-free: it
// operates on the post-parse plan struct only.
package audit

import (
	"fmt"

	"github.com/nexuscrm/shared/pkg/contract"
)

// Column identifies which History column a FieldRef or Predicate targets.
type Column string

const (
	// ColumnOldValue / ColumnNewValue are the two FLS-gated value columns.
	ColumnOldValue Column = "OldValue"
	ColumnNewValue Column = "NewValue"
	// ColumnFieldName / ColumnChangedAt / ColumnChangedById are metadata
	// columns: not FLS-gated in their own right.
	ColumnFieldName   Column = "FieldName"
	ColumnChangedAt   Column = "ChangedAt"
	ColumnChangedById Column = "ChangedById"
)

// Op is a placeholder for the predicate operator. The redactor only needs
// to know whether a predicate is on a value column — the op itself is not
// inspected for the FLS decision. The caller populates this for downstream use.
type Op string

const (
	OpNotEqual Op = "!="
)

// PredicateKind classifies what a predicate is probing. The FLS decision
// depends on Kind, not Op.
type PredicateKind int

const (
	// PredicateValue: predicate on a field-value column (Old/NewValue).
	// FLS-denied -> reject.
	PredicateValue PredicateKind = iota
	// PredicateMetadata: predicate on a metadata column (FieldName,
	// ChangedAt, ChangedById). Always allowed.
	PredicateMetadata
	// PredicateSuppressField: planner-injected "FieldName != F" to hide the
	// existence of a denied field. Produced by RedactFieldHistoryPlan.
	PredicateSuppressField
)

// FieldRef identifies a (field, column) pair the caller wants projected.
type FieldRef struct {
	FieldName string // API name of the SObject field; empty for metadata-only
	Column    Column
}

// Predicate is a WHERE-clause fragment the caller has built up.
//
// RawValueRedacted is populated by the caller for downstream use (e.g. parameter
// binding). The redactor does NOT include it in error messages — that
// would itself be an FLS side channel.
type Predicate struct {
	FieldName        string // target field API name (empty for metadata)
	Column           Column
	Op               Op
	Kind             PredicateKind
	RawValueRedacted string // filter literal; NEVER echoed in errors
}

// HistoryQueryPlan is the minimal post-parse shape the redactor operates on.
// The redactor returns a sanitized copy or an error.
type HistoryQueryPlan struct {
	ObjectApiName string
	Projections   []FieldRef
	Predicates    []Predicate
	filters       []historyQueryFilter
	// DeniedFields is populated by RedactFieldHistoryPlan: the set of
	// fields the caller referenced but lacks FLS Read on. Informational
	// only — callers should not expose this list to end users.
	DeniedFields []string
	// AllowedFields is a metadata/FLS-derived positive allow-list for the public
	// <Object>History view. A non-nil empty slice means fail closed and return no
	// field rows. Nil preserves legacy/internal callers that do not enforce an
	// allow-list.
	AllowedFields []string
}

// UserPermissions is the minimal FLS interface the redactor needs.
// Implementations may wrap the project's permission repo / effective-perm
// cache; the redactor is permission-source-agnostic.
type UserPermissions interface {
	HasFieldRead(objectApiName, fieldApiName string) (bool, error)
}

// CheckFLSReadAccess returns true iff the caller has FLS Read on
// (objectApiName, fieldApiName). A nil UserPermissions denies everything
// — fail closed.
func CheckFLSReadAccess(perms UserPermissions, objectApiName, fieldApiName string) (bool, error) {
	if perms == nil {
		return false, nil
	}
	return perms.HasFieldRead(objectApiName, fieldApiName)
}

// FieldDeniedByFLSError is the typed neutral error returned when the planner
// detects a predicate on a field the caller cannot read. Keep this backend-local
// name as an alias so existing audit callers do not drift, while L1 dialect
// modules can classify QueryHistory errors through shared/pkg/contract.
type FieldDeniedByFLSError = contract.FieldDeniedByFLSError

// RedactFieldHistoryPlan enforces the §4.1 CH-6 invariant.
//
// Returns a NEW plan; the input is not mutated.
//
// Ordering:
//  1. Validate ObjectApiName is set.
//  2. Scan predicates first — a predicate on a denied field's value column
//     is fatal, so fail fast before any projection mutation. This also
//     means callers get a deterministic error even if the plan has both a
//     droppable projection and a rejectable predicate.
//  3. Seed denied field suppression from the caller-provided object metadata
//     field list. History queries project generic OldValue/NewValue columns, so
//     row-level FLS suppression must not depend on the selected history columns.
//  4. Walk projections, dropping any whose field lacks FLS read and recording
//     the field in DeniedFields.
//  5. For each denied field, inject a PredicateSuppressField so the DB returns
//     no rows referencing it.
func RedactFieldHistoryPlan(plan HistoryQueryPlan, perms UserPermissions, deniedFieldNames ...string) (HistoryQueryPlan, error) {
	if plan.ObjectApiName == "" {
		return HistoryQueryPlan{}, fmt.Errorf("history query plan: ObjectApiName is required")
	}

	// Step 1: reject predicates on denied value columns.
	for _, p := range plan.Predicates {
		if p.Kind != PredicateValue {
			continue
		}
		if !isValueColumn(p.Column) {
			continue
		}
		if p.FieldName == "" {
			continue
		}
		allowed, err := CheckFLSReadAccess(perms, plan.ObjectApiName, p.FieldName)
		if err != nil {
			return HistoryQueryPlan{}, err
		}
		if allowed {
			continue
		}
		return HistoryQueryPlan{}, &FieldDeniedByFLSError{FieldApiName: p.FieldName}
	}

	// Build a new plan and seed row-level suppression from the metadata-derived
	// denied-field list. This is the critical history-query path: OldValue and
	// NewValue are generic columns, so denied audit rows must be suppressed even
	// when the SELECT list only names history columns.
	out := HistoryQueryPlan{
		ObjectApiName: plan.ObjectApiName,
		Projections:   make([]FieldRef, 0, len(plan.Projections)),
		Predicates:    make([]Predicate, 0, len(plan.Predicates)+4),
		filters:       append([]historyQueryFilter(nil), plan.filters...),
	}
	if plan.AllowedFields != nil {
		out.AllowedFields = append([]string{}, plan.AllowedFields...)
	}
	deniedSet := make(map[string]struct{})
	deniedOrder := make([]string, 0, len(deniedFieldNames)+len(plan.Projections))
	addDenied := func(fieldName string) {
		if fieldName == "" {
			return
		}
		if _, exists := deniedSet[fieldName]; exists {
			return
		}
		deniedSet[fieldName] = struct{}{}
		deniedOrder = append(deniedOrder, fieldName)
	}
	for _, fieldName := range deniedFieldNames {
		addDenied(fieldName)
	}
	for _, proj := range plan.Projections {
		if proj.FieldName == "" {
			// Metadata projection — always allowed.
			out.Projections = append(out.Projections, proj)
			continue
		}
		allowed, err := CheckFLSReadAccess(perms, plan.ObjectApiName, proj.FieldName)
		if err != nil {
			return HistoryQueryPlan{}, err
		}
		if allowed {
			out.Projections = append(out.Projections, proj)
			continue
		}
		// Silently drop and remember for suppression.
		addDenied(proj.FieldName)
	}

	// Carry original predicates forward (all already validated).
	out.Predicates = append(out.Predicates, plan.Predicates...)

	// Emit suppression predicates + DeniedFields in deterministic order: first
	// metadata-derived denied fields in schema order, then projection-derived
	// denied fields in projection order.
	for _, fieldName := range deniedOrder {
		out.DeniedFields = append(out.DeniedFields, fieldName)
		out.Predicates = append(out.Predicates, Predicate{
			FieldName: fieldName,
			Column:    ColumnFieldName,
			Op:        OpNotEqual,
			Kind:      PredicateSuppressField,
		})
	}

	return out, nil
}

func isValueColumn(c Column) bool {
	return c == ColumnOldValue || c == ColumnNewValue
}
