package coingecko

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 coingeckoConnector struct {
	id       mmodel.Driver
	provider string
	client   *http.Client
	base     string
	logger   mlogger.Logger
}

const defaultCoinGeckoBaseURL = "https://api.coingecko.com/api/v3"

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

const (
	expectedSymbolParts = 2
	tsToMillis          = 1000
)

func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) { //nolint:ireturn
	baseURL := defaultCoinGeckoBaseURL
	provider := strings.ToLower(mmodel.DriverCoinGecko.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, "coingecko: invalid base url", "base_url")
	}

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

	connector := &coingeckoConnector{
		id:       mmodel.DriverCoinGecko,
		provider: provider,
		client: &http.Client{
			Timeout:   requestTimeout,
			Transport: transport,
		},
		base:   strings.TrimRight(parsed.String(), "/"),
		logger: logger.Named("coingecko"),
	}

	return connector, nil
}

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

//nolint:cyclop,funlen
func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*mmodel.Ticker, error) {
	coinID, vsCurrency, err := parseSymbol(symbol)
	if err != nil {
		return nil, err
	}

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

	endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/simple/price"
	query := endpoint.Query()
	query.Set("ids", coinID)
	query.Set("vs_currencies", vsCurrency)
	query.Set("include_last_updated_at", "true")
	endpoint.RawQuery = query.Encode()

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

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

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

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

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

	decoder := json.NewDecoder(resp.Body)
	decoder.UseNumber()

	var payload map[string]map[string]any

	decodeErr := decoder.Decode(&payload)
	if decodeErr != nil {
		c.logger.Warn("CoinGecko decode failed", zap.String("symbol", symbol), zap.Error(decodeErr))

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

	coinData, coinFound := payload[coinID]
	if !coinFound {
		return nil, merrors.Internal("coingecko: coin id not found in response")
	}

	priceValue, priceFound := coinData[vsCurrency]
	if !priceFound {
		return nil, merrors.Internal("coingecko: vs currency not found in response")
	}

	price, priceOk := toFloat(priceValue)
	if !priceOk || price <= 0 {
		return nil, merrors.Internal("coingecko: invalid price value in response")
	}

	priceStr := strconv.FormatFloat(price, 'f', -1, 64)

	timestamp := time.Now().UnixMilli()

	if tsValue, tsFound := coinData["last_updated_at"]; tsFound {
		if tsFloat, tsOk := toFloat(tsValue); tsOk && tsFloat > 0 {
			tsMillis := int64(tsFloat * tsToMillis)
			if tsMillis > 0 {
				timestamp = tsMillis
			}
		}
	}

	refSymbol := coinID + "_" + vsCurrency

	return &mmodel.Ticker{
		Symbol:    refSymbol,
		BidPrice:  priceStr,
		AskPrice:  priceStr,
		Provider:  c.provider,
		Timestamp: timestamp,
	}, nil
}

func parseSymbol(symbol string) (string, string, error) {
	trimmed := strings.TrimSpace(symbol)
	if trimmed == "" {
		return "", "", merrors.InvalidArgument("coingecko: symbol is empty", "symbol")
	}

	parts := strings.FieldsFunc(strings.ToLower(trimmed), func(r rune) bool {
		switch r {
		case ':', '/', '-', '_':
			return true
		}

		return false
	})

	if len(parts) != expectedSymbolParts {
		return "", "", merrors.InvalidArgument("coingecko: symbol must be <coin_id>/<vs_currency>", "symbol")
	}

	coinID := strings.TrimSpace(parts[0])

	vsCurrency := strings.TrimSpace(parts[1])
	if coinID == "" || vsCurrency == "" {
		return "", "", merrors.InvalidArgument("coingecko: symbol contains empty segments", "symbol")
	}

	return coinID, vsCurrency, nil
}

func toFloat(value any) (float64, bool) {
	switch val := value.(type) {
	case json.Number:
		f, err := val.Float64()
		if err != nil {
			return 0, false
		}

		return f, true
	case float64:
		return val, true
	case float32:
		return float64(val), true
	case int:
		return float64(val), true
	case int64:
		return float64(val), true
	case uint64:
		return float64(val), true
	case string:
		parsed, parseErr := strconv.ParseFloat(val, 64)
		if parseErr == nil {
			return parsed, true
		}
	}

	return 0, false
}
