package contract

import "context"

// RollupMaintenanceServingAttestationProtocol identifies the forward-only
// neutral host protocol required by a dialect that invokes rollup maintenance.
// The version belongs to the contract rather than either HTTP field name: L1
// owns transport syntax, while L0 owns the request carrier and mutation fence.
const (
	RollupMaintenanceServingAttestationProtocolID    = "nexuscrm.rollup-maintenance.serving-attestation.v1"
	RollupMaintenanceServingAttestationProtocolMajor = 1
	RollupMaintenanceServingAttestationProtocolMinor = 0
)

// RollupMaintenance is the neutral rollup-summary recalculation surface.
type RollupMaintenance interface {
	RecalculateRollups(ctx context.Context, request RollupRecalculateRequest) (*RollupRecalculateResult, error)
}

// RollupRecalculateRequest binds a mutating recalculation to the exact server
// process and active app-pack state that the operator attested through health.
// API dialects own the wire header; the platform owns validating it inside the
// mutation fence.
type RollupRecalculateRequest struct {
	Object                     string
	ExpectedServingAttestation string

	// AfterID resumes the keyset scan strictly after this parent record Id.
	// Empty starts at the beginning of the object.
	//
	// The server does not and cannot validate that a cursor came from a scan of
	// this same object: it is compared directly against record Ids, and
	// ExpectedServingAttestation attests the serving process and app-pack state,
	// not the scan. Passing a cursor obtained from a different object therefore
	// skips every parent sorting below it and still reports a complete scan.
	// Drivers must keep one cursor per object.
	AfterID string

	// MaxParents optionally bounds one request to at most N parents. Zero means
	// no count bound. A time bound, when the server applies one, is independent
	// and can end a page before this count is reached.
	MaxParents int
}

type RollupRecalculateResult struct {
	Object string

	// Recalculated counts the parents recomputed by THIS request, not by the
	// scan as a whole. Summing it across a cursor loop gives the object total.
	Recalculated int

	// NextAfterID is the cursor to pass back as AfterID to continue this scan.
	// The server guarantees it is empty whenever Complete is true, so a driver
	// is never handed a cursor that would yield nothing.
	NextAfterID string

	// Complete reports that the scan reached the end of the object from the
	// AfterID this request supplied, and is the termination signal a driver
	// should branch on because it says so explicitly.
	//
	// Complete is not a statement about whole-object coverage. A driver that
	// starts from a non-empty AfterID and stops at Complete has covered only
	// the tail; only a loop that starts from an empty AfterID and runs to
	// Complete has visited the whole object.
	//
	// Coverage: a cursor-driven scan visits every parent that existed and
	// sorted after the cursor at the time each page ran. A parent inserted
	// mid-scan whose Id sorts below the current cursor is not visited. That is
	// accepted for this endpoint, whose purpose is healing historical drift —
	// records written while the scan runs get their rollups on the write path.
	//
	// The fence is likewise per-page: each request is bound to the serving
	// state its own ExpectedServingAttestation attests, not the whole scan. A
	// serving change mid-scan fails the next page's precondition and the
	// operator restarts, rather than silently continuing across a state change.
	Complete bool
}

type RollupMaintenanceErrorKind string

const (
	RollupMaintenanceErrorUnavailable          RollupMaintenanceErrorKind = "unavailable"
	RollupMaintenanceErrorNotFound             RollupMaintenanceErrorKind = "not_found"
	RollupMaintenanceErrorValidation           RollupMaintenanceErrorKind = "validation"
	RollupMaintenanceErrorValidationRule       RollupMaintenanceErrorKind = "validation_rule"
	RollupMaintenanceErrorFieldDenied          RollupMaintenanceErrorKind = "field_denied"
	RollupMaintenanceErrorPermission           RollupMaintenanceErrorKind = "permission"
	RollupMaintenanceErrorLockContention       RollupMaintenanceErrorKind = "lock_contention"
	RollupMaintenanceErrorDomainWire           RollupMaintenanceErrorKind = "domain_wire"
	RollupMaintenanceErrorInternal             RollupMaintenanceErrorKind = "internal"
	RollupMaintenanceErrorInvalidRequest       RollupMaintenanceErrorKind = "invalid_request"
	RollupMaintenanceErrorPreconditionRequired RollupMaintenanceErrorKind = "precondition_required"
	RollupMaintenanceErrorPreconditionFailed   RollupMaintenanceErrorKind = "precondition_failed"
)

// RollupMaintenanceError carries rollup admin route classifications across the
// neutral seam without exposing backend error types to the L1 module.
type RollupMaintenanceError struct {
	Kind    RollupMaintenanceErrorKind
	Message string
	Fields  []string
	Cause   error
}

func (e *RollupMaintenanceError) Error() string {
	if e == nil {
		return ""
	}
	if e.Message != "" {
		return e.Message
	}
	return string(e.Kind)
}

func (e *RollupMaintenanceError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Cause
}
