package binance

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

	"github.com/tech/sendico/fx/ingestor/internal/fmerrors"
	"github.com/tech/sendico/fx/ingestor/internal/market/common"
	mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
	"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 (
	defaultDialTimeoutSeconds           = 5 * time.Second
	defaultDialKeepAliveSeconds         = 30 * time.Second
	defaultTLSHandshakeTimeoutSeconds   = 5 * time.Second
	defaultResponseHeaderTimeoutSeconds = 10 * time.Second
	defaultRequestTimeoutSeconds        = 10 * time.Second
)

func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) {
	baseURL := defaultBinanceBaseURL
	provider := strings.ToLower(mmodel.DriverBinance.String())
	dialTimeout := defaultDialTimeoutSeconds
	dialKeepAlive := defaultDialKeepAliveSeconds
	tlsHandshakeTimeout := defaultTLSHandshakeTimeoutSeconds
	responseHeaderTimeout := defaultResponseHeaderTimeoutSeconds
	requestTimeout := defaultRequestTimeoutSeconds

	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, fmerrors.Wrap("binance: invalid base url", err)
	}

	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, fmerrors.New("binance: symbol is empty")
	}

	endpoint, err := url.Parse(c.base)
	if err != nil {
		return nil, fmerrors.Wrap("binance: parse base url", err)
	}
	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, fmerrors.Wrap("binance: build request", err)
	}

	resp, err := c.client.Do(req)
	if err != nil {
		c.logger.Warn("Binance request failed", zap.String("symbol", symbol), zap.Error(err))
		return nil, fmerrors.Wrap("binance: request failed", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		c.logger.Warn("Binance returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode))
		return nil, fmerrors.New("binance: unexpected status " + strconv.Itoa(resp.StatusCode))
	}

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

	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		c.logger.Warn("Binance decode failed", zap.String("symbol", symbol), zap.Error(err))
		return nil, fmerrors.Wrap("binance: decode response", err)
	}

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