package socket

import (
	"context"
	"errors"

	protoSocket "git.toki-labs.com/toki/proto-socket/go"

	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"
	"git.toki-labs.com/toki/alt/services/worker/internal/papertrading"
)

// PaperService is the worker-owned paper trading runtime the socket layer drives.
// The papertrading package provides the concrete process-local implementation;
// the narrow interface keeps the handlers testable with a fake.
type PaperService interface {
	StartPaperTrading(ctx context.Context, req papertrading.StartRequest) (papertrading.State, error)
	GetPaperTradingState(ctx context.Context, accountID backtest.PaperAccountID) (papertrading.State, error)

	// Virtual order lifecycle: submit records a pending order, cancel transitions
	// a pending order, fill simulates execution against the account portfolio.
	SubmitPaperOrder(ctx context.Context, req papertrading.SubmitOrderRequest) (papertrading.PaperOrder, error)
	CancelPaperOrder(ctx context.Context, accountID backtest.PaperAccountID, orderID string) (papertrading.PaperOrder, error)
	FillPaperOrder(ctx context.Context, accountID backtest.PaperAccountID, orderID string, fillPrice market.Price) (papertrading.PaperOrder, papertrading.State, error)
}

// paperHandlers returns the session handlers for the paper trading command and
// query surface, each closing over the shared dependencies.
func paperHandlers(deps Deps) []sessionHandler {
	return []sessionHandler{
		{
			requestType: protoSocket.TypeNameOf(&altv1.StartPaperTradingRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.StartPaperTradingRequest, *altv1.StartPaperTradingResponse](
					&client.Communicator,
					func(req *altv1.StartPaperTradingRequest) (*altv1.StartPaperTradingResponse, error) {
						return handleStartPaperTrading(deps, req)
					},
				)
			},
		},
		{
			requestType: protoSocket.TypeNameOf(&altv1.GetPaperTradingStateRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.GetPaperTradingStateRequest, *altv1.GetPaperTradingStateResponse](
					&client.Communicator,
					func(req *altv1.GetPaperTradingStateRequest) (*altv1.GetPaperTradingStateResponse, error) {
						return handleGetPaperTradingState(deps, req)
					},
				)
			},
		},
		{
			requestType: protoSocket.TypeNameOf(&altv1.SubmitPaperOrderRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.SubmitPaperOrderRequest, *altv1.SubmitPaperOrderResponse](
					&client.Communicator,
					func(req *altv1.SubmitPaperOrderRequest) (*altv1.SubmitPaperOrderResponse, error) {
						return handleSubmitPaperOrder(deps, req)
					},
				)
			},
		},
		{
			requestType: protoSocket.TypeNameOf(&altv1.CancelPaperOrderRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.CancelPaperOrderRequest, *altv1.CancelPaperOrderResponse](
					&client.Communicator,
					func(req *altv1.CancelPaperOrderRequest) (*altv1.CancelPaperOrderResponse, error) {
						return handleCancelPaperOrder(deps, req)
					},
				)
			},
		},
		{
			requestType: protoSocket.TypeNameOf(&altv1.FillPaperOrderRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.FillPaperOrderRequest, *altv1.FillPaperOrderResponse](
					&client.Communicator,
					func(req *altv1.FillPaperOrderRequest) (*altv1.FillPaperOrderResponse, error) {
						return handleFillPaperOrder(deps, req)
					},
				)
			},
		},
	}
}

func handleStartPaperTrading(deps Deps, req *altv1.StartPaperTradingRequest) (*altv1.StartPaperTradingResponse, error) {
	startReq, err := startPaperRequestFromProto(req)
	if err != nil {
		return &altv1.StartPaperTradingResponse{Error: invalidPaperRequest(err.Error())}, nil
	}
	if deps.Paper == nil {
		return &altv1.StartPaperTradingResponse{Error: unavailableError("paper trading is not available")}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
	defer cancel()

	state, err := deps.Paper.StartPaperTrading(ctx, startReq)
	if err != nil {
		return &altv1.StartPaperTradingResponse{Error: paperBackendErrorInfo(err)}, nil
	}
	return &altv1.StartPaperTradingResponse{State: paperStateToProto(startReq.AccountID, state)}, nil
}

func handleGetPaperTradingState(deps Deps, req *altv1.GetPaperTradingStateRequest) (*altv1.GetPaperTradingStateResponse, error) {
	if req.GetAccountId() == "" {
		return &altv1.GetPaperTradingStateResponse{Error: invalidPaperRequest("account_id is required")}, nil
	}
	if deps.Paper == nil {
		return &altv1.GetPaperTradingStateResponse{Error: unavailableError("paper trading is not available")}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
	defer cancel()

	accountID := backtest.PaperAccountID(req.GetAccountId())
	state, err := deps.Paper.GetPaperTradingState(ctx, accountID)
	if err != nil {
		return &altv1.GetPaperTradingStateResponse{Error: paperBackendErrorInfo(err)}, nil
	}
	return &altv1.GetPaperTradingStateResponse{State: paperStateToProto(accountID, state)}, nil
}

func handleSubmitPaperOrder(deps Deps, req *altv1.SubmitPaperOrderRequest) (*altv1.SubmitPaperOrderResponse, error) {
	submitReq, err := submitPaperOrderRequestFromProto(req)
	if err != nil {
		return &altv1.SubmitPaperOrderResponse{Error: invalidPaperRequest(err.Error())}, nil
	}
	if deps.Paper == nil {
		return &altv1.SubmitPaperOrderResponse{Error: unavailableError("paper trading is not available")}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
	defer cancel()

	order, err := deps.Paper.SubmitPaperOrder(ctx, submitReq)
	if err != nil {
		return &altv1.SubmitPaperOrderResponse{Error: paperBackendErrorInfo(err)}, nil
	}
	return &altv1.SubmitPaperOrderResponse{Order: paperOrderToProto(order)}, nil
}

func handleCancelPaperOrder(deps Deps, req *altv1.CancelPaperOrderRequest) (*altv1.CancelPaperOrderResponse, error) {
	if req.GetAccountId() == "" {
		return &altv1.CancelPaperOrderResponse{Error: invalidPaperRequest("account_id is required")}, nil
	}
	if req.GetOrderId() == "" {
		return &altv1.CancelPaperOrderResponse{Error: invalidPaperRequest("order_id is required")}, nil
	}
	if deps.Paper == nil {
		return &altv1.CancelPaperOrderResponse{Error: unavailableError("paper trading is not available")}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
	defer cancel()

	order, err := deps.Paper.CancelPaperOrder(ctx, backtest.PaperAccountID(req.GetAccountId()), req.GetOrderId())
	if err != nil {
		return &altv1.CancelPaperOrderResponse{Error: paperBackendErrorInfo(err)}, nil
	}
	return &altv1.CancelPaperOrderResponse{Order: paperOrderToProto(order)}, nil
}

func handleFillPaperOrder(deps Deps, req *altv1.FillPaperOrderRequest) (*altv1.FillPaperOrderResponse, error) {
	if req.GetAccountId() == "" {
		return &altv1.FillPaperOrderResponse{Error: invalidPaperRequest("account_id is required")}, nil
	}
	if req.GetOrderId() == "" {
		return &altv1.FillPaperOrderResponse{Error: invalidPaperRequest("order_id is required")}, nil
	}
	var fillPrice market.Price
	if req.GetFillPrice() != nil {
		price, err := priceFromProto(req.GetFillPrice())
		if err != nil {
			return &altv1.FillPaperOrderResponse{Error: invalidPaperRequest("fill_price: " + err.Error())}, nil
		}
		if err := papertrading.ValidateOrderDecimal("fill_price", price.Amount.Value, false); err != nil {
			return &altv1.FillPaperOrderResponse{Error: invalidPaperRequest(err.Error())}, nil
		}
		fillPrice = price
	}
	if deps.Paper == nil {
		return &altv1.FillPaperOrderResponse{Error: unavailableError("paper trading is not available")}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
	defer cancel()

	accountID := backtest.PaperAccountID(req.GetAccountId())
	order, state, err := deps.Paper.FillPaperOrder(ctx, accountID, req.GetOrderId(), fillPrice)
	if err != nil {
		return &altv1.FillPaperOrderResponse{Error: paperBackendErrorInfo(err)}, nil
	}
	return &altv1.FillPaperOrderResponse{
		Order: paperOrderToProto(order),
		State: paperStateToProto(accountID, state),
	}, nil
}

func invalidPaperRequest(reason string) *altv1.ErrorInfo {
	return errorInfo(backtestErrorInvalidRequest, "invalid paper trading request: "+reason)
}

// paperBackendErrorInfo maps paper runtime errors onto the shared typed error
// vocabulary: a missing account is not_found, a deadline is timeout, everything
// else is internal.
func paperBackendErrorInfo(err error) *altv1.ErrorInfo {
	if err == nil {
		return nil
	}
	switch {
	case errors.Is(err, papertrading.ErrAccountNotFound), errors.Is(err, papertrading.ErrOrderNotFound):
		return errorInfo(backtestErrorNotFound, err.Error())
	case errors.Is(err, papertrading.ErrOrderNotPending), errors.Is(err, papertrading.ErrFillPriceRequired), errors.Is(err, papertrading.ErrInvalidOrderInput):
		return errorInfo(backtestErrorInvalidRequest, err.Error())
	case errors.Is(err, context.DeadlineExceeded):
		return errorInfo(backtestErrorTimeout, err.Error())
	default:
		return errorInfo(backtestErrorInternal, err.Error())
	}
}
