// Package audit — history_soql.go implements the §5.1 SOQL translator for
// `<Object>History` pseudo-objects.
//
// Design (§5.1): `<Object>History` is not a real table. It is a virtual view
// over _System_AuditLog filtered by `ObjectApiName = '<Object>'`. When the
// SOQL dispatcher sees `FROM AccountHistory`, it rewrites the query:
//
//   SELECT Id, Field, OldValue, NewValue, CreatedDate, CreatedById
//   FROM AccountHistory WHERE ParentId = 'xxx' ORDER BY CreatedDate DESC
//
// becomes
//
//   SELECT Id, FieldName AS Field, OldValue, NewValue,
//          ChangedAt AS CreatedDate, ChangedById AS CreatedById
//   FROM _System_AuditLog
//   WHERE ObjectApiName = 'Account' AND RecordId = 'xxx'
//     AND FieldName <> '__record__'            -- hide head rows
//     AND <FLS suppression predicates>
//   ORDER BY ChangedAt DESC
//
// This file owns three things:
//  1. Detecting the `<Object>History` suffix and stripping it.
//  2. Building a HistoryQueryPlan from the parsed SOQL (SF field names on
//     the left, AuditLog columns on the right).
//  3. Rendering the post-FLS plan to a SQL string + bind args.
//
// FLS gate (§4.1 CH-6): we ALWAYS call RedactFieldHistoryPlan before
// rendering. A predicate on a denied field's value column surfaces as
// *FieldDeniedByFLSError (HTTP 400). A projection on a denied field is
// silently dropped and a suppression predicate `FieldName <> X` is added.
//
// This file is parser-adjacent but NOT a parser: callers hand us the
// already-parsed SOQL request (api.QueryRequest shape) plus the object
// name (with "History" suffix still attached) and the active user.
package audit

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
)

// HistorySuffix is the constant <Object>History objects end with.
const HistorySuffix = "History"

// HistoryRow is one row returned by the <Object>History virtual view.
// Field names match the Salesforce history-object contract (§5.1).
type HistoryRow struct {
	ID          string
	ParentID    string
	Field       string
	OldValue    *string
	NewValue    *string
	DataType    string // stubbed as "string" in Phase 2A
	CreatedDate string // ISO8601
	CreatedByID string
}

// HistorySOQLInput is the minimal post-parse shape the translator needs
// from the upstream SOQL parser. Using a local struct keeps this package
// independent of shared/pkg/api (which would introduce a dependency cycle
// for tests).
type HistorySOQLInput struct {
	// ObjectAPIName still includes the "History" suffix (e.g. "AccountHistory").
	ObjectAPIName string
	// SelectFields are the user-requested columns in SF names.
	SelectFields []string
	// ParentID is the value the user filtered on via WHERE ParentId = '...'.
	// Phase 2A only supports this single filter; richer WHERE translation
	// lands with broader predicate support.
	ParentID string
	// Limit / Offset mirror the parsed SOQL values (0 means "no override").
	Limit  int
	Offset int
	// SortDesc selects ChangedAt DESC (true) or ASC (false). Default: DESC.
	SortDesc bool
}

// IsHistoryObject reports whether an API object name references the
// <Object>History virtual view.
func IsHistoryObject(name string) bool {
	return strings.HasSuffix(name, HistorySuffix) && len(name) > len(HistorySuffix)
}

// ParentObjectFromHistoryName strips the "History" suffix.
// Pre: IsHistoryObject(name) is true.
func ParentObjectFromHistoryName(name string) string {
	return strings.TrimSuffix(name, HistorySuffix)
}

// BuildHistoryPlan converts an upstream SOQL input into a HistoryQueryPlan
// ready for RedactFieldHistoryPlan. Projection SF names (Field, OldValue,
// NewValue, CreatedDate, CreatedById, Id, ParentId, DataType) are accepted
// case-insensitively; unknown SF names are passed through as field-name
// references so the redactor can gate on them.
//
// Predicate support in Phase 2A is deliberately minimal — only
// `ParentId = '...'` is surfaced (via HistorySOQLInput.ParentID); other
// predicates are ignored at the plan layer. Expanding this is a follow-up.
func BuildHistoryPlan(in HistorySOQLInput) (HistoryQueryPlan, error) {
	if !IsHistoryObject(in.ObjectAPIName) {
		return HistoryQueryPlan{}, fmt.Errorf("audit: %q is not a History object", in.ObjectAPIName)
	}
	parent := ParentObjectFromHistoryName(in.ObjectAPIName)
	plan := HistoryQueryPlan{
		ObjectApiName: parent,
		Projections:   make([]FieldRef, 0, len(in.SelectFields)),
	}
	for _, sel := range in.SelectFields {
		ref := projectionRef(sel)
		plan.Projections = append(plan.Projections, ref)
	}
	// Phase 2A: the only supported user predicate is ParentId = '...'. That
	// maps to AuditLog.RecordId, which is metadata (not FLS-gated), so we
	// do not need to surface it in Predicates for the redactor — it is
	// applied at render time below.
	return plan, nil
}

// projectionRef maps a SF history column name to a FieldRef the redactor
// can reason about. Metadata columns (Field, Id, ParentId, DataType,
// CreatedDate, CreatedById) produce FieldRef with an empty FieldName so
// the redactor treats them as metadata and never drops them. Value
// columns (OldValue, NewValue) preserve an empty FieldName here because
// the SF history contract does not name the field in the projection —
// the FieldName column IS what identifies which field each row is for,
// so the redactor's FLS gate is enforced via the FieldName suppression
// predicate rather than the projection.
func projectionRef(sel string) FieldRef {
	switch strings.ToLower(strings.TrimSpace(sel)) {
	case "oldvalue":
		return FieldRef{Column: ColumnOldValue}
	case "newvalue":
		return FieldRef{Column: ColumnNewValue}
	case "field", "fieldname":
		return FieldRef{Column: ColumnFieldName}
	case "createddate":
		return FieldRef{Column: ColumnChangedAt}
	case "createdbyid":
		return FieldRef{Column: ColumnChangedById}
	default:
		// Id / ParentId / DataType and anything else — treat as metadata
		// projection so it survives redaction. The SQL renderer maps the
		// SF name to its AuditLog column.
		return FieldRef{Column: Column(sel)}
	}
}

// ExecuteHistoryPlan renders the post-redaction plan to SQL and executes
// it against _System_AuditLog via the provided *sql.DB. Returns the rows
// as typed HistoryRow values.
//
// The head-row sentinel ("__record__") is always filtered out so callers
// only see field-level diffs. If len(plan.DeniedFields) > 0 we inject the
// FieldName suppression predicates generated by RedactFieldHistoryPlan.
//
// Error messages MUST NOT echo the user-supplied predicate value (§5.1 +
// fls_plan rule) — we only surface the field name, never the filter value.
func ExecuteHistoryPlan(
	ctx context.Context,
	db *sql.DB,
	plan HistoryQueryPlan,
	parentRecordID string,
	limit, offset int,
	sortDesc bool,
) ([]HistoryRow, error) {
	if db == nil {
		return nil, fmt.Errorf("audit: ExecuteHistoryPlan requires a non-nil *sql.DB")
	}
	if plan.ObjectApiName == "" {
		return nil, fmt.Errorf("audit: plan.ObjectApiName is required")
	}

	var (
		where = []string{
			"ObjectApiName = ?",
			"FieldName <> ?",
		}
		args = []interface{}{plan.ObjectApiName, headFieldNameSentinel}
	)
	if parentRecordID != "" {
		where = append(where, "RecordId = ?")
		args = append(args, parentRecordID)
	}
	// FLS suppression: for every denied field, add `FieldName <> ?` so no
	// rows for that field surface.
	for _, p := range plan.Predicates {
		if p.Kind != PredicateSuppressField {
			continue
		}
		where = append(where, "FieldName <> ?")
		args = append(args, p.FieldName)
	}

	orderDir := "DESC"
	if !sortDesc {
		orderDir = "ASC"
	}

	q := fmt.Sprintf(
		`SELECT Id, RecordId, FieldName, OldValue, NewValue, ChangedAt, ChangedById
		 FROM _System_AuditLog
		 WHERE %s
		 ORDER BY ChangedAt %s`,
		strings.Join(where, " AND "), orderDir,
	)
	if limit > 0 {
		q += fmt.Sprintf(" LIMIT %d", limit)
		if offset > 0 {
			q += fmt.Sprintf(" OFFSET %d", offset)
		}
	}

	rows, err := db.QueryContext(ctx, q, args...)
	if err != nil {
		// Do NOT wrap with args — any user-supplied predicate literal
		// would leak through error logs otherwise.
		return nil, fmt.Errorf("audit: history query: %w", err)
	}
	defer rows.Close()

	out := make([]HistoryRow, 0, 32)
	for rows.Next() {
		var (
			r                HistoryRow
			old, nw          sql.NullString
			changedAt        sql.NullTime
			changedByID      sql.NullString
			recordID, field  sql.NullString
			idCol            sql.NullString
		)
		if err := rows.Scan(&idCol, &recordID, &field, &old, &nw, &changedAt, &changedByID); err != nil {
			return nil, fmt.Errorf("audit: history scan: %w", err)
		}
		r.ID = idCol.String
		r.ParentID = recordID.String
		r.Field = field.String
		if old.Valid {
			v := old.String
			r.OldValue = &v
		}
		if nw.Valid {
			v := nw.String
			r.NewValue = &v
		}
		if changedAt.Valid {
			r.CreatedDate = changedAt.Time.UTC().Format("2006-01-02T15:04:05.000Z")
		}
		r.CreatedByID = changedByID.String
		r.DataType = "string" // Phase 2A stub per loop-prompt §5.1
		out = append(out, r)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("audit: history rows: %w", err)
	}
	return out, nil
}
