// Package run persists the durable graph runtime for first-class Runs.
package run

import (
	"context"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/workflow/runlimit"
)

const (
	defaultListLimit = 50
	maxListLimit     = 500
	// MaxRunPayloadBytes bounds each individual input/output/effect/event JSON
	// document. The aggregate reducer state has a separate, larger budget.
	MaxRunPayloadBytes = runlimit.MaxRunPayloadBytes
	maxRuntimeJSON     = MaxRunPayloadBytes
	// MaxRunStateBytes is intentionally larger than a single effect/event
	// payload because the complete reducer snapshot contains Plan input plus
	// many activation inputs/outputs. Runtime rejects larger snapshots as a
	// deterministic resource failure instead of poison-retrying SaveTurn.
	MaxRunStateBytes        = runlimit.MaxRunStateBytes
	MaxRunStateStorageBytes = runlimit.MaxRunStateStorageBytes
	// A reducer snapshot wraps caller values in activation, scope, parallel,
	// and loop envelopes. It therefore needs structural budgets independent
	// from the 32-level/8192-value budget for one caller-authored JSON object.
	// Sixty-four values per maximum activation leaves room for both an
	// Activation and an owned Scope while the aggregate byte cap remains the
	// authoritative memory/wire bound.
	MaxRunStateJSONNesting = runlimit.MaxRunStateJSONNesting
	MaxRunStateJSONValues  = runlimit.MaxRunStateJSONValues
)

var (
	ErrNotFound   = storeerr.ErrNotFound
	ErrConflict   = storeerr.ErrConflict
	ErrBadRequest = storeerr.ErrBadRequest
)

type StoreOptions struct {
	WorkspaceGroupID string
	// TestPreCommitMutationFault, when non-nil, is invoked after a real
	// ResolveAwaitingEffect pre-commit mutation has written through the
	// transaction and immediately before the effect/result rows are committed.
	// It is nil in production; integration tests use it to prove rollback of
	// the message/outbox side effect and durable effect receipt.
	TestPreCommitMutationFault func(context.Context, Run, Activation, Effect) error
	// TestRecoveryTickBarrier, when non-nil, is invoked inside RecoveryTick's
	// transaction after the effect/Run rows are locked FOR UPDATE and before the
	// conditional transition commits. It is nil in production. The committed-DB
	// full-chain fence test uses it to hold the rows deterministically while a
	// concurrent Hub writer proves it cannot cross the machine observation guard,
	// then release and assert the exact durable park/recovery state.
	TestRecoveryTickBarrier func(ctx context.Context) error
	// TestRowLockBarrier, when non-nil, is invoked inside each conflicting
	// operation's transaction after its row-lock/precondition is satisfied and
	// before the write. It is nil in production. The deterministic concurrent
	// two-order matrix uses it to pause the first operation while it holds the
	// row lock, launch the competitor, then release and assert exactly one
	// winner through the production APIs.
	TestRowLockBarrier func(operation string) error
	// TestRowLockAttempting, when non-nil, is invoked at the START of each
	// conflicting operation, BEFORE the transaction acquires the row lock. It is
	// nil in production. The scheduler-independent contention matrix uses it to
	// prove a competitor has reached the pre-conflict boundary and is blocked on
	// the winner's row lock, without a fixed-sleep negative check.
	TestRowLockAttempting func(operation string)
}

type Store struct {
	db               coredb.DB
	workspaceGroupID string
	testBarriers     storeTestBarriers
}

type storeTestBarriers struct {
	preCommitMutation func(context.Context, Run, Activation, Effect) error
	recoveryTick      func(ctx context.Context) error
	rowLock           func(operation string) error
	rowAttempt        func(operation string)
}

type TxStore struct {
	tx coredb.Tx
}

func New(db coredb.DB) *Store { return NewWithOptions(db, StoreOptions{}) }

func NewWithOptions(db coredb.DB, options StoreOptions) *Store {
	return &Store{
		db:               db,
		workspaceGroupID: strings.TrimSpace(options.WorkspaceGroupID),
		testBarriers: storeTestBarriers{
			preCommitMutation: options.TestPreCommitMutationFault,
			recoveryTick:      options.TestRecoveryTickBarrier,
			rowLock:           options.TestRowLockBarrier,
			rowAttempt:        options.TestRowLockAttempting,
		},
	}
}

func (store *Store) Validate() error {
	if store == nil || store.db == nil {
		return fmt.Errorf("workflow Run store database is not configured")
	}
	return nil
}

// runPreCommitMutationFault invokes the test-only fault seam after a real
// transaction-local approval message/outbox mutation and before the durable
// effect/result update. It is inert in production (nil hook), and any error
// deliberately aborts the surrounding transaction.
func (store *Store) runPreCommitMutationFault(ctx context.Context, run Run, activation Activation, effect Effect) error {
	if store.testBarriers.preCommitMutation == nil {
		return nil
	}
	return store.testBarriers.preCommitMutation(ctx, run, activation, effect)
}

// runRowLockBarrier invokes the test-only row-lock barrier for one operation,
// after the operation's row-lock/precondition is satisfied and before its write
// commits. It is inert in production (nil hook). The deterministic concurrent
// two-order matrix uses it to pause the first operation while it holds the row
// lock, launch the competitor, then release and assert exactly one winner.
func (store *Store) runRowLockBarrier(operation string) error {
	if store.testBarriers.rowLock == nil {
		return nil
	}
	return store.testBarriers.rowLock(operation)
}

// runRowLockAttempting signals (test-only, inert in production) that one
// conflicting operation has started and is attempting its row lock. The
// scheduler-independent contention matrix waits for this signal to prove the
// competitor has reached the pre-conflict boundary and is blocked on the winner's
// row lock.
func (store *Store) runRowLockAttempting(operation string) {
	if store.testBarriers.rowAttempt == nil {
		return
	}
	store.testBarriers.rowAttempt(operation)
}

func (store *Store) InTx(tx coredb.Tx) *TxStore { return &TxStore{tx: tx} }

func (store *TxStore) Validate() error {
	if store == nil || store.tx == nil {
		return fmt.Errorf("workflow Run transaction is not configured")
	}
	return nil
}

func boundedLimit(limit int) (int, error) {
	if limit == 0 {
		return defaultListLimit, nil
	}
	if limit < 1 || limit > maxListLimit {
		return 0, storeerr.BadRequest("limit must be between 1 and 500")
	}
	return limit, nil
}
