package ports

import (
	"context"
	"time"

	"github.com/nexuscrm/kernel/pkg/recordlocks"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

// QueryService defines the interface for analytics querying
type QueryService interface {
	QueryWithSpec(ctx context.Context, req queryspec.QuerySpec, user *models.UserSession) (*models.QueryResult, error)
	QueryWithFilter(ctx context.Context, objectName string, filterExpr string, user *models.UserSession, sortField string, sortDir string, limit int) (*models.QueryResult, error)
	RunAnalytics(ctx context.Context, analyticsQuery models.AnalyticsQuery, currentUser *models.UserSession) (interface{}, error)
	SearchSingleObject(ctx context.Context, objectName string, term string, searchFields []string, returnFields []string, limit int, offset int, orderBy string, orderDir string, filterExpr string, currentUser *models.UserSession) ([]models.SObject, error)
	// SearchSuggestions is the name-prefix autocomplete path behind
	// GET /search/suggestions. It reuses the SearchSingleObject FLS/readable-field
	// pipeline but matches a prefix against index-eligible physical name columns
	// only and reads the object's base table (never the formula view).
	SearchSuggestions(ctx context.Context, objectName string, term string, searchFields []string, returnFields []string, limit int, currentUser *models.UserSession) ([]models.SObject, error)
	GlobalSearch(ctx context.Context, term string, currentUser *models.UserSession) ([]models.SearchResult, error)
	Calculate(ctx context.Context, objectName string, record models.SObject, user *models.UserSession) (models.SObject, error)
	ExecuteRawSQL(ctx context.Context, sql string, params []interface{}, user *models.UserSession) ([]models.SObject, error)
}

// ViewResolver resolves whether to read from a physical table or a formula view.
// FormulaViewService implements this interface.
type ViewResolver interface {
	// ResolveTableName returns v_{object} for objects with active computed
	// fields, otherwise the physical table name.
	ResolveTableName(ctx context.Context, schema *models.ObjectMetadata) string
}

// RollupProcessor defines the interface for rollup summary field calculations
type RollupProcessor interface {
	// ProcessInsertRollups orchestrates rollups for a newly inserted child
	// record and may use insert-only optimizations that are invalid for other
	// mutation types.
	ProcessInsertRollups(ctx context.Context, childObjName string, childRecord models.SObject) error

	// ProcessRollups orchestrates the calculation of rollups for a given child record change
	ProcessRollups(ctx context.Context, childObjName string, childRecord models.SObject) error

	// AffectedParentRows returns the parent rows that ProcessRollups would lock
	// for this child image. Records uses this before acquiring the child row
	// lock so delete/update paths share the same parent-lock order as the
	// rollup engine without duplicating rollup metadata interpretation.
	AffectedParentRows(ctx context.Context, childObjName string, childRecord models.SObject) ([]recordlocks.ParentKey, error)

	// AffectedParentRowsForInsert returns the parent rows that still require a
	// synchronous parent lock for ProcessInsertRollups. Explicit eventual COUNT
	// rollups whose insert delta path is safe are excluded so hot child inserts
	// can enqueue durable rollup work without serializing on the parent row.
	AffectedParentRowsForInsert(ctx context.Context, childObjName string, childRecord models.SObject) ([]recordlocks.ParentKey, error)

	// AffectedParentRowsForUpdate returns the update-specific parent rows that
	// ProcessRollupsForUpdate would lock for the transition from oldRecord to
	// finalRecord. Benign child updates whose changed fields cannot affect any
	// rollup return an empty set; unknown rollup dependencies fail closed by
	// returning the relevant parent rows.
	AffectedParentRowsForUpdate(ctx context.Context, childObjName string, finalRecord, oldRecord models.SObject) ([]recordlocks.ParentKey, error)

	// ProcessRollupsForUpdate handles rollup processing for a child record update
	// that may involve a reparent (parent field change).  It merges the affected
	// rollup sets from both the final and old child images, deduplicates, sorts
	// for deterministic lock acquisition order, and processes each unique parent
	// exactly once.
	ProcessRollupsForUpdate(ctx context.Context, childObjName string, finalRecord, oldRecord models.SObject) error

	// StageRollupCountUpdatePreimage captures, for each COUNT rollup the old
	// child image links, whether the pre-update child row satisfied the rollup
	// filter, and stashes the answers on the returned context. Records calls it
	// after the child row is locked but BEFORE the physical UPDATE, so the
	// committed old row is still readable. ProcessRollupsForUpdate consumes the
	// staged answers to apply ±1 COUNT deltas; without them it falls back to a
	// full recompute. Returns the original ctx unchanged when nothing to stage.
	StageRollupCountUpdatePreimage(ctx context.Context, childObjName string, oldRecord models.SObject) (context.Context, error)

	// ProcessBulkRollups handles rollup processing for a bulk insert of child
	// records.  It receives the full set of valid records and the landing vector
	// (INSERT IGNORE evidence) so that only truly landed rows contribute to
	// parent rollup updates.  The implementation deduplicates affected tuples
	// by (parentObj, parentID, rollupField) before recalculating.
	ProcessBulkRollups(ctx context.Context, childObjName string, childRecords []models.SObject, landedRows []bool) error

	// RecalculateAll batch-recalculates rollup fields for a given parent object,
	// bounded by bounds and resumable through the returned cursor. A zero-value
	// bounds means "walk the whole object in this call", which is what callers
	// that are not serving an HTTP request want; callers behind a request
	// deadline must supply a budget.
	RecalculateAll(ctx context.Context, parentObjName string, bounds RollupRecalculateBounds) (RollupRecalculateProgress, error)
}

// RollupRecalculateBounds bounds one RecalculateAll pass so a caller behind a
// request deadline can walk a large object through several calls. The zero
// value imposes no bound and reproduces the original walk-everything behavior.
type RollupRecalculateBounds struct {
	// AfterID resumes the keyset scan strictly after this parent record Id.
	AfterID string
	// MaxParents stops the pass once this many parents have been recalculated.
	// Zero means no count bound.
	MaxParents int
	// Deadline stops the pass once the clock reaches it. The zero value means no
	// time bound.
	//
	// It is an absolute instant rather than a duration on purpose: a caller that
	// must answer within a request deadline starts the clock when the request
	// arrives, so time the request spends waiting for a lock or a serving fence
	// before this pass begins counts against the same window instead of
	// extending it.
	//
	// The deadline governs whether another parent is started, not when the call
	// returns, so a pass can overrun it by at most one parent's cost.
	Deadline time.Time
}

// RollupRecalculateProgress reports what one bounded RecalculateAll pass did and
// how to continue it.
type RollupRecalculateProgress struct {
	// Recalculated counts parents whose rollups were recomputed and committed.
	Recalculated int
	// NextAfterID is the cursor to resume from. It is empty whenever Complete is
	// true, so a caller cannot be handed a cursor that yields nothing.
	NextAfterID string
	// Complete reports that the scan reached the end of the object, and is the
	// signal a caller should branch on to stop.
	Complete bool
}

// DerivedWriter is the records-side callback that RollupService uses to load
// the internal actor session and write derived (computed) values like rollup
// aggregates back to a parent record without importing the records package
// (PersistenceService already holds a RollupProcessor, so a direct dependency
// would be circular). The implementation lives in records.PersistenceService.
// Wired post-construction in ServiceManager.
//
// LoadSystemUserSession returns the greenfield-safe platform actor used for
// derived writes. It must not require a persisted User row for the synthetic
// system ID; records owns any storage-backed profile/formula facts.
//
// UpdateDerived routes the write through the full save lifecycle (validation
// rules, audit log, outbox) and sets SystemContext on ctx so the write bypasses
// FLS and the intrinsic-readonly gate that ordinarily drops writes to derived
// fields.
//
// ApplyDerivedCountRollupDeltas is a narrower records-owned write path for
// COUNT rollup summary deltas. It is intentionally limited to values produced
// by the rollup engine when a full parent save lifecycle would hold a hot
// parent row lock too long.
//
// EnqueueDerivedCountRollupDeltas records insert-only COUNT dirty markers in
// the same child-write transaction. A background drain worker later serializes
// by (parent object, parent id), reconciles the affected rollup fields from the
// live child row set, and writes one derived parent update transaction. Callers
// must only use it after CanApplyDerivedCountRollupDeltas accepts the parent
// field set.
type DerivedWriter interface {
	LoadSystemUserSession(ctx context.Context) (*models.UserSession, error)
	LockDerivedRecord(ctx context.Context, objectName, recordID string) error
	GetDerivedRecordForUpdate(ctx context.Context, objectName, recordID string) (models.SObject, error)
	UpdateDerived(ctx context.Context, objectName, recordID string, updates models.SObject, currentUser *models.UserSession) error
	UpdateDerivedCountRollupValues(ctx context.Context, objectName, recordID string, rollupFieldNames []string, currentUser *models.UserSession, recalculate func(context.Context) (models.SObject, error)) error
	CanApplyDerivedCountRollupDeltas(ctx context.Context, objectName string, rollupFieldNames ...string) (bool, error)
	CountRollupFieldsDeclareEventual(ctx context.Context, objectName string, rollupFieldNames ...string) (bool, error)
	EnqueueDerivedCountRollupDeltas(ctx context.Context, sourceObjectName, sourceRecordID, parentObjectName, parentRecordID string, deltas map[string]int64) error
	ApplyDerivedCountRollupDeltas(ctx context.Context, objectName, recordID string, deltas map[string]int64, currentUser *models.UserSession) error
}

// DerivedUpdateHistoryPlanner exposes the Records-owned save-lifecycle audit
// closure for a derived update. Rollup planning uses it before the first
// history reservation so metadata-owned system stamps (for example the
// platform modification stamp) and authored derived fields enter one ordered
// attempt plan. It is deliberately optional: non-Records implementations and
// legacy tests retain the existing replay fallback.
type DerivedUpdateHistoryPlanner interface {
	DerivedUpdateHistoryFieldIDs(ctx context.Context, objectName string, fieldNames ...string) ([]string, error)
}
