package binance

import (
	"context"
	"encoding/json"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/tech/sendico/fx/ingestor/internal/market/common"
	mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	"go.uber.org/zap"
)

type binanceConnector struct {
	id       mmodel.Driver
	provider string
	client   *http.Client
	base     string
	logger   mlogger.Logger
}

const defaultBinanceBaseURL = "https://api.binance.com"

const (
	defaultDialTimeout           = 5 * time.Second
	defaultDialKeepAlive         = 30 * time.Second
	defaultTLSHandshakeTimeout   = 5 * time.Second
	defaultResponseHeaderTimeout = 10 * time.Second
	defaultRequestTimeout        = 10 * time.Second
)

func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) { //nolint:ireturn
	baseURL := defaultBinanceBaseURL
	provider := strings.ToLower(mmodel.DriverBinance.String())
	dialTimeout := defaultDialTimeout
	dialKeepAlive := defaultDialKeepAlive
	tlsHandshakeTimeout := defaultTLSHandshakeTimeout
	responseHeaderTimeout := defaultResponseHeaderTimeout
	requestTimeout := defaultRequestTimeout

	if settings != nil {
		if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" {
			baseURL = strings.TrimSpace(value)
		}

		if value, ok := settings["provider"].(string); ok && strings.TrimSpace(value) != "" {
			provider = strings.TrimSpace(value)
		}

		dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout)
		dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive)
		tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout)
		responseHeaderTimeout = common.DurationSetting(settings, "response_header_timeout_seconds", responseHeaderTimeout)
		requestTimeout = common.DurationSetting(settings, "request_timeout_seconds", requestTimeout)
	}

	parsed, err := url.Parse(baseURL)
	if err != nil {
		return nil, merrors.InvalidArgumentWrap(err, "binance: invalid base url", "base_url")
	}

	transport := &http.Transport{
		DialContext:           (&net.Dialer{Timeout: dialTimeout, KeepAlive: dialKeepAlive}).DialContext,
		TLSHandshakeTimeout:   tlsHandshakeTimeout,
		ResponseHeaderTimeout: responseHeaderTimeout,
	}

	connector := &binanceConnector{
		id:       mmodel.DriverBinance,
		provider: provider,
		client: &http.Client{
			Timeout:   requestTimeout,
			Transport: transport,
		},
		base:   parsed.String(),
		logger: logger.Named("binance"),
	}

	return connector, nil
}

func (c *binanceConnector) ID() mmodel.Driver {
	return c.id
}

func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmodel.Ticker, error) {
	if strings.TrimSpace(symbol) == "" {
		return nil, merrors.InvalidArgument("binance: symbol is empty", "symbol")
	}

	endpoint, err := url.Parse(c.base)
	if err != nil {
		return nil, merrors.InternalWrap(err, "binance: parse base url")
	}

	endpoint.Path = "/api/v3/ticker/bookTicker"
	query := endpoint.Query()
	query.Set("symbol", strings.ToUpper(strings.TrimSpace(symbol)))
	endpoint.RawQuery = query.Encode()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
	if err != nil {
		return nil, merrors.InternalWrap(err, "binance: build request")
	}

	resp, err := c.client.Do(req)
	if err != nil {
		c.logger.Warn("Binance request failed", zap.String("symbol", symbol), zap.Error(err))

		return nil, merrors.InternalWrap(err, "binance: request failed")
	}
	defer func() {
		if closeErr := resp.Body.Close(); closeErr != nil {
			c.logger.Warn("Failed to close Binance response body", zap.Error(closeErr))
		}
	}()

	if resp.StatusCode != http.StatusOK {
		c.logger.Warn("Binance returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode))

		return nil, merrors.Internal("binance: unexpected status " + strconv.Itoa(resp.StatusCode))
	}

	var payload struct {
		Symbol   string `json:"symbol"`
		BidPrice string `json:"bidPrice"`
		AskPrice string `json:"askPrice"`
	}

	decodeErr := json.NewDecoder(resp.Body).Decode(&payload)
	if decodeErr != nil {
		c.logger.Warn("Binance decode failed", zap.String("symbol", symbol), zap.Error(decodeErr))

		return nil, merrors.InternalWrap(decodeErr, "binance: decode response")
	}

	return &mmodel.Ticker{
		Symbol:    payload.Symbol,
		BidPrice:  payload.BidPrice,
		AskPrice:  payload.AskPrice,
		Provider:  c.provider,
		Timestamp: time.Now().UnixMilli(),
	}, nil
}
