package jobs

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/backtest"
	"git.toki-labs.com/toki/alt/services/worker/internal/storage"
)

// BacktestStarter turns a start command into a persisted pending run plus an
// asynchronous KindRunBacktest job. It reuses the existing Runner so the rail
// does not introduce a second execution engine: the start path only records the
// pending run and hands execution to the same handler the runner already owns.
type BacktestStarter struct {
	runner *Runner
	store  storage.BacktestRunStore
	newID  func() string
	now    func() time.Time
	// launch runs the execution goroutine. It is injectable so tests can run the
	// job synchronously instead of racing a detached goroutine.
	launch func(func())
}

// BacktestStarterOption customises a BacktestStarter for testing or wiring.
type BacktestStarterOption func(*BacktestStarter)

// WithStarterNow overrides the clock used for run timestamps.
func WithStarterNow(now func() time.Time) BacktestStarterOption {
	return func(s *BacktestStarter) { s.now = now }
}

// WithStarterIDGenerator overrides the run id generator.
func WithStarterIDGenerator(newID func() string) BacktestStarterOption {
	return func(s *BacktestStarter) { s.newID = newID }
}

// WithStarterLaunch overrides how the execution goroutine is launched. Tests use
// this to execute synchronously and observe the runner deterministically.
func WithStarterLaunch(launch func(func())) BacktestStarterOption {
	return func(s *BacktestStarter) { s.launch = launch }
}

// NewBacktestStarter builds a starter bound to the given runner and run store.
func NewBacktestStarter(runner *Runner, store storage.BacktestRunStore, opts ...BacktestStarterOption) *BacktestStarter {
	s := &BacktestStarter{
		runner: runner,
		store:  store,
		newID:  newRunID,
		now:    time.Now,
		launch: func(fn func()) { go fn() },
	}
	for _, opt := range opts {
		opt(s)
	}
	return s
}

// StartBacktest records a pending run for the spec and schedules execution. It
// returns the pending run so the caller can answer the start request immediately
// while the runner finishes the work in the background.
func (s *BacktestStarter) StartBacktest(ctx context.Context, spec backtest.RunSpec) (backtest.Run, error) {
	now := s.now().UTC()
	run := backtest.Run{
		ID:        backtest.RunID(s.newID()),
		Spec:      spec,
		Status:    backtest.RunStatusPending,
		CreatedAt: now,
		UpdatedAt: now,
	}

	if err := s.store.UpsertRun(ctx, run); err != nil {
		return backtest.Run{}, fmt.Errorf("failed to persist pending run: %w", err)
	}

	payload := RunBacktestPayload{
		RunID:      string(run.ID),
		StrategyID: string(spec.StrategyID),
		Market:     string(spec.Market),
		Timeframe:  string(spec.Timeframe),
		From:       spec.From.UTC().Format(time.RFC3339),
		To:         spec.To.UTC().Format(time.RFC3339),
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		return backtest.Run{}, fmt.Errorf("failed to encode run backtest payload: %w", err)
	}

	job := Job{
		ID:      string(run.ID),
		Kind:    KindRunBacktest,
		Status:  StatusPending,
		Payload: raw,
	}

	// Execution is detached from the request lifecycle: the start response must
	// not block on a long-running backtest, so the goroutine uses a background
	// context instead of the request context.
	s.launch(func() {
		_ = s.runner.Execute(context.Background(), job)
	})

	return run, nil
}

// newRunID returns a random hex run identifier. crypto/rand keeps it collision
// resistant without pulling an external uuid dependency into the worker module.
func newRunID() string {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		// rand.Read failures are practically impossible; fall back to a
		// timestamp-based id so a start never panics.
		return fmt.Sprintf("run-%d", time.Now().UTC().UnixNano())
	}
	return "run-" + hex.EncodeToString(b[:])
}
