package socket

import (
	"errors"
	"fmt"

	altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
	"git.toki-labs.com/toki/alt/packages/domain/market"
	"git.toki-labs.com/toki/alt/packages/domain/trading"
	"git.toki-labs.com/toki/alt/services/worker/internal/livetrading"
)

func accountSnapshotToProto(snap trading.AccountSnapshot) *altv1.LiveAccountSnapshot {
	proto := &altv1.LiveAccountSnapshot{
		AccountId:      snap.AccountID,
		Broker:         string(snap.Broker),
		SyncedAtUnixMs: snap.SyncedAt.UnixMilli(),
		Stale:          snap.Stale,
	}
	for _, cb := range snap.Cash {
		proto.Cash = append(proto.Cash, &altv1.LiveCashBalance{
			Currency: currencyToProto(cb.Currency),
			Amount:   &altv1.Decimal{Value: cb.Amount.Value},
		})
	}
	for _, pos := range snap.Positions {
		proto.Positions = append(proto.Positions, positionSnapshotToProto(pos))
	}
	return proto
}

func positionSnapshotToProto(pos trading.PositionSnapshot) *altv1.LivePositionSnapshot {
	p := &altv1.LivePositionSnapshot{
		InstrumentId: string(pos.InstrumentID),
		Side:         string(pos.Side),
		MarketValue:  &altv1.Decimal{Value: pos.MarketValue.Value},
		Pnl:          &altv1.Decimal{Value: pos.PnL.Value},
	}
	if pos.Quantity.Amount.Value != "" {
		p.Quantity = &altv1.Quantity{Amount: &altv1.Decimal{Value: pos.Quantity.Amount.Value}}
	}
	if pos.AvgPrice.Amount.Value != "" {
		p.AvgPrice = priceToProto(pos.AvgPrice)
	}
	if pos.CurrentPrice.Amount.Value != "" {
		p.CurrentPrice = priceToProto(pos.CurrentPrice)
	}
	return p
}

func liveCapabilityToProto(c trading.BrokerCapability) *altv1.LiveBrokerCapability {
	cap := &altv1.LiveBrokerCapability{
		Broker:            string(c.Broker),
		SupportsAmend:     c.SupportsAmend,
		SupportsCancel:    c.SupportsCancel,
		SupportsAccount:   c.SupportsAccount,
		SupportsAuditTags: c.SupportsAuditTags,
		CheckedAtUnixMs:   c.CheckedAt.UnixMilli(),
	}
	for m := range c.Markets {
		cap.Markets = append(cap.Markets, marketToProto(m))
	}
	for v := range c.Venues {
		cap.Venues = append(cap.Venues, venueToProto(v))
	}
	for _, t := range c.OrderTypes {
		cap.OrderTypes = append(cap.OrderTypes, string(t))
	}
	return cap
}

// protoToSubmitRequest maps a SubmitLiveOrderRequest proto onto the domain
// SubmitRequest type. The intent.type field is preserved as-is so custom
// broker order type strings are not narrowed to market/limit.
// Returns an error for unsupported limit_price currency values.
func protoToSubmitRequest(req *altv1.SubmitLiveOrderRequest) (livetrading.SubmitRequest, error) {
	intent := req.GetIntent()
	domainIntent := trading.OrderIntent{}
	if intent != nil {
		domainIntent = trading.OrderIntent{
			InstrumentID: market.InstrumentID(intent.GetInstrumentId()),
			Side:         trading.OrderSide(intent.GetSide()),
			Type:         trading.OrderType(intent.GetType()),
			TimeInForce:  trading.OrderTimeInForce(intent.GetTimeInForce()),
		}
		if q := intent.GetQuantity(); q != nil {
			if amt := q.GetAmount(); amt != nil {
				domainIntent.Quantity = market.Quantity{Amount: market.Decimal{Value: amt.GetValue()}}
			}
		}
		if lp := intent.GetLimitPrice(); lp != nil {
			if amt := lp.GetAmount(); amt != nil {
				currency, err := currencyFromProto(lp.GetCurrency())
				if err != nil {
					return livetrading.SubmitRequest{}, fmt.Errorf("limit_price: %w", err)
				}
				domainIntent.LimitPrice = market.Price{
					Currency: currency,
					Amount:   market.Decimal{Value: amt.GetValue()},
				}
			}
		}
		if tags := intent.GetCustomTags(); len(tags) > 0 {
			domainIntent.CustomTags = tags
		}
	}

	conf := req.GetOperatorConfirmation()
	domainConf := livetrading.OperatorConfirmation{}
	if conf != nil {
		domainConf = livetrading.OperatorConfirmation{
			Confirmed:     conf.GetConfirmed(),
			OperatorID:    conf.GetOperatorId(),
			Reason:        conf.GetReason(),
			ConfirmedAtMs: conf.GetConfirmedAtUnixMs(),
		}
	}

	return livetrading.SubmitRequest{
		AccountID:      req.GetAccountId(),
		Intent:         domainIntent,
		Confirmation:   domainConf,
		IdempotencyKey: req.GetIdempotencyKey(),
	}, nil
}

// liveOrderToProto converts a domain LiveOrder to the proto wire type.
func liveOrderToProto(o livetrading.LiveOrder) *altv1.LiveOrder {
	proto := &altv1.LiveOrder{
		Id:              o.ID,
		BrokerId:        o.BrokerOrderID,
		AccountId:       o.AccountID,
		InstrumentId:    string(o.Intent.InstrumentID),
		Side:            string(o.Intent.Side),
		Type:            string(o.Intent.Type),
		TimeInForce:     string(o.Intent.TimeInForce),
		Status:          string(o.Status),
		BrokerStatus:    string(o.BrokerStatus),
		RejectionReason: o.RejectionReason,
		CreatedAtUnixMs: o.CreatedAt.UnixMilli(),
		UpdatedAtUnixMs: o.UpdatedAt.UnixMilli(),
	}
	if o.Intent.Quantity.Amount.Value != "" {
		proto.Quantity = &altv1.Quantity{Amount: &altv1.Decimal{Value: o.Intent.Quantity.Amount.Value}}
	}
	if o.Intent.LimitPrice.Amount.Value != "" {
		proto.LimitPrice = priceToProto(o.Intent.LimitPrice)
	}
	return proto
}

// isLiveErr unwraps err and checks whether it matches target using errors.Is.
func isLiveErr(err, target error) bool {
	return errors.Is(err, target)
}

func riskPolicyToProto(p trading.RiskPolicy) *altv1.LiveRiskPolicy {
	proto := &altv1.LiveRiskPolicy{
		MaxDailyOrders:    int32(p.MaxDailyOrders),
		MaxOpenOrders:     int32(p.MaxOpenOrders),
		AllowShortSelling: p.AllowShortSelling,
	}
	if len(p.MaxOrderNotionalByCurrency) > 0 {
		proto.MaxOrderNotionalByCurrency = make(map[string]string, len(p.MaxOrderNotionalByCurrency))
		for cur, dec := range p.MaxOrderNotionalByCurrency {
			proto.MaxOrderNotionalByCurrency[string(cur)] = dec.Value
		}
	}
	return proto
}

func killSwitchToProto(ks trading.KillSwitchState) *altv1.LiveKillSwitchState {
	return &altv1.LiveKillSwitchState{
		Halted: ks.Halted,
		Reason: ks.Reason,
	}
}

func riskDecisionToProto(d trading.RiskDecision) *altv1.LiveRiskDecision {
	return &altv1.LiveRiskDecision{
		Allowed: d.Allowed,
		Reason:  d.Reason,
	}
}

func protoToAuditFilter(req *altv1.ListLiveAuditEventsRequest) trading.AuditFilter {
	return trading.AuditFilter{
		AccountID: req.GetAccountId(),
		OrderID:   req.GetOrderId(),
		EventType: trading.AuditEventType(req.GetEventType()),
		Limit:     int(req.GetLimit()),
	}
}

func auditEventToProto(ev trading.AuditEvent) *altv1.LiveAuditEvent {
	return &altv1.LiveAuditEvent{
		EventId:         ev.EventID,
		Type:            string(ev.Type),
		TimestampUnixMs: ev.Timestamp.UnixMilli(),
		Broker:          string(ev.Broker),
		AccountId:       ev.AccountID,
		Payload:         ev.Payload,
		OrderId:         ev.OrderID,
		Status:          ev.Status,
		Reason:          ev.Reason,
		Actor:           ev.Actor,
		CorrelationId:   ev.CorrelationID,
	}
}
