// package bybit provides rates of cryptocurrencies from bybit exchange orderbook.
package bybit

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"strconv"
	"time"

	"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/config"
	"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/entities"
	"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/provider"

	"github.com/shopspring/decimal"
)

const (
	// mainnetURL  = "https://api.bybit.com"
	httpTimeout = time.Second * 10
)

type Bybit struct {
	log    *slog.Logger
	cfg    *config.Bybit
	client *http.Client
}

func New(log *slog.Logger, cfg *config.Bybit) provider.Provider {
	return &Bybit{
		log: log,
		cfg: cfg,
		client: &http.Client{
			Timeout: httpTimeout,
		},
	}
}

func (b *Bybit) symbol(pair *entities.Instrument) string {
	return fmt.Sprintf("%s%s", pair.BaseCurrency, pair.QuoteCurrency)
}

// Price returns the current price of the pair (base currency / quote currency).
// e.g. BTC/USDT.
func (b *Bybit) Price(ctx context.Context, instrument entities.Instrument) (*entities.Price, error) {
	// build request
	req := marketOrderbookReq{
		Category: categorySpot,
		Symbol:   b.symbol(&instrument),
	}
	var resp marketOrderbookResp
	// make request
	err := b.makeRequest(ctx, http.MethodGet, "/v5/market/orderbook", req, &resp)
	if err != nil {
		return nil, fmt.Errorf("failed to make request: %w", err)
	}
	// get results
	if len(resp.A) == 0 || len(resp.B) == 0 {
		return nil, fmt.Errorf("incorrect response: nil data")
	}
	if len(resp.A[0]) != 2 || len(resp.B[0]) != 2 {
		return nil, fmt.Errorf("incorrect response: nil prices")
	}

	askPrice := decimal.RequireFromString(resp.A[0][0])
	bidPrice := decimal.RequireFromString(resp.B[0][0])
	spread := askPrice.Sub(bidPrice)

	return &entities.Price{
		Ask:        askPrice,
		Bid:        bidPrice,
		Spread:     spread,
		Instrument: instrument,
	}, nil
}

const klineLimit = 1000

var intervalToBybitIntervalMapper = map[provider.KlineInterval]string{
	provider.Kline1m:  "1",
	provider.Kline3m:  "3",
	provider.Kline5m:  "5",
	provider.Kline15m: "15",
	provider.Kline30m: "30",
	provider.Kline1H:  "60",
	provider.Kline4H:  "240",
	provider.Kline6H:  "360",
	provider.Kline12H: "720",
	provider.Kline1D:  "D",
	provider.Kline1W:  "W",
}

// intervalDuration returns the time.Duration corresponding to a Bybit kline interval string.
func intervalBybit(interval provider.KlineInterval) (string, error) {
	i, ok := intervalToBybitIntervalMapper[interval]
	if !ok {
		return "", fmt.Errorf("interval not found")
	}
	return i, nil
}

// Candles returns OHLC candles for the given interval in the [from, to) range.
// It paginates automatically when the range exceeds klineLimit candles per request.
func (b *Bybit) Candles(ctx context.Context, instrument entities.Instrument, from, to time.Time, interval provider.KlineInterval) ([]entities.Candle, error) {
	dur := interval.ToDuration()
	limit := klineLimit

	var allCandles []entities.Candle
	batchFrom := from

	for batchFrom.Before(to) {
		batchTo := batchFrom.Add(time.Duration(limit) * dur)
		if batchTo.After(to) {
			batchTo = to
		}

		bybitInterval, err := intervalBybit(interval)
		if err != nil {
			return nil, err
		}

		req := marketKlineReq{
			Category: categorySpot,
			Symbol:   b.symbol(&instrument),
			Interval: bybitInterval,
			Start:    strconv.FormatInt(batchFrom.UnixMilli(), 10),
			End:      strconv.FormatInt(batchTo.UnixMilli(), 10),
			Limit:    &limit,
		}

		var resp marketKlineResp
		if err := b.makeRequest(ctx, http.MethodGet, "/v5/market/kline", req, &resp); err != nil {
			return nil, fmt.Errorf("failed to get kline [%s, %s]: %w", batchFrom.Format(time.RFC3339), batchTo.Format(time.RFC3339), err)
		}

		for _, item := range resp.List {
			if len(item) < 4 {
				b.log.Error("bybit candles: length of elements less then 4", "len", len(item))
				continue
			}
			startMs, err := strconv.ParseInt(item[0], 10, 64)
			if err != nil {
				b.log.Error("bybit candles: failed to parse start time", "err", err)
				continue
			}
			high, err := decimal.NewFromString(item[2])
			if err != nil {
				b.log.Error("bybit candles: failed to parse candle high price", "err", err)
				continue
			}
			low, err := decimal.NewFromString(item[3])
			if err != nil {
				b.log.Error("bybit candles: failed to parse candle low price", "err", err)
				continue
			}
			allCandles = append(allCandles, entities.Candle{
				OpenTime: time.UnixMilli(startMs),
				High:     high,
				Low:      low,
			})
		}

		batchFrom = batchTo
	}

	return allCandles, nil
}
