package payment

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"

	walletv1 "hyapp.local/api/proto/wallet/v1"
)

type exchangeRateClient interface {
	LatestUSDRates(ctx context.Context, currencies []string) (exchangeRateResult, error)
}

type exchangeRateResult struct {
	Rates       map[string]string             `json:"rates"`
	SourceNames []string                      `json:"sourceNames"`
	Sources     []exchangeRateSourceStatusDTO `json:"sources"`
}

type exchangeRateSourceStatusDTO struct {
	Name       string   `json:"name"`
	URL        string   `json:"url"`
	Success    bool     `json:"success"`
	Currencies []string `json:"currencies,omitempty"`
	Error      string   `json:"error,omitempty"`
}

type thirdPartyPaymentRateSyncDTO struct {
	UpdatedCount      int                           `json:"updatedCount"`
	SkippedCount      int                           `json:"skippedCount"`
	MissingCurrencies []string                      `json:"missingCurrencies"`
	SourceNames       []string                      `json:"sourceNames"`
	Sources           []exchangeRateSourceStatusDTO `json:"sources"`
	Rates             map[string]string             `json:"rates"`
}

type httpExchangeRateClient struct {
	client  *http.Client
	sources []exchangeRateSource
}

type exchangeRateSource struct {
	name  string
	url   string
	parse func([]byte) (map[string]float64, error)
}

func newDefaultExchangeRateClient() exchangeRateClient {
	return &httpExchangeRateClient{
		client: &http.Client{Timeout: 8 * time.Second},
		sources: []exchangeRateSource{
			{name: "open.er-api.com", url: "https://open.er-api.com/v6/latest/USD", parse: parseOpenExchangeRateAPI},
			{name: "fawazahmed0-currency-api", url: "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json", parse: parseFawazCurrencyAPI},
			{name: "FloatRates", url: "https://www.floatrates.com/daily/usd.json", parse: parseFloatRates},
			{name: "Frankfurter", url: "https://api.frankfurter.dev/v1/latest?base=USD", parse: parseFrankfurterRates},
		},
	}
}

func (c *httpExchangeRateClient) LatestUSDRates(ctx context.Context, currencies []string) (exchangeRateResult, error) {
	required, rates := splitRequiredCurrencies(currencies)
	if len(required) == 0 {
		return exchangeRateResult{Rates: rates}, nil
	}
	sourceNames := make([]string, 0, len(c.sources))
	sourceStatuses := make([]exchangeRateSourceStatusDTO, 0, len(c.sources))
	for _, source := range c.sources {
		// 每个源独立失败，不阻断后续源；同一个币种只取最先成功源，避免不同源的更新时间混在同一个币种上来回抖动。
		sourceRates, err := c.fetchSource(ctx, source)
		if err != nil {
			sourceStatuses = append(sourceStatuses, exchangeRateSourceStatusDTO{Name: source.name, URL: source.url, Success: false, Error: err.Error()})
			continue
		}
		matched := make([]string, 0, len(required))
		for currency := range required {
			if _, exists := rates[currency]; exists {
				continue
			}
			value := sourceRates[currency]
			if value <= 0 {
				continue
			}
			rates[currency] = formatExchangeRate(value)
			matched = append(matched, currency)
		}
		sort.Strings(matched)
		if len(matched) > 0 {
			sourceNames = append(sourceNames, source.name)
		}
		sourceStatuses = append(sourceStatuses, exchangeRateSourceStatusDTO{Name: source.name, URL: source.url, Success: true, Currencies: matched})
		if len(rates)-usdRateCount(rates) == len(required) {
			break
		}
	}
	if len(rates)-usdRateCount(rates) == 0 {
		return exchangeRateResult{}, errors.New("no exchange rate source returned requested currencies")
	}
	return exchangeRateResult{Rates: rates, SourceNames: sourceNames, Sources: sourceStatuses}, nil
}

func (c *httpExchangeRateClient) fetchSource(ctx context.Context, source exchangeRateSource) (map[string]float64, error) {
	client := c.client
	if client == nil {
		client = &http.Client{Timeout: 8 * time.Second}
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, source.url, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("User-Agent", "HYAppAdmin/1.0 exchange-rate-sync")
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("http %d", resp.StatusCode)
	}
	body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
	if err != nil {
		return nil, err
	}
	return source.parse(body)
}

func parseOpenExchangeRateAPI(body []byte) (map[string]float64, error) {
	var payload struct {
		Result string             `json:"result"`
		Rates  map[string]float64 `json:"rates"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, err
	}
	if payload.Result != "" && payload.Result != "success" {
		return nil, fmt.Errorf("result %s", payload.Result)
	}
	return normalizeFloatRateMap(payload.Rates), nil
}

func parseFrankfurterRates(body []byte) (map[string]float64, error) {
	var payload struct {
		Rates map[string]float64 `json:"rates"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, err
	}
	return normalizeFloatRateMap(payload.Rates), nil
}

func parseFawazCurrencyAPI(body []byte) (map[string]float64, error) {
	var payload struct {
		Rates map[string]float64 `json:"usd"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, err
	}
	return normalizeFloatRateMap(payload.Rates), nil
}

func parseFloatRates(body []byte) (map[string]float64, error) {
	var payload map[string]struct {
		Code      string `json:"code"`
		AlphaCode string `json:"alphaCode"`
		Rate      string `json:"rate"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, err
	}
	rates := make(map[string]float64, len(payload))
	for key, item := range payload {
		currency := strings.ToUpper(firstNonEmptyString(item.AlphaCode, item.Code, key))
		value, err := strconv.ParseFloat(strings.TrimSpace(item.Rate), 64)
		if currency == "" || err != nil || value <= 0 {
			continue
		}
		rates[currency] = value
	}
	return rates, nil
}

func normalizeFloatRateMap(input map[string]float64) map[string]float64 {
	rates := make(map[string]float64, len(input))
	for currency, value := range input {
		currency = strings.ToUpper(strings.TrimSpace(currency))
		if currency == "" || value <= 0 {
			continue
		}
		rates[currency] = value
	}
	return rates
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func collectThirdPartyPaymentMethods(channels []*walletv1.ThirdPartyPaymentChannel) []*walletv1.ThirdPartyPaymentMethod {
	seen := make(map[int64]struct{})
	methods := make([]*walletv1.ThirdPartyPaymentMethod, 0)
	for _, channel := range channels {
		for _, method := range channel.GetMethods() {
			if method.GetMethodId() <= 0 {
				continue
			}
			if _, exists := seen[method.GetMethodId()]; exists {
				continue
			}
			seen[method.GetMethodId()] = struct{}{}
			methods = append(methods, method)
		}
	}
	return methods
}

func collectThirdPartyPaymentCurrencies(methods []*walletv1.ThirdPartyPaymentMethod) []string {
	currencies := make(map[string]struct{})
	for _, method := range methods {
		if currency := strings.ToUpper(strings.TrimSpace(method.GetCurrencyCode())); currency != "" {
			currencies[currency] = struct{}{}
		}
	}
	return sortedMapKeys(currencies)
}

func splitRequiredCurrencies(currencies []string) (map[string]struct{}, map[string]string) {
	required := make(map[string]struct{})
	rates := make(map[string]string)
	for _, currency := range currencies {
		currency = strings.ToUpper(strings.TrimSpace(currency))
		if currency == "" {
			continue
		}
		if currency == "USD" {
			rates[currency] = "1.00000000"
			continue
		}
		required[currency] = struct{}{}
	}
	return required, rates
}

func usdRateCount(rates map[string]string) int {
	if rates["USD"] == "" {
		return 0
	}
	return 1
}

func sortedMapKeys(values map[string]struct{}) []string {
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func formatExchangeRate(value float64) string {
	return strconv.FormatFloat(value, 'f', 8, 64)
}
