package backtest

import (
	"context"
	"fmt"
	"sort"
	"time"

	"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.
type BarSource interface {
	GetBars(ctx context.Context, mkt market.Market, timeframe market.Timeframe, from, to time.Time) ([]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.
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.GetBars(ctx, run.Spec.Market, run.Spec.Timeframe, run.Spec.From, run.Spec.To)
	if err != nil {
		return fmt.Errorf("failed to get bars: %w", err)
	}

	// Ensure chronological sorting of bars
	sort.Slice(bars, func(i, j int) bool {
		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

	for _, bar := range bars {
		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)
		}

		for _, order := range orders {
			fill := backtest.Fill{
				InstrumentID: order.InstrumentID,
				Side:         order.Side,
				Quantity:     order.Quantity,
				Price:        bar.Close,
			}
			nextPortfolio, err := portfolio.ApplyFill(fill)
			if err != nil {
				return fmt.Errorf("failed to apply fill for order %+v: %w", order, err)
			}
			portfolio = nextPortfolio

			// Record trade
			trades = append(trades, backtest.TradeSummary{
				InstrumentID: fill.InstrumentID,
				Side:         fill.Side,
				Quantity:     fill.Quantity,
				Price:        fill.Price,
				Timestamp:    bar.Timestamp,
			})
		}

		// 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)
			}
		}

		// 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
}
