package socket

import (
	"fmt"
	"time"

	altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
	"git.toki-labs.com/toki/alt/packages/domain/backtest"
	"git.toki-labs.com/toki/alt/packages/domain/market"
)

// backtest_mapping.go owns backtest-specific conversions plus shared scalar
// enum helpers. Keeping domain-to-proto mapping in worker socket code (and out
// of the API) honours the rail design where the worker owns persistence shapes
// while the API stays a thin pass-through.

// runSpecFromProto validates an inbound BacktestRunSpec and converts it into the
// worker-owned domain RunSpec. Validation lives next to the conversion so a
// malformed start request never reaches the runner.
func runSpecFromProto(spec *altv1.BacktestRunSpec) (backtest.RunSpec, error) {
	if spec == nil {
		return backtest.RunSpec{}, fmt.Errorf("backtest spec is required")
	}
	if spec.GetStrategyId() == "" {
		return backtest.RunSpec{}, fmt.Errorf("backtest spec: strategy_id is required")
	}

	mkt, err := marketFromProto(spec.GetMarket())
	if err != nil {
		return backtest.RunSpec{}, fmt.Errorf("backtest spec: %w", err)
	}
	timeframe, err := timeframeFromProto(spec.GetTimeframe())
	if err != nil {
		return backtest.RunSpec{}, fmt.Errorf("backtest spec: %w", err)
	}

	fromMs := spec.GetFromUnixMs()
	toMs := spec.GetToUnixMs()
	if fromMs == 0 {
		return backtest.RunSpec{}, fmt.Errorf("backtest spec: from_unix_ms is required")
	}
	if toMs == 0 {
		return backtest.RunSpec{}, fmt.Errorf("backtest spec: to_unix_ms is required")
	}
	from := time.UnixMilli(fromMs).UTC()
	to := time.UnixMilli(toMs).UTC()
	if from.After(to) {
		return backtest.RunSpec{}, fmt.Errorf("backtest spec: from cannot be after to")
	}

	return backtest.RunSpec{
		StrategyID: backtest.StrategyID(spec.GetStrategyId()),
		Market:     mkt,
		Timeframe:  timeframe,
		From:       from,
		To:         to,
	}, nil
}

func marketFromProto(m altv1.Market) (market.Market, error) {
	switch m {
	case altv1.Market_MARKET_KR:
		return market.MarketKR, nil
	case altv1.Market_MARKET_US:
		return market.MarketUS, nil
	default:
		return "", fmt.Errorf("unsupported market %q", m.String())
	}
}

func marketToProto(m market.Market) altv1.Market {
	switch m {
	case market.MarketKR:
		return altv1.Market_MARKET_KR
	case market.MarketUS:
		return altv1.Market_MARKET_US
	default:
		return altv1.Market_MARKET_UNSPECIFIED
	}
}

func timeframeFromProto(t altv1.Timeframe) (market.Timeframe, error) {
	switch t {
	case altv1.Timeframe_TIMEFRAME_DAILY:
		return market.TimeframeDaily, nil
	case altv1.Timeframe_TIMEFRAME_MINUTE_1:
		return market.TimeframeMin1, nil
	case altv1.Timeframe_TIMEFRAME_MINUTE_5:
		return market.TimeframeMin5, nil
	default:
		return "", fmt.Errorf("unsupported timeframe %q", t.String())
	}
}

func timeframeToProto(t market.Timeframe) altv1.Timeframe {
	switch t {
	case market.TimeframeDaily:
		return altv1.Timeframe_TIMEFRAME_DAILY
	case market.TimeframeMin1:
		return altv1.Timeframe_TIMEFRAME_MINUTE_1
	case market.TimeframeMin5:
		return altv1.Timeframe_TIMEFRAME_MINUTE_5
	default:
		return altv1.Timeframe_TIMEFRAME_UNSPECIFIED
	}
}

// runStatusFromProto maps a request status filter to the domain status. The
// unspecified enum maps to an empty status, which the store treats as "list all".
func runStatusFromProto(s altv1.BacktestRunStatus) backtest.RunStatus {
	switch s {
	case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_PENDING:
		return backtest.RunStatusPending
	case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_RUNNING:
		return backtest.RunStatusRunning
	case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_SUCCEEDED:
		return backtest.RunStatusSucceeded
	case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_FAILED:
		return backtest.RunStatusFailed
	case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_CANCELED:
		return backtest.RunStatusCanceled
	default:
		return ""
	}
}

func runStatusToProto(s backtest.RunStatus) altv1.BacktestRunStatus {
	switch s {
	case backtest.RunStatusPending:
		return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_PENDING
	case backtest.RunStatusRunning:
		return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_RUNNING
	case backtest.RunStatusSucceeded:
		return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_SUCCEEDED
	case backtest.RunStatusFailed:
		return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_FAILED
	case backtest.RunStatusCanceled:
		return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_CANCELED
	default:
		return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_UNSPECIFIED
	}
}

func runSpecToProto(spec backtest.RunSpec) *altv1.BacktestRunSpec {
	return &altv1.BacktestRunSpec{
		StrategyId: string(spec.StrategyID),
		Market:     marketToProto(spec.Market),
		Timeframe:  timeframeToProto(spec.Timeframe),
		FromUnixMs: timeToUnixMs(spec.From),
		ToUnixMs:   timeToUnixMs(spec.To),
	}
}

func runToProto(run backtest.Run) *altv1.BacktestRun {
	return &altv1.BacktestRun{
		Id:              string(run.ID),
		Spec:            runSpecToProto(run.Spec),
		Status:          runStatusToProto(run.Status),
		CreatedAtUnixMs: timeToUnixMs(run.CreatedAt),
		UpdatedAtUnixMs: timeToUnixMs(run.UpdatedAt),
	}
}

func runsToProto(runs []backtest.Run) []*altv1.BacktestRun {
	out := make([]*altv1.BacktestRun, len(runs))
	for i, run := range runs {
		out[i] = runToProto(run)
	}
	return out
}

func resultToProto(result backtest.Result) *altv1.BacktestResult {
	return &altv1.BacktestResult{
		RunId:        string(result.RunID),
		StartingCash: priceToProto(result.StartingCash),
		EndingEquity: priceToProto(result.EndingEquity),
		Trades:       tradesToProto(result.Trades),
		Positions:    positionsToProto(result.Positions),
		Summary:      summaryToProto(result.Summary),
		EquityCurve:  equityCurveToProto(result.EquityCurve),
	}
}

func resultsToProto(results []backtest.Result) []*altv1.BacktestResult {
	out := make([]*altv1.BacktestResult, len(results))
	for i, result := range results {
		out[i] = resultToProto(result)
	}
	return out
}

func tradesToProto(trades []backtest.TradeSummary) []*altv1.BacktestTrade {
	if len(trades) == 0 {
		return nil
	}
	out := make([]*altv1.BacktestTrade, len(trades))
	for i, t := range trades {
		out[i] = &altv1.BacktestTrade{
			InstrumentId:    string(t.InstrumentID),
			Side:            string(t.Side),
			Quantity:        quantityToProto(t.Quantity),
			Price:           priceToProto(t.Price),
			TimestampUnixMs: timeToUnixMs(t.Timestamp),
		}
	}
	return out
}

func positionsToProto(positions []backtest.PositionSummary) []*altv1.BacktestPosition {
	if len(positions) == 0 {
		return nil
	}
	out := make([]*altv1.BacktestPosition, len(positions))
	for i, p := range positions {
		out[i] = &altv1.BacktestPosition{
			InstrumentId: string(p.InstrumentID),
			Quantity:     quantityToProto(p.Quantity),
			LastPrice:    priceToProto(p.LastPrice),
		}
	}
	return out
}

func summaryToProto(summary backtest.SummaryMetrics) *altv1.BacktestSummaryMetrics {
	return &altv1.BacktestSummaryMetrics{
		StartingCash: priceToProto(summary.StartingCash),
		EndingEquity: priceToProto(summary.EndingEquity),
		TotalReturn:  decimalToProto(summary.TotalReturn),
		TradeCount:   int32(summary.TradeCount),
	}
}

func equityCurveToProto(points []backtest.EquityPoint) []*altv1.BacktestEquityPoint {
	if len(points) == 0 {
		return nil
	}
	out := make([]*altv1.BacktestEquityPoint, len(points))
	for i, p := range points {
		out[i] = &altv1.BacktestEquityPoint{
			TimestampUnixMs: timeToUnixMs(p.Timestamp),
			Equity:          priceToProto(p.Equity),
		}
	}
	return out
}

func priceToProto(price market.Price) *altv1.Price {
	return &altv1.Price{
		Currency: currencyToProto(price.Currency),
		Amount:   decimalToProto(price.Amount),
	}
}

func quantityToProto(quantity market.Quantity) *altv1.Quantity {
	return &altv1.Quantity{
		Amount: decimalToProto(quantity.Amount),
	}
}

func decimalToProto(decimal market.Decimal) *altv1.Decimal {
	return &altv1.Decimal{Value: decimal.Value}
}

func currencyToProto(c market.Currency) altv1.Currency {
	switch c {
	case market.CurrencyKRW:
		return altv1.Currency_CURRENCY_KRW
	case market.CurrencyUSD:
		return altv1.Currency_CURRENCY_USD
	default:
		return altv1.Currency_CURRENCY_UNSPECIFIED
	}
}

// timeToUnixMs converts a domain timestamp to epoch milliseconds. A zero time
// maps to 0 so empty/unset timestamps stay unambiguous on the wire.
func timeToUnixMs(t time.Time) int64 {
	if t.IsZero() {
		return 0
	}
	return t.UTC().UnixMilli()
}
