package fix

import (
	"log/slog"
	"os"
	"sync"
	"time"

	"github.com/rs/zerolog/log"
	"github.com/shopspring/decimal"

	"quantex.com/qfixpt/quickfix"
	"quantex.com/qfixpt/quickfix/gen/enum"
	"quantex.com/qfixpt/quickfix/gen/field"
	"quantex.com/qfixpt/quickfix/gen/fix50sp2/allocationreport"
	"quantex.com/qfixpt/quickfix/gen/fix50sp2/allocationreportack"
	"quantex.com/qfixpt/quickfix/gen/fix50sp2/confirmation"
	"quantex.com/qfixpt/quickfix/gen/fix50sp2/confirmationack"
	"quantex.com/qfixpt/quickfix/gen/fix50sp2/tradecapturereport"
	"quantex.com/qfixpt/quickfix/gen/fix50sp2/tradecapturereportack"
	filelog "quantex.com/qfixpt/quickfix/log/file"
	filestore "quantex.com/qfixpt/quickfix/store/file"
	"quantex.com/qfixpt/src/app"
	"quantex.com/qfixpt/src/common/tracerr"
	"quantex.com/qfixpt/src/domain"
)

// Custom Tradeweb tags not in generated code.
const (
	tagTestMessage     = 23029
	tagTWTradeID       = 23068
	tagTWOrigTradeID   = 23096
	tagTWClrID         = 23025
	tagDlrClrID        = 23027
	tagClearingStatus  = 5440
	tagTradingSystemID = 6731
)

// postTrade is the internal representation of a trade in memory.
type postTrade struct {
	TradeID         string
	TradeReportID   string
	TradeReportType string
	Side            string
	LastQty         decimal.Decimal
	LastPx          decimal.Decimal
	SettlDate       string
	TradeDate       string
	Status          domain.PostTradeStatus
	TWTradeID       string
}

// Manager wraps the QuickFIX initiator for post-trade message handling.
type Manager struct {
	initiator *quickfix.Initiator
	app       *application
	tradesMu  sync.RWMutex
	trades    map[string]domain.TradeCaptureReport
	store     domain.PersistenceStore
	notify    domain.Notifier
	cfg       app.FIXConfig
}

func NewManager(cfg app.FIXConfig, store domain.PersistenceStore, notify domain.Notifier) *Manager {
	return &Manager{
		trades: make(map[string]domain.TradeCaptureReport),
		store:  store,
		notify: notify,
		cfg:    cfg,
	}
}

func (m *Manager) Start() error {
	fixApp := newApplication(m.notify)
	fixApp.onLogon = m.onLogon
	fixApp.onLogout = m.onLogout
	fixApp.onTradeCaptureReport = m.handleTradeCaptureReport
	fixApp.onAllocationReport = m.handleAllocationReport
	fixApp.onConfirmation = m.handleConfirmation
	fixApp.onRawMessage = m.handleRawMessage
	m.app = fixApp

	if err := m.loadTrades(); err != nil {
		slog.Error("failed to load trades from DB, starting with empty state", "error", err)
	}

	f, err := os.Open(m.cfg.SettingsFile)
	if err != nil {
		err = tracerr.Errorf("error opening FIX settings file %q: %s", m.cfg.SettingsFile, err)
		log.Error().Msg(err.Error())

		return err
	}
	defer f.Close()

	settings, err := quickfix.ParseSettings(f)
	if err != nil {
		err = tracerr.Errorf("error parsing FIX settings: %s", err)
		log.Error().Msg(err.Error())

		return err
	}

	storeFactory := filestore.NewStoreFactory(settings)
	logFactory, err := filelog.NewLogFactory(settings)
	if err != nil {
		err = tracerr.Errorf("error creating file log factory: %s", err)
		log.Error().Msg(err.Error())

		return err
	}

	initiator, err := quickfix.NewInitiator(fixApp, storeFactory, settings, logFactory)
	if err != nil {
		err = tracerr.Errorf("error creating FIX initiator: %s", err)
		log.Error().Msg(err.Error())

		return err
	}

	m.initiator = initiator

	if err = m.initiator.Start(); err != nil {
		err = tracerr.Errorf("error starting FIX initiator: %s", err)
		log.Error().Msg(err.Error())

		return err
	}

	slog.Info("FIX initiator started", "settings", m.cfg.SettingsFile)

	return nil
}

func (m *Manager) Stop() {
	if m.initiator != nil {
		m.initiator.Stop()
		slog.Info("FIX initiator stopped")
	}
}

func (m *Manager) onLogon(sessionID quickfix.SessionID) {
	slog.Info("FIX PT session logged on", "session", sessionID.String())
}

func (m *Manager) onLogout(sessionID quickfix.SessionID) {
	slog.Info("FIX PT session logged out", "session", sessionID.String())
}

// handleTradeCaptureReport processes an incoming TradeCaptureReport (35=AE).
func (m *Manager) handleTradeCaptureReport(msg tradecapturereport.TradeCaptureReport, sessionID quickfix.SessionID) {
	tradeReportID, _ := msg.GetTradeReportID()

	// ACK immediately before any validation.
	ack := tradecapturereportack.New()
	ack.SetTradeReportID(tradeReportID)

	if err := quickfix.SendToTarget(ack, sessionID); err != nil {
		slog.Error("failed to send TradeCaptureReportAck", "tradeReportID", tradeReportID, "error", err.Error())
	} else {
		slog.Info("TradeCaptureReportAck sent", "tradeReportID", tradeReportID)
	}

	// Check if test message.
	testMessage := getCustomBool(&msg.Message.Body.FieldMap, tagTestMessage)
	if testMessage {
		slog.Info("test TradeCaptureReport, skipping", "tradeReportID", tradeReportID)
		return
	}

	// Parse trade data.
	tradeReportType, _ := msg.GetTradeReportType()
	tradeID, _ := msg.GetTradeID()
	execID, _ := msg.GetExecID()
	lastQty, _ := msg.GetLastQty()
	lastPx, _ := msg.GetLastPx()
	spread, _ := msg.GetSpread()
	settlDate, _ := msg.GetSettlDate()
	tradeDate, _ := msg.GetTradeDate()
	grossTradeAmt, _ := msg.GetGrossTradeAmt()
	tradeReportRefID, _ := msg.GetTradeReportRefID()

	// Custom fields.
	twTradeID := getCustomString(&msg.Message.Body.FieldMap, tagTWTradeID)
	twOrigTradeID := getCustomString(&msg.Message.Body.FieldMap, tagTWOrigTradeID)

	// Extract side-level fields (parties, accrued interest, net money).
	var side string
	var accruedInterest, netMoney decimal.Decimal
	var clearingFirm, twTraderID string

	sides, sideErr := msg.GetNoSides()
	if sideErr == nil && sides.Len() > 0 {
		s := sides.Get(0)
		sideVal, _ := s.GetSide()
		side = string(sideVal)
		accruedInterest, _ = s.GetAccruedInterestAmt()
		netMoney, _ = s.GetNetMoney()

		clearingFirm = extractPartyByRole(s, enum.PartyRole_CLEARING_FIRM)
		// 1007 is a custom Tradeweb role, access via string comparison.
		twTraderID = extractPartyByRole(s, "1007")
	}

	trade := domain.TradeCaptureReport{
		TradeReportID:    tradeReportID,
		TradeID:          tradeID,
		ExecID:           execID,
		LastQty:          lastQty,
		LastPx:           lastPx,
		Spread:           spread,
		Side:             side,
		AccruedInterest:  accruedInterest,
		NetMoney:         netMoney,
		GrossTradeAmt:    grossTradeAmt,
		SettlDate:        settlDate,
		TradeDate:        tradeDate,
		TradeReportType:  string(tradeReportType),
		TradeReportRefID: tradeReportRefID,
		ClearingFirm:     clearingFirm,
		TradewebTraderID: twTraderID,
		TWTradeID:        twTradeID,
		TWOrigTradeID:    twOrigTradeID,
	}

	slog.Info("TradeCaptureReport parsed",
		"tradeReportID", trade.TradeReportID,
		"tradeReportType", trade.TradeReportType,
		"tradeID", trade.TradeID,
		"lastQty", trade.LastQty.String(),
		"lastPx", trade.LastPx.String(),
		"side", trade.Side,
		"settlDate", trade.SettlDate,
	)

	// Persist structured message.
	m.persistMessage(tradeID, parseTradeCaptureReportJSON(msg))

	m.tradesMu.Lock()
	m.trades[tradeID] = trade
	m.tradesMu.Unlock()
}

// handleAllocationReport processes an incoming AllocationReport (35=AS).
func (m *Manager) handleAllocationReport(msg allocationreport.AllocationReport, sessionID quickfix.SessionID) {
	allocReportID, _ := msg.GetAllocReportID()

	// ACK immediately.
	ack := allocationreportack.New(field.NewAllocReportID(allocReportID))

	if err := quickfix.SendToTarget(ack, sessionID); err != nil {
		slog.Error("failed to send AllocationReportAck", "allocReportID", allocReportID, "error", err.Error())
	} else {
		slog.Info("AllocationReportAck sent", "allocReportID", allocReportID)
	}

	// Check if test message.
	testMessage := getCustomBool(&msg.Message.Body.FieldMap, tagTestMessage)
	if testMessage {
		slog.Info("test AllocationReport, skipping", "allocReportID", allocReportID)
		return
	}

	allocTransType, _ := msg.GetAllocTransType()

	// Extract TradeID: try tag 1003 directly, fall back to SecondaryOrderID (198) from NoOrders.
	tradeID := getCustomString(&msg.Message.Body.FieldMap, 1003)
	if tradeID == "" {
		if orders, err := msg.GetNoOrders(); err == nil && orders.Len() > 0 {
			if v, e := orders.Get(0).GetSecondaryOrderID(); e == nil && v != "" {
				tradeID = v
			}
		}
	}

	alloc := domain.Allocation{
		AllocReportID:  allocReportID,
		AllocTransType: string(allocTransType),
		TradeID:        tradeID,
	}

	// Parse NoAllocs repeating group.
	allocs, allocErr := msg.GetNoAllocs()
	if allocErr == nil {
		for i := 0; i < allocs.Len(); i++ {
			entry := allocs.Get(i)
			account, _ := entry.GetAllocAccount()
			qty, _ := entry.GetAllocQty()
			individualID, _ := entry.GetIndividualAllocID()
			price, _ := entry.GetAllocPrice()

			alloc.Entries = append(alloc.Entries, domain.AllocEntry{
				AllocAccount:      account,
				AllocQty:          qty,
				IndividualAllocID: individualID,
				AllocPrice:        price,
			})
		}
	}

	slog.Info("AllocationReport parsed",
		"allocReportID", alloc.AllocReportID,
		"allocTransType", alloc.AllocTransType,
		"tradeID", alloc.TradeID,
		"numEntries", len(alloc.Entries),
	)

	// Persist structured message.
	m.persistMessage(tradeID, parseAllocationReportJSON(msg))

	switch allocTransType {
	case enum.AllocTransType_NEW:
		slog.Info("new allocation", "allocReportID", allocReportID)
	case enum.AllocTransType_REPLACE:
		slog.Info("replace allocation", "allocReportID", allocReportID)
	case enum.AllocTransType_CANCEL:
		slog.Info("cancel allocation", "allocReportID", allocReportID)
	default:
		slog.Warn("unhandled AllocTransType", "allocTransType", string(allocTransType), "allocReportID", allocReportID)
	}
}

// handleConfirmation processes an incoming Confirmation (35=AK).
func (m *Manager) handleConfirmation(msg confirmation.Confirmation, sessionID quickfix.SessionID) {
	confirmID, _ := msg.GetConfirmID()
	tradeDate, _ := msg.GetTradeDate()
	transactTime, _ := msg.GetTransactTime()

	// ACK immediately.
	ack := confirmationack.New(
		field.NewConfirmID(confirmID),
		field.NewTradeDate(tradeDate),
		field.NewTransactTime(transactTime),
		field.NewAffirmStatus(enum.AffirmStatus_RECEIVED),
	)

	if err := quickfix.SendToTarget(ack, sessionID); err != nil {
		slog.Error("failed to send ConfirmationAck", "confirmID", confirmID, "error", err.Error())
	} else {
		slog.Info("ConfirmationAck sent", "confirmID", confirmID)
	}

	// Check if test message.
	testMessage := getCustomBool(&msg.Message.Body.FieldMap, tagTestMessage)
	if testMessage {
		slog.Info("test Confirmation, skipping", "confirmID", confirmID)
		return
	}

	confirmStatus, _ := msg.GetConfirmStatus()
	confirmTransType, _ := msg.GetConfirmTransType()
	individualAllocID, _ := msg.GetIndividualAllocID()

	// Custom fields.
	twClrID := getCustomString(&msg.Message.Body.FieldMap, tagTWClrID)
	dlrClrID := getCustomString(&msg.Message.Body.FieldMap, tagDlrClrID)
	clearingStatus := getCustomString(&msg.Message.Body.FieldMap, tagClearingStatus)

	conf := domain.ConfirmationMsg{
		ConfirmID:         confirmID,
		ConfirmStatus:     string(confirmStatus),
		ConfirmTransType:  string(confirmTransType),
		IndividualAllocID: individualAllocID,
		TWClrID:           twClrID,
		DlrClrID:          dlrClrID,
		ClearingStatus:    clearingStatus,
		TradeDate:         tradeDate,
	}

	slog.Info("Confirmation parsed",
		"confirmID", conf.ConfirmID,
		"confirmStatus", conf.ConfirmStatus,
		"confirmTransType", conf.ConfirmTransType,
		"individualAllocID", conf.IndividualAllocID,
		"clearingStatus", conf.ClearingStatus,
	)

	// Persist structured message. Use TradeID if available, fall back to ConfirmID.
	persistID := getCustomString(&msg.Message.Body.FieldMap, 1003)
	if persistID == "" {
		persistID = confirmID
	}

	m.persistMessage(persistID, parseConfirmationJSON(msg))
}

// persistMessage saves a structured FIX message to the messages table.
func (m *Manager) persistMessage(tradeID string, fixJSON domain.FixMessageJSON) {
	if err := m.store.SaveMessage(domain.TradeMessage{
		TradeID:  tradeID,
		JMessage: fixJSON,
	}); err != nil {
		slog.Error("failed to persist message", "msgType", fixJSON.MsgType, "tradeID", tradeID, "error", err)
	}
}

// handleRawMessage persists raw FIX message strings to the logs table.
func (m *Manager) handleRawMessage(direction string, msg *quickfix.Message) {
	tradeID := extractTradeIdentifier(msg)
	if tradeID == "" {
		return
	}

	if err := m.store.SaveLog(domain.LogEntry{
		TradeID: tradeID,
		RawMsg:  "[" + direction + "] " + msg.String(),
	}); err != nil {
		slog.Error("failed to persist raw log", "error", err)
	}
}

// loadTrades reconstructs all trades and their states from today's messages in the database.
func (m *Manager) loadTrades() error {
	messages, err := m.store.GetTodayMessages()
	if err != nil {
		return err
	}

	trades := make(map[string]domain.TradeCaptureReport)

	for _, msg := range messages {
		switch msg.JMessage.MsgType {
		case "AE": // TradeCaptureReport
			body := msg.JMessage.Body
			tradeReportType, _ := body["TradeReportType"].(string)
			tradeID := msg.TradeID

			if tradeID == "" {
				continue
			}

			t, exists := trades[tradeID]
			if !exists {
				t = domain.TradeCaptureReport{
					TradeID: tradeID,
					// Status:  domain.PostTradeStatusActive,
				}
				trades[tradeID] = t
			}

			if v, ok := body["TradeReportID"].(string); ok {
				t.TradeReportID = v
			}

			t.TradeReportType = tradeReportType

			if v, ok := body["Side"].(string); ok {
				t.Side = v
			}

			if v, ok := body["LastQty"].(string); ok {
				t.LastQty, _ = decimal.NewFromString(v)
			}

			if v, ok := body["LastPx"].(string); ok {
				t.LastPx, _ = decimal.NewFromString(v)
			}

			if v, ok := body["SettlDate"].(string); ok {
				t.SettlDate = v
			}

			if v, ok := body["TradeDate"].(string); ok {
				t.TradeDate = v
			}

			if v, ok := body["TWTradeID"].(string); ok {
				t.TWTradeID = v
			}

			// switch tradeReportType {
			// case "103": // TRDCORR
			// 	t.Status = domain.PostTradeStatusCorrected
			// case "104": // TRDCXL
			// 	t.Status = domain.PostTradeStatusCancelled
			// default:
			// 	if t.Status != domain.PostTradeStatusCorrected && t.Status != domain.PostTradeStatusCancelled {
			// 		t.Status = domain.PostTradeStatusActive
			// 	}
			// }
		}
	}

	// active := 0
	// for _, t := range trades {
	// 	if t.Status == domain.PostTradeStatusActive {
	// 		active++
	// 	}
	// }

	m.tradesMu.Lock()
	m.trades = trades
	m.tradesMu.Unlock()

	// slog.Info("recovery completed", "totalTrades", len(trades), "activeTrades", active)

	return nil
}

func (m *Manager) TradeCaptureReports() []domain.TradeCaptureReport {
	m.tradesMu.RLock()
	defer m.tradesMu.RUnlock()

	var result []domain.TradeCaptureReport

	for _, t := range m.trades {
		result = append(result, t)
	}

	return result
}

// GetAllTrades returns all trades regardless of status.
func (m *Manager) GetAllTrades() []domain.TradeCaptureReport {
	m.tradesMu.RLock()
	defer m.tradesMu.RUnlock()

	result := make([]domain.TradeCaptureReport, 0, len(m.trades))

	for _, t := range m.trades {
		result = append(result, t)
	}

	return result
}

func (t *postTrade) toPostTrade() domain.PostTrade {
	return domain.PostTrade{
		TradeID:         t.TradeID,
		TradeReportID:   t.TradeReportID,
		TradeReportType: t.TradeReportType,
		Side:            t.Side,
		LastQty:         t.LastQty.String(),
		LastPx:          t.LastPx.String(),
		SettlDate:       t.SettlDate,
		TradeDate:       t.TradeDate,
		Status:          t.Status,
		TWTradeID:       t.TWTradeID,
	}
}

// Ensure transactTime has a sensible default if not present in the message.
var _ = time.Now // used by confirmationack.New via field.NewTransactTime
