package coingecko

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

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

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 := defaultCoinGeckoBaseURL
	provider := strings.ToLower(mmodel.DriverCoinGecko.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("coingecko: invalid base url", err)
	}

	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
}

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, fmerrors.Wrap("coingecko: parse base url", err)
	}
	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, fmerrors.Wrap("coingecko: build request", err)
	}

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

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

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

	var payload map[string]map[string]interface{}
	if err := decoder.Decode(&payload); err != nil {
		c.logger.Warn("CoinGecko decode failed", zap.String("symbol", symbol), zap.Error(err))
		return nil, fmerrors.Wrap("coingecko: decode response", err)
	}

	coinData, ok := payload[coinID]
	if !ok {
		return nil, fmerrors.New("coingecko: coin id not found in response")
	}
	priceValue, ok := coinData[vsCurrency]
	if !ok {
		return nil, fmerrors.New("coingecko: vs currency not found in response")
	}

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

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

	timestamp := time.Now().UnixMilli()
	if tsValue, ok := coinData["last_updated_at"]; ok {
		if tsFloat, ok := toFloat(tsValue); ok && tsFloat > 0 {
			tsMillis := int64(tsFloat * 1000)
			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 "", "", fmerrors.New("coingecko: symbol is empty")
	}

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

	if len(parts) != 2 {
		return "", "", fmerrors.New("coingecko: symbol must be <coin_id>/<vs_currency>")
	}

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

	return coinID, vsCurrency, nil
}

func toFloat(value interface{}) (float64, bool) {
	switch v := value.(type) {
	case json.Number:
		f, err := v.Float64()
		if err != nil {
			return 0, false
		}
		return f, true
	case float64:
		return v, true
	case float32:
		return float64(v), true
	case int:
		return float64(v), true
	case int64:
		return float64(v), true
	case uint64:
		return float64(v), true
	case string:
		if parsed, err := strconv.ParseFloat(v, 64); err == nil {
			return parsed, true
		}
	}
	return 0, false
}
