package papertrading

import (
	"context"
	"errors"
	"fmt"
	"math/big"
	"sort"
	"strings"
	"sync"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/backtest"
	"git.toki-labs.com/toki/alt/packages/domain/market"
)

// ErrAccountNotFound is returned when a paper trading state is requested for an
// account that has not been started in this process.
var ErrAccountNotFound = errors.New("paper trading account not found")

// Order lifecycle sentinels. They let the socket layer map a lifecycle failure
// onto the shared typed error vocabulary: a missing order is not_found, an
// out-of-sequence transition or a missing market fill price is invalid_request.
var (
	// ErrOrderNotFound is returned when a submit/cancel/fill targets an order id
	// that was never recorded for the account.
	ErrOrderNotFound = errors.New("paper order not found")
	// ErrOrderNotPending is returned when cancel/fill targets an order that has
	// already reached a terminal status (filled, canceled, rejected).
	ErrOrderNotPending = errors.New("paper order is not pending")
	// ErrFillPriceRequired is returned when a market order is filled without a
	// simulated execution price; a market order has no inherent price.
	ErrFillPriceRequired = errors.New("fill_price is required for a market order")
	// ErrInvalidOrderInput is returned when an order lifecycle decimal
	// (quantity, limit price, fill price) is non-numeric or has the wrong sign.
	// The socket layer maps it to invalid_request so malformed input is rejected
	// before it can be stored as a pending order or applied as a fill.
	ErrInvalidOrderInput = errors.New("invalid paper order input")
)

// StartRequest carries the validated inputs needed to start a paper run for an
// account. The socket layer maps the proto request onto this shape so the
// service stays decoupled from the wire contract.
type StartRequest struct {
	AccountID    backtest.PaperAccountID
	Spec         backtest.RunSpec
	StartingCash market.Price
}

// State is the terminal snapshot of a paper account after a run. Positions are
// kept as a stable, instrument-sorted slice so socket output is deterministic.
type State struct {
	Run         backtest.Run
	Cash        market.Price
	Positions   []backtest.Position
	Fills       []backtest.Fill
	EquityCurve []backtest.EquityPoint
	// Rejected carries orders the run's risk gate or portfolio refused, preserved
	// from the engine snapshot so the socket layer can expose a risk summary
	// without re-running risk evaluation.
	Rejected []RejectedOrder
}

// Service is the worker-owned, process-local paper trading runtime. It runs the
// daily paper Engine on start and keeps the resulting state in memory keyed by
// account id so the operator can inspect it through the API/CLI without a
// durable store. Paper readiness is intentionally in-memory: the milestone only
// needs deterministic start/inspect, not persistence.
type Service struct {
	engine *Engine
	now    func() time.Time

	mu     sync.Mutex
	states map[backtest.PaperAccountID]State
	// books holds operator-submitted order lifecycle state per account. It is
	// seeded lazily from a started account's terminal run state on first submit,
	// so the manual order path continues from where the run left off without a
	// durable store.
	books map[backtest.PaperAccountID]*orderBook
}

// NewService builds a paper trading service around an Engine. now defaults to
// time.Now when nil so callers can inject a clock in tests.
func NewService(engine *Engine, now func() time.Time) *Service {
	if now == nil {
		now = time.Now
	}
	return &Service{
		engine: engine,
		now:    now,
		states: make(map[backtest.PaperAccountID]State),
		books:  make(map[backtest.PaperAccountID]*orderBook),
	}
}

// StartPaperTrading runs a paper simulation for the request and records the
// terminal state under the account id, replacing any earlier state for that
// account. The run is executed synchronously: unlike a backtest it is a bounded
// daily replay over already-imported bars, so the operator gets the terminal
// account state in the start response.
func (s *Service) StartPaperTrading(ctx context.Context, req StartRequest) (State, error) {
	if s == nil || s.engine == nil {
		return State{}, fmt.Errorf("paper trading engine is not configured")
	}
	if req.AccountID == "" {
		return State{}, fmt.Errorf("account_id is required")
	}

	now := s.now().UTC()
	run := backtest.Run{
		ID:        backtest.RunID(fmt.Sprintf("paper-%s-%d", req.AccountID, now.UnixNano())),
		Spec:      req.Spec,
		Status:    backtest.RunStatusRunning,
		CreatedAt: now,
		UpdatedAt: now,
	}
	account := backtest.PaperAccount{
		ID:           req.AccountID,
		Portfolio:    backtest.NewPortfolioState(req.StartingCash),
		UpdatedAt:    now,
		FillPolicy:   backtest.FillPolicyDailyNextBarOHLC,
		RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
	}

	snap, err := s.engine.Run(ctx, RunRequest{Account: account, Run: run})
	if err != nil {
		return State{}, fmt.Errorf("paper run failed: %w", err)
	}

	run.Status = backtest.RunStatusSucceeded
	run.UpdatedAt = s.now().UTC()
	state := stateFromSnapshot(run, snap)

	s.mu.Lock()
	s.states[req.AccountID] = state
	// Restarting an account discards any prior order book so the new run state is
	// not shadowed by a stale manual portfolio/order sequence. Without this,
	// GetPaperTradingState would keep returning the old order-book snapshot and
	// subsequent orders would continue the old id sequence.
	delete(s.books, req.AccountID)
	s.mu.Unlock()

	return state, nil
}

// GetPaperTradingState returns the recorded state for an account or
// ErrAccountNotFound when the account was never started in this process. Once an
// order book exists for the account (the operator submitted virtual orders), the
// live order-book snapshot is returned so the account view reflects manual fills
// instead of the stale terminal run state.
func (s *Service) GetPaperTradingState(ctx context.Context, accountID backtest.PaperAccountID) (State, error) {
	if accountID == "" {
		return State{}, fmt.Errorf("account_id is required")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if book, ok := s.books[accountID]; ok {
		return book.snapshot(), nil
	}
	state, ok := s.states[accountID]
	if !ok {
		return State{}, ErrAccountNotFound
	}
	return state, nil
}

// stateFromSnapshot flattens the engine snapshot into the inspectable State,
// sorting positions by instrument id for stable output.
func stateFromSnapshot(run backtest.Run, snap *Snapshot) State {
	positions := make([]backtest.Position, 0, len(snap.Account.Portfolio.Positions))
	for _, p := range snap.Account.Portfolio.Positions {
		positions = append(positions, p)
	}
	sort.Slice(positions, func(i, j int) bool {
		return positions[i].InstrumentID < positions[j].InstrumentID
	})
	return State{
		Run:         run,
		Cash:        snap.Account.Portfolio.Cash,
		Positions:   positions,
		Fills:       snap.Fills,
		EquityCurve: snap.EquityCurve,
		Rejected:    snap.Rejected,
	}
}

// PaperOrderStatus is the lifecycle status of an operator-submitted virtual
// order. The set is closed: an order starts pending and moves to exactly one of
// the terminal statuses.
type PaperOrderStatus string

const (
	PaperOrderStatusPending  PaperOrderStatus = "pending"
	PaperOrderStatusFilled   PaperOrderStatus = "filled"
	PaperOrderStatusCanceled PaperOrderStatus = "canceled"
	PaperOrderStatusRejected PaperOrderStatus = "rejected"
)

// PaperOrder is a single virtual order tracked in memory against an account. It
// is paper-only: no real broker order is created, and fills are simulated
// against the account's in-memory portfolio.
type PaperOrder struct {
	OrderID   string
	AccountID backtest.PaperAccountID
	Intent    backtest.OrderIntent
	Status    PaperOrderStatus
	// Reason carries the risk/portfolio denial cause when Status is rejected.
	Reason string
	// Fill is set when Status is filled.
	Fill      *backtest.Fill
	CreatedAt time.Time
	UpdatedAt time.Time
}

// SubmitOrderRequest carries a validated order intent to record against an
// account. The socket layer maps the proto request onto this shape so the
// service stays decoupled from the wire contract.
type SubmitOrderRequest struct {
	AccountID backtest.PaperAccountID
	Intent    backtest.OrderIntent
}

// orderBook is the per-account, in-memory lifecycle state. It holds a live
// portfolio that manual fills mutate, the seed run state for the unchanged
// fields (equity curve, run, risk rejections), the manual fills applied so far,
// and the recorded orders keyed by order id.
type orderBook struct {
	seed        State
	portfolio   backtest.PortfolioState
	risk        backtest.RiskSettings
	manualFills []backtest.Fill
	seq         int
	orders      map[string]*PaperOrder
}

// snapshot renders the live order book as an inspectable State. Cash, positions
// and the cumulative fill list reflect manual fills, while equity curve and risk
// rejections are carried unchanged from the seed run.
func (b *orderBook) snapshot() State {
	positions := make([]backtest.Position, 0, len(b.portfolio.Positions))
	for _, p := range b.portfolio.Positions {
		positions = append(positions, p)
	}
	sort.Slice(positions, func(i, j int) bool {
		return positions[i].InstrumentID < positions[j].InstrumentID
	})
	fills := make([]backtest.Fill, 0, len(b.seed.Fills)+len(b.manualFills))
	fills = append(fills, b.seed.Fills...)
	fills = append(fills, b.manualFills...)
	return State{
		Run:         b.seed.Run,
		Cash:        b.portfolio.Cash,
		Positions:   positions,
		Fills:       fills,
		EquityCurve: b.seed.EquityCurve,
		Rejected:    b.seed.Rejected,
	}
}

// orderBookLocked returns the account's order book, seeding it from the recorded
// run state on first access. It must be called with s.mu held.
func (s *Service) orderBookLocked(accountID backtest.PaperAccountID) (*orderBook, error) {
	if book, ok := s.books[accountID]; ok {
		return book, nil
	}
	seed, ok := s.states[accountID]
	if !ok {
		return nil, ErrAccountNotFound
	}
	positions := make(map[market.InstrumentID]backtest.Position, len(seed.Positions))
	for _, p := range seed.Positions {
		positions[p.InstrumentID] = p
	}
	book := &orderBook{
		seed:      seed,
		portfolio: backtest.PortfolioState{Cash: seed.Cash, Positions: positions},
		// Paper risk mirrors StartPaperTrading: short selling stays disabled so a
		// sell without an open position is blocked rather than silently shorting.
		risk:   backtest.RiskSettings{AllowShortSelling: false},
		orders: make(map[string]*PaperOrder),
	}
	s.books[accountID] = book
	return book, nil
}

// SubmitPaperOrder records a new pending virtual order for the account. The
// account must already have been started; the order book is seeded from its run
// state. The intent is expected to be pre-validated by the socket mapping, but a
// defensive check keeps the registry from storing a malformed order.
func (s *Service) SubmitPaperOrder(ctx context.Context, req SubmitOrderRequest) (PaperOrder, error) {
	if req.AccountID == "" {
		return PaperOrder{}, fmt.Errorf("account_id is required")
	}
	if err := validateOrderIntent(req.Intent); err != nil {
		return PaperOrder{}, err
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	book, err := s.orderBookLocked(req.AccountID)
	if err != nil {
		return PaperOrder{}, err
	}

	book.seq++
	now := s.now().UTC()
	order := &PaperOrder{
		OrderID:   fmt.Sprintf("paper-order-%s-%d", req.AccountID, book.seq),
		AccountID: req.AccountID,
		Intent:    req.Intent,
		Status:    PaperOrderStatusPending,
		CreatedAt: now,
		UpdatedAt: now,
	}
	book.orders[order.OrderID] = order
	return *order, nil
}

// CancelPaperOrder transitions a pending order to canceled. A missing order is
// ErrOrderNotFound; an already-terminal order is ErrOrderNotPending.
func (s *Service) CancelPaperOrder(ctx context.Context, accountID backtest.PaperAccountID, orderID string) (PaperOrder, error) {
	if accountID == "" {
		return PaperOrder{}, fmt.Errorf("account_id is required")
	}
	if orderID == "" {
		return PaperOrder{}, fmt.Errorf("order_id is required")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	order, err := s.findOrderLocked(accountID, orderID)
	if err != nil {
		return PaperOrder{}, err
	}
	if order.Status != PaperOrderStatusPending {
		return PaperOrder{}, ErrOrderNotPending
	}
	order.Status = PaperOrderStatusCanceled
	order.UpdatedAt = s.now().UTC()
	return *order, nil
}

// FillPaperOrder simulates the execution of a pending order against the account
// portfolio and returns the resulting order plus the post-fill account state.
//
// Request-level problems (missing account/order, non-pending order, missing
// market fill price) return a typed error. Business denials (risk gate or
// insufficient cash/position) are not errors: the order transitions to rejected
// with a reason so the operator sees the status change, and the returned state
// is the unchanged account snapshot.
func (s *Service) FillPaperOrder(ctx context.Context, accountID backtest.PaperAccountID, orderID string, fillPrice market.Price) (PaperOrder, State, error) {
	if accountID == "" {
		return PaperOrder{}, State{}, fmt.Errorf("account_id is required")
	}
	if orderID == "" {
		return PaperOrder{}, State{}, fmt.Errorf("order_id is required")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	book, ok := s.books[accountID]
	if !ok {
		// No order book means the account was never started or never received an
		// order; either way the order id cannot exist.
		if _, started := s.states[accountID]; !started {
			return PaperOrder{}, State{}, ErrAccountNotFound
		}
		return PaperOrder{}, State{}, ErrOrderNotFound
	}
	order, ok := book.orders[orderID]
	if !ok {
		return PaperOrder{}, State{}, ErrOrderNotFound
	}
	if order.Status != PaperOrderStatusPending {
		return PaperOrder{}, State{}, ErrOrderNotPending
	}

	// A supplied fill price must be a non-negative number before it can drive a
	// fill; reject malformed/negative input as invalid request, not an internal
	// fill error. An empty fill price is handled per order type in syntheticFillBar.
	if fillPrice.Amount.Value != "" {
		if err := ValidateOrderDecimal("fill_price", fillPrice.Amount.Value, false); err != nil {
			return PaperOrder{}, State{}, err
		}
	}

	now := s.now().UTC()

	// Resolve the execution bar before any portfolio change so a missing market
	// price is reported as a request error, not an order rejection.
	bar, err := syntheticFillBar(order.Intent, fillPrice, now)
	if err != nil {
		return PaperOrder{}, State{}, err
	}

	account := backtest.PaperAccount{
		ID:           accountID,
		Portfolio:    book.portfolio,
		RiskSettings: book.risk,
	}
	if decision := backtest.CheckRisk(account, order.Intent); !decision.Allowed {
		order.Status = PaperOrderStatusRejected
		order.Reason = decision.Reason
		order.UpdatedAt = now
		return *order, book.snapshot(), nil
	}

	fill, filled, err := backtest.FillOrderOnDailyBar(order.Intent, bar)
	if err != nil {
		return PaperOrder{}, State{}, err
	}
	if !filled {
		// With a synthetic single-price bar a market order always fills and a limit
		// order fills at its own price, so this is unreachable in practice. Treat it
		// as a rejection rather than silently leaving the order pending.
		order.Status = PaperOrderStatusRejected
		order.Reason = "order did not fill at the simulated price"
		order.UpdatedAt = now
		return *order, book.snapshot(), nil
	}

	next, err := book.portfolio.ApplyFill(fill)
	if err != nil {
		order.Status = PaperOrderStatusRejected
		order.Reason = err.Error()
		order.UpdatedAt = now
		return *order, book.snapshot(), nil
	}

	book.portfolio = next
	book.manualFills = append(book.manualFills, fill)
	filledCopy := fill
	order.Status = PaperOrderStatusFilled
	order.Fill = &filledCopy
	order.UpdatedAt = now
	return *order, book.snapshot(), nil
}

// findOrderLocked resolves an order id within an account, distinguishing a
// never-started account from a missing order. It must be called with s.mu held.
func (s *Service) findOrderLocked(accountID backtest.PaperAccountID, orderID string) (*PaperOrder, error) {
	book, ok := s.books[accountID]
	if !ok {
		if _, started := s.states[accountID]; !started {
			return nil, ErrAccountNotFound
		}
		return nil, ErrOrderNotFound
	}
	order, ok := book.orders[orderID]
	if !ok {
		return nil, ErrOrderNotFound
	}
	return order, nil
}

// ValidateOrderDecimal checks that an order lifecycle decimal string parses as a
// number and meets its sign requirement, returning an error wrapping
// ErrInvalidOrderInput so the worker socket boundary maps malformed input to
// invalid_request. requirePositive enforces strictly positive (quantities);
// otherwise the value must be non-negative (prices). It is exported so the socket
// conversion can reject malformed input before building a domain order, while the
// service stays defensive for direct package callers.
func ValidateOrderDecimal(label, value string, requirePositive bool) error {
	rat, ok := new(big.Rat).SetString(strings.TrimSpace(value))
	if !ok {
		return fmt.Errorf("%w: %s must be a number", ErrInvalidOrderInput, label)
	}
	if requirePositive {
		if rat.Sign() <= 0 {
			return fmt.Errorf("%w: %s must be greater than zero", ErrInvalidOrderInput, label)
		}
	} else if rat.Sign() < 0 {
		return fmt.Errorf("%w: %s must not be negative", ErrInvalidOrderInput, label)
	}
	return nil
}

// validateOrderIntent enforces the minimum shape of an order intent: an
// instrument, a known side, a positive numeric quantity, a known type, and a
// non-negative numeric limit price for limit orders.
func validateOrderIntent(intent backtest.OrderIntent) error {
	if intent.InstrumentID == "" {
		return fmt.Errorf("%w: instrument_id is required", ErrInvalidOrderInput)
	}
	switch intent.Side {
	case backtest.OrderSideBuy, backtest.OrderSideSell:
	default:
		return fmt.Errorf("%w: unsupported order side %q", ErrInvalidOrderInput, intent.Side)
	}
	if intent.Quantity.Amount.Value == "" {
		return fmt.Errorf("%w: quantity is required", ErrInvalidOrderInput)
	}
	if err := ValidateOrderDecimal("quantity", intent.Quantity.Amount.Value, true); err != nil {
		return err
	}
	switch intent.OrderType() {
	case backtest.OrderTypeMarket:
	case backtest.OrderTypeLimit:
		if intent.LimitPrice.Currency == "" || intent.LimitPrice.Amount.Value == "" {
			return fmt.Errorf("%w: limit order requires a limit price", ErrInvalidOrderInput)
		}
		if err := ValidateOrderDecimal("limit_price", intent.LimitPrice.Amount.Value, false); err != nil {
			return err
		}
	default:
		return fmt.Errorf("%w: unsupported order type %q", ErrInvalidOrderInput, intent.Type)
	}
	return nil
}

// syntheticFillBar builds the single-bar input for FillOrderOnDailyBar so the
// command-driven fill reuses the same domain fill helper as the run loop. A
// market order fills at the operator-supplied price; a limit order fills at its
// own limit price (the operator is asserting the market reached it).
func syntheticFillBar(intent backtest.OrderIntent, fillPrice market.Price, ts time.Time) (market.Bar, error) {
	var price market.Price
	switch intent.OrderType() {
	case backtest.OrderTypeMarket:
		if fillPrice.Currency == "" || fillPrice.Amount.Value == "" {
			return market.Bar{}, ErrFillPriceRequired
		}
		price = fillPrice
	case backtest.OrderTypeLimit:
		price = intent.LimitPrice
	default:
		return market.Bar{}, fmt.Errorf("unsupported order type %q", intent.Type)
	}
	return market.Bar{
		InstrumentID: intent.InstrumentID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    ts,
		Open:         price,
		High:         price,
		Low:          price,
		Close:        price,
	}, nil
}
