package backtest

import (
	"context"
	"fmt"
	"sort"

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

// BarSource resolves daily bars for a backtest run. It receives the full RunSpec
// so input selectors (instrument ids / symbols) reach the storage boundary and
// the engine only iterates the bars the run actually selected.
type BarSource interface {
	GetBarsForRun(ctx context.Context, spec backtest.RunSpec) ([]market.Bar, error)
}

// StrategyPort resolves strategies for a backtest run.
type StrategyPort interface {
	GetStrategy(ctx context.Context, id backtest.StrategyID) (backtest.Strategy, error)
}

// Engine implements the jobs.BacktestExecutor interface and runs the backtest strategy loop.
type Engine struct {
	barSource    BarSource
	strategyPort StrategyPort
	resultStore  storage.BacktestResultStore
}

// NewEngine creates a new backtest Engine instance.
func NewEngine(barSource BarSource, strategyPort StrategyPort, resultStore storage.BacktestResultStore) *Engine {
	return &Engine{
		barSource:    barSource,
		strategyPort: strategyPort,
		resultStore:  resultStore,
	}
}

// Execute runs the backtest strategy loop.
//
// Fill policy: orders generated by strategy.Decide are held as pending and
// executed on the *next* available bar for the same instrument using
// backtest.FillOrderOnBar.  This provides deterministic next-bar OHLC fill
// semantics that are consistent across monthly/daily/minute timeframes.
// Unfilled pending orders at the end of the bar series are discarded.
func (e *Engine) Execute(ctx context.Context, run backtest.Run) error {
	strategy, err := e.strategyPort.GetStrategy(ctx, run.Spec.StrategyID)
	if err != nil {
		return fmt.Errorf("failed to get strategy %s: %w", run.Spec.StrategyID, err)
	}

	bars, err := e.barSource.GetBarsForRun(ctx, run.Spec)
	if err != nil {
		return fmt.Errorf("failed to get bars: %w", err)
	}

	sort.Slice(bars, func(i, j int) bool {
		if bars[i].Timestamp.Equal(bars[j].Timestamp) {
			return bars[i].InstrumentID < bars[j].InstrumentID
		}
		return bars[i].Timestamp.Before(bars[j].Timestamp)
	})

	var startingCash market.Price
	if run.Spec.Market == market.MarketUS {
		startingCash = market.Price{
			Currency: market.CurrencyUSD,
			Amount:   market.Decimal{Value: "10000"},
		}
	} else {
		startingCash = market.Price{
			Currency: market.CurrencyKRW,
			Amount:   market.Decimal{Value: "10000000"},
		}
	}

	portfolio := backtest.NewPortfolioState(startingCash)
	var history []market.Bar
	var trades []backtest.TradeSummary
	var equityCurve []backtest.EquityPoint

	// pendingOrders maps instrumentID -> OrderIntent accumulated since the last
	// bar for that instrument.  On the next available bar for the same
	// instrument these orders are executed using FillOrderOnBar.
	pendingOrders := make(map[market.InstrumentID][]backtest.OrderIntent)

	for _, bar := range bars {
		instID := bar.InstrumentID

		// 1) Execute pending orders for this instrument on the next available bar.
		if orders, ok := pendingOrders[instID]; ok {
			// Clear this bucket before processing so unfilled orders can be
			// reinserted without mixing with newly generated orders.
			delete(pendingOrders, instID)

			var nextBarPending []backtest.OrderIntent
			for _, order := range orders {
				fill, ok, err := backtest.FillOrderOnBar(order, bar)
				if err != nil {
					return fmt.Errorf("failed to fill order for instrument %s: %w", instID, err)
				}
				if !ok {
					// Order not fillable (e.g. limit not crossed) - keep pending for future bars.
					nextBarPending = append(nextBarPending, order)
					continue
				}
				nextPortfolio, err := portfolio.ApplyFill(fill)
				if err != nil {
					return fmt.Errorf("failed to apply fill for order %+v: %w", order, err)
				}
				portfolio = nextPortfolio

				trades = append(trades, backtest.TradeSummary{
					InstrumentID: fill.InstrumentID,
					Side:         fill.Side,
					Quantity:     fill.Quantity,
					Price:        fill.Price,
					Timestamp:    fill.Timestamp,
				})
			}
			// Re-insert orders that did not fill this bar.
			if len(nextBarPending) > 0 {
				pendingOrders[instID] = nextBarPending
			}
		}

		// 2) Generate strategy decision for the current bar.
		input := backtest.StrategyInput{
			Run:       run,
			Bar:       bar,
			Portfolio: portfolio,
			History:   history,
		}

		orders, err := strategy.Decide(input)
		if err != nil {
			return fmt.Errorf("strategy decide failed for bar at %s: %w", bar.Timestamp, err)
		}

		// 3) Queue orders for next-bar execution, keyed by order.InstrumentID.
		for _, order := range orders {
			if order.InstrumentID == "" {
				return fmt.Errorf("strategy returned order with empty InstrumentID at bar %s", bar.Timestamp)
			}
			pendingOrders[order.InstrumentID] = append(pendingOrders[order.InstrumentID], order)
		}

		// 4) Update mark price of any open position for the current bar.
		if _, ok := portfolio.Position(bar.InstrumentID); ok {
			portfolio, err = portfolio.MarkPrice(bar.InstrumentID, bar.Close)
			if err != nil {
				return fmt.Errorf("failed to mark price for instrument %s: %w", bar.InstrumentID, err)
			}
		}

		// 5) Record one equity point per processed bar after marking the current close
		// so the curve is deterministic and ordered by bar timestamp.
		equity, err := portfolio.Equity()
		if err != nil {
			return fmt.Errorf("failed to calculate equity at %s: %w", bar.Timestamp, err)
		}
		equityCurve = append(equityCurve, backtest.EquityPoint{Timestamp: bar.Timestamp, Equity: equity})

		history = append(history, bar)
	}

	// Calculate ending equity
	endingEquity, err := portfolio.Equity()
	if err != nil {
		return fmt.Errorf("failed to calculate ending equity: %w", err)
	}

	// Collect positions
	var positions []backtest.PositionSummary
	for instID, pos := range portfolio.Positions {
		positions = append(positions, backtest.PositionSummary{
			InstrumentID: instID,
			Quantity:     pos.Quantity,
			LastPrice:    pos.LastPrice,
		})
	}
	// Sort positions alphabetically by InstrumentID for determinism
	sort.Slice(positions, func(i, j int) bool {
		return positions[i].InstrumentID < positions[j].InstrumentID
	})

	result := backtest.Result{
		RunID:        run.ID,
		StartingCash: startingCash,
		EndingEquity: endingEquity,
		Trades:       trades,
		Positions:    positions,
		Summary: backtest.SummaryMetrics{
			StartingCash: startingCash,
			EndingEquity: endingEquity,
			TotalReturn:  backtest.TotalReturn(startingCash, endingEquity),
			TradeCount:   len(trades),
		},
		EquityCurve: equityCurve,
	}

	if e.resultStore != nil {
		if err := e.resultStore.UpsertResult(ctx, result); err != nil {
			return fmt.Errorf("failed to upsert result: %w", err)
		}
	}

	return nil
}
