package auth

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	"hyapp/pkg/xerr"
	authdomain "hyapp/services/user-service/internal/domain/auth"
)

const maxHTTPGeoResponseBytes = 64 * 1024

// HTTPIPGeoProviderConfig 描述一个外部 HTTP IP Geo 源；URL 和 header 支持用 {ip} 注入入口 IP。
type HTTPIPGeoProviderConfig struct {
	URL             string
	Method          string
	CountryJSONPath string
	Headers         map[string]string
	HTTPClient      *http.Client
}

type edgeCountryProvider struct{}

type staticCountryProvider struct {
	name        string
	countryCode string
}

type httpIPGeoProvider struct {
	name            string
	urlTemplate     string
	method          string
	countryJSONPath string
	headers         map[string]string
	client          *http.Client
}

type unavailableIPGeoProvider struct {
	name string
}

// defaultHTTPIPGeoProviders 固定复用 chatapp3 当前线上使用的四个公开 GeoIP 源；配置只保留顺序，不把 URL 下放到 YAML。
var defaultHTTPIPGeoProviders = map[string]HTTPIPGeoProviderConfig{
	"primary_geo": {
		URL:             "https://api.ip.sb/geoip/{ip}",
		Method:          http.MethodGet,
		CountryJSONPath: "country_code",
	},
	"fallback_geo_a": {
		URL:             "https://freeipapi.com/api/json/{ip}",
		Method:          http.MethodGet,
		CountryJSONPath: "countryCode",
	},
	"fallback_geo_b": {
		URL:             "https://ipwhois.app/json/{ip}?format=json",
		Method:          http.MethodGet,
		CountryJSONPath: "country_code",
	},
	"fallback_geo_c": {
		URL:             "https://ip.nc.gy/json?ip={ip}",
		Method:          http.MethodGet,
		CountryJSONPath: "country.iso_code",
	},
}

func BuildIPGeoProviders(names []string) []IPGeoProvider {
	return BuildIPGeoProvidersWithHTTP(names, defaultHTTPIPGeoProviders)
}

func BuildIPGeoProvidersWithHTTP(names []string, httpConfigs map[string]HTTPIPGeoProviderConfig) []IPGeoProvider {
	providers := make([]IPGeoProvider, 0, len(names))
	for _, name := range names {
		name = strings.TrimSpace(name)
		if name == "" {
			continue
		}
		switch {
		case strings.EqualFold(name, "edge_country"):
			providers = append(providers, edgeCountryProvider{})
		case strings.HasPrefix(strings.ToLower(name), "static:"):
			country := normalizeCountryCode(strings.TrimPrefix(name, "static:"))
			providers = append(providers, staticCountryProvider{name: name, countryCode: country})
		case httpConfigs != nil:
			if config, ok := httpConfigs[name]; ok {
				providers = append(providers, newHTTPIPGeoProvider(name, config))
			} else {
				providers = append(providers, unavailableIPGeoProvider{name: name})
			}
		default:
			// 未配置具体 provider 适配器时显式降级为 unavailable，worker 会按顺序继续尝试下一个。
			providers = append(providers, unavailableIPGeoProvider{name: name})
		}
	}
	return providers
}

func newHTTPIPGeoProvider(name string, config HTTPIPGeoProviderConfig) IPGeoProvider {
	method := strings.ToUpper(strings.TrimSpace(config.Method))
	if method == "" {
		method = http.MethodGet
	}
	client := config.HTTPClient
	if client == nil {
		client = http.DefaultClient
	}
	headers := make(map[string]string, len(config.Headers))
	for key, value := range config.Headers {
		key = strings.TrimSpace(key)
		if key == "" {
			continue
		}
		headers[key] = strings.TrimSpace(value)
	}
	return httpIPGeoProvider{
		name:            name,
		urlTemplate:     strings.TrimSpace(config.URL),
		method:          method,
		countryJSONPath: strings.TrimSpace(config.CountryJSONPath),
		headers:         headers,
		client:          client,
	}
}

func (p edgeCountryProvider) Name() string {
	return "edge_country"
}

func (p edgeCountryProvider) ResolveCountry(_ context.Context, job authdomain.LoginIPRiskJob) (IPGeoResult, error) {
	country := normalizeCountryCode(job.EdgeCountryCode)
	if country == "" {
		return IPGeoResult{}, xerr.New(xerr.Unavailable, "edge country is empty")
	}
	return IPGeoResult{CountryCode: country, Confidence: 60}, nil
}

func (p staticCountryProvider) Name() string {
	return p.name
}

func (p staticCountryProvider) ResolveCountry(_ context.Context, _ authdomain.LoginIPRiskJob) (IPGeoResult, error) {
	if p.countryCode == "" {
		return IPGeoResult{}, xerr.New(xerr.Unavailable, "static country is empty")
	}
	return IPGeoResult{CountryCode: p.countryCode, Confidence: 100}, nil
}

func (p httpIPGeoProvider) Name() string {
	return p.name
}

func (p httpIPGeoProvider) ResolveCountry(ctx context.Context, job authdomain.LoginIPRiskJob) (IPGeoResult, error) {
	if strings.TrimSpace(p.urlTemplate) == "" {
		return IPGeoResult{}, xerr.New(xerr.Unavailable, "http geo provider url is empty")
	}
	clientIP := strings.TrimSpace(job.ClientIP)
	if clientIP == "" {
		return IPGeoResult{}, xerr.New(xerr.Unavailable, "client ip is empty")
	}
	endpoint := renderIPGeoTemplate(p.urlTemplate, clientIP)
	request, err := http.NewRequestWithContext(ctx, p.method, endpoint, nil)
	if err != nil {
		return IPGeoResult{}, err
	}
	for key, value := range p.headers {
		request.Header.Set(key, renderIPGeoTemplate(value, clientIP))
	}
	response, err := p.client.Do(request)
	if err != nil {
		return IPGeoResult{}, err
	}
	defer response.Body.Close()
	if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
		return IPGeoResult{}, fmt.Errorf("http geo provider status %d", response.StatusCode)
	}
	body, err := io.ReadAll(io.LimitReader(response.Body, maxHTTPGeoResponseBytes+1))
	if err != nil {
		return IPGeoResult{}, err
	}
	if len(body) > maxHTTPGeoResponseBytes {
		return IPGeoResult{}, xerr.New(xerr.Unavailable, "http geo provider response too large")
	}
	var payload any
	if err := json.Unmarshal(body, &payload); err != nil {
		return IPGeoResult{}, err
	}
	country := countryFromHTTPGeoPayload(payload, p.countryJSONPath)
	if country == "" {
		return IPGeoResult{}, xerr.New(xerr.Unavailable, "http geo provider country is empty")
	}
	return IPGeoResult{CountryCode: country, Confidence: 80}, nil
}

func renderIPGeoTemplate(template string, clientIP string) string {
	return strings.ReplaceAll(template, "{ip}", url.QueryEscape(clientIP))
}

func countryFromHTTPGeoPayload(payload any, configuredPath string) string {
	if country := countryAtJSONPath(payload, configuredPath); country != "" {
		return country
	}
	for _, path := range []string{
		"country_code",
		"countryCode",
		"iso_code",
		"isoCode",
		"country.iso_code",
		"country.country_code",
		"data.country_code",
		"data.countryCode",
		"result.country_code",
		"result.countryCode",
		"registered_country.iso_code",
	} {
		if country := countryAtJSONPath(payload, path); country != "" {
			return country
		}
	}
	if text, ok := payload.(string); ok {
		return normalizeCountryCode(text)
	}
	return ""
}

func countryAtJSONPath(payload any, path string) string {
	path = strings.TrimSpace(path)
	if path == "" {
		return ""
	}
	current := payload
	for _, part := range strings.Split(path, ".") {
		part = strings.TrimSpace(part)
		if part == "" {
			return ""
		}
		object, ok := current.(map[string]any)
		if !ok {
			return ""
		}
		next, ok := object[part]
		if !ok {
			return ""
		}
		current = next
	}
	text, ok := current.(string)
	if !ok {
		return ""
	}
	return normalizeCountryCode(text)
}

func (p unavailableIPGeoProvider) Name() string {
	return p.name
}

func (p unavailableIPGeoProvider) ResolveCountry(_ context.Context, _ authdomain.LoginIPRiskJob) (IPGeoResult, error) {
	return IPGeoResult{}, fmt.Errorf("ip geo provider %s is not configured", p.name)
}
