package service

import (
	"fmt"
	"sync"
	"sync/atomic"

	"github.com/genex/trading-service/internal/domain/entity"
	"github.com/genex/trading-service/internal/domain/event"
	"github.com/genex/trading-service/internal/domain/vo"
)

// MatchResult holds the outcome of a matching attempt.
type MatchResult struct {
	Trades       []*entity.Trade
	UpdatedOrder *entity.Order
}

// MatchingService is the application-level service responsible for the matching engine.
// It orchestrates order books and produces trades, delegating persistence to repositories
// and event publishing to the event publisher.
type MatchingService struct {
	orderbooks map[string]*entity.OrderBook
	mu         sync.RWMutex
	tradeSeq   int64
	publisher  event.EventPublisher
}

// NewMatchingService creates a new MatchingService.
func NewMatchingService(publisher event.EventPublisher) *MatchingService {
	if publisher == nil {
		publisher = &event.NoopEventPublisher{}
	}
	return &MatchingService{
		orderbooks: make(map[string]*entity.OrderBook),
		publisher:  publisher,
	}
}

// getOrCreateOrderBook retrieves or lazily initializes an order book for a coupon.
func (s *MatchingService) getOrCreateOrderBook(couponID string) *entity.OrderBook {
	s.mu.Lock()
	defer s.mu.Unlock()
	ob, exists := s.orderbooks[couponID]
	if !exists {
		ob = entity.NewOrderBook(couponID)
		s.orderbooks[couponID] = ob
	}
	return ob
}

// PlaceOrder submits an order to the matching engine, attempts matching,
// and places any unmatched remainder on the book.
func (s *MatchingService) PlaceOrder(order *entity.Order) *MatchResult {
	ob := s.getOrCreateOrderBook(order.CouponID)
	result := &MatchResult{UpdatedOrder: order}

	// Create a trade factory closure that the orderbook entity can use
	createTrade := func(buy, sell *entity.Order, price vo.Price, qty int) *entity.Trade {
		seq := atomic.AddInt64(&s.tradeSeq, 1)
		tradeID := fmt.Sprintf("trade-%d", seq)
		trade, _ := entity.NewTrade(tradeID, buy, sell, price, qty)

		// Publish trade executed event
		_ = s.publisher.Publish(event.NewTradeExecutedEvent(
			trade.ID, trade.CouponID, trade.BuyOrderID, trade.SellOrderID,
			trade.BuyerID, trade.SellerID, trade.Price.Float64(), trade.Quantity,
		))

		return trade
	}

	// Execute matching via the domain entity
	if order.Side == vo.Buy {
		result.Trades = ob.MatchBuyOrder(order, createTrade)
	} else {
		result.Trades = ob.MatchSellOrder(order, createTrade)
	}

	// If the order still has remaining quantity, add to the book (limit orders only)
	if order.RemainingQty.IsPositive() && order.Status != entity.OrderCancelled {
		if order.Type == vo.Limit {
			ob.AddOrder(order)
			if order.FilledQty.IsPositive() {
				order.Status = entity.OrderPartial
			}
		}
	}

	// Publish order placed event
	_ = s.publisher.Publish(event.NewOrderPlacedEvent(
		order.ID, order.UserID, order.CouponID,
		order.Side, order.Type, order.Price.Float64(), order.Quantity.Int(),
	))

	// Publish match events for each trade
	for _, t := range result.Trades {
		_ = s.publisher.Publish(event.NewOrderMatchedEvent(
			order.ID, order.CouponID, t.Quantity, t.Price.Float64(), order.IsFilled(),
		))
	}

	return result
}

// CancelOrder removes an order from the order book.
func (s *MatchingService) CancelOrder(couponID, orderID string, side vo.OrderSide) bool {
	ob := s.getOrCreateOrderBook(couponID)
	removed := ob.RemoveOrder(orderID, side)

	if removed {
		_ = s.publisher.Publish(event.NewOrderCancelledEvent(orderID, "", couponID))
	}

	return removed
}

// GetOrderBookSnapshot returns a depth-limited snapshot of an order book.
func (s *MatchingService) GetOrderBookSnapshot(couponID string, depth int) (bids []entity.PriceLevel, asks []entity.PriceLevel) {
	ob := s.getOrCreateOrderBook(couponID)
	return ob.Snapshot(depth)
}

// GetAllOrderBooks returns a snapshot map of all active order books (for admin use).
func (s *MatchingService) GetAllOrderBooks() map[string]*entity.OrderBook {
	s.mu.RLock()
	defer s.mu.RUnlock()
	result := make(map[string]*entity.OrderBook, len(s.orderbooks))
	for k, v := range s.orderbooks {
		result[k] = v
	}
	return result
}

// GetTradeCount returns the total number of trades executed.
func (s *MatchingService) GetTradeCount() int64 {
	return atomic.LoadInt64(&s.tradeSeq)
}
