package payment

import (
	"context"
	"fmt"
	"hash/fnv"
	"strconv"
	"strings"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/config"
	"hyapp-admin-server/internal/repository"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

const (
	legacySyntheticRegionIDBase  int64  = 9_000_000_000_000_000_000
	legacySyntheticRegionIDRange uint64 = 223_372_036_854_775_807
)

type MoneyRegionSource interface {
	ListMoneyMasterData(ctx context.Context, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error)
	CountryBelongsToRegion(ctx context.Context, appCode string, regionID int64, countryCode string) (bool, bool, error)
}

type MongoMoneyRegionSource struct {
	collection *mongo.Collection
	config     config.MoneyRegionSourceConfig
}

type legacyRegionDocument struct {
	ID           any    `bson:"_id"`
	RegionCode   string `bson:"regionCode"`
	SysOrigin    string `bson:"sysOrigin"`
	RegionName   string `bson:"regionName"`
	CountryCodes string `bson:"countryCodes"`
	Del          bool   `bson:"del"`
}

func NewMongoMoneyRegionSource(database *mongo.Database, sourceConfig config.MoneyRegionSourceConfig) MoneyRegionSource {
	if database == nil || !sourceConfig.Enabled {
		return nil
	}
	collectionName := strings.TrimSpace(sourceConfig.MongoCollection)
	if collectionName == "" {
		collectionName = "sys_region_config"
	}
	sourceConfig.AppCode = appctx.Normalize(sourceConfig.AppCode)
	sourceConfig.AppName = strings.TrimSpace(sourceConfig.AppName)
	sourceConfig.SysOrigin = strings.ToUpper(strings.TrimSpace(sourceConfig.SysOrigin))
	sourceConfig.MongoCollection = collectionName
	return &MongoMoneyRegionSource{collection: database.Collection(collectionName), config: sourceConfig}
}

func (s *MongoMoneyRegionSource) ListMoneyMasterData(ctx context.Context, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error) {
	if s == nil || s.collection == nil {
		return nil, nil, nil, nil
	}
	appCode := appctx.Normalize(s.config.AppCode)
	if appCode == "" || !moneyAccessAllowsApp(access, appCode) {
		return nil, nil, nil, nil
	}
	filter := bson.M{
		"sysOrigin": s.config.SysOrigin,
		"del":       false,
	}
	// Yumi/Aslan 的区域事实仍由 legacy likei Mongo 持有；admin 只读与 Java 当前查询等价的字段，不复制对方写模型。
	cursor, err := s.collection.Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "createTime", Value: -1}}))
	if err != nil {
		return nil, nil, nil, fmt.Errorf("query legacy money regions for %s: %w", appCode, err)
	}
	defer cursor.Close(ctx)

	documents := []legacyRegionDocument{}
	for cursor.Next(ctx) {
		var document legacyRegionDocument
		if err := cursor.Decode(&document); err != nil {
			return nil, nil, nil, fmt.Errorf("decode legacy money region for %s: %w", appCode, err)
		}
		documents = append(documents, document)
	}
	if err := cursor.Err(); err != nil {
		return nil, nil, nil, fmt.Errorf("iterate legacy money regions for %s: %w", appCode, err)
	}
	return legacyRegionDocumentsToMoneyData(s.config, documents, access)
}

func (s *MongoMoneyRegionSource) CountryBelongsToRegion(ctx context.Context, appCode string, regionID int64, countryCode string) (bool, bool, error) {
	if s == nil || s.collection == nil || appctx.Normalize(s.config.AppCode) != appctx.Normalize(appCode) {
		return false, false, nil
	}
	filter := bson.M{
		"sysOrigin": s.config.SysOrigin,
		"del":       false,
	}
	cursor, err := s.collection.Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "createTime", Value: -1}}))
	if err != nil {
		return false, false, fmt.Errorf("query legacy region countries for %s: %w", appCode, err)
	}
	defer cursor.Close(ctx)
	for cursor.Next(ctx) {
		var document legacyRegionDocument
		if err := cursor.Decode(&document); err != nil {
			return false, false, fmt.Errorf("decode legacy region countries for %s: %w", appCode, err)
		}
		documentRegionID, _, err := legacyMoneyRegionID(s.config, document)
		if err != nil {
			return false, false, err
		}
		if !repository.RegionIDLooseEqual(documentRegionID, regionID) {
			continue
		}
		for _, item := range splitLegacyCountryCodes(document.CountryCodes) {
			if item == strings.ToUpper(strings.TrimSpace(countryCode)) {
				return true, true, nil
			}
		}
		return false, true, nil
	}
	if err := cursor.Err(); err != nil {
		return false, false, fmt.Errorf("iterate legacy region countries for %s: %w", appCode, err)
	}
	return false, false, nil
}

func (s *MongoMoneyRegionSource) CountryCodesForRegion(ctx context.Context, appCode string, regionID int64) ([]string, bool, error) {
	if s == nil || s.collection == nil || appctx.Normalize(s.config.AppCode) != appctx.Normalize(appCode) {
		return nil, false, nil
	}
	filter := bson.M{
		"sysOrigin": s.config.SysOrigin,
		"del":       false,
	}
	cursor, err := s.collection.Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "createTime", Value: -1}}))
	if err != nil {
		return nil, false, fmt.Errorf("query legacy region countries for %s: %w", appCode, err)
	}
	defer cursor.Close(ctx)
	for cursor.Next(ctx) {
		var document legacyRegionDocument
		if err := cursor.Decode(&document); err != nil {
			return nil, false, fmt.Errorf("decode legacy region countries for %s: %w", appCode, err)
		}
		documentRegionID, _, err := legacyMoneyRegionID(s.config, document)
		if err != nil {
			return nil, false, err
		}
		if !repository.RegionIDLooseEqual(documentRegionID, regionID) {
			continue
		}
		// 大屏外接源只认 legacy country_code；这里集中复用财务范围的 Mongo 区域口径，避免每个外接 App 自己维护一套区域映射。
		return splitLegacyCountryCodes(document.CountryCodes), true, nil
	}
	if err := cursor.Err(); err != nil {
		return nil, false, fmt.Errorf("iterate legacy region countries for %s: %w", appCode, err)
	}
	return nil, false, nil
}

// RegionIDForCountryCode 把 legacy 用户的国家码解析成该 App 的区域 ID。
// 遍历顺序与 ListMoneyMasterData 相同（createTime 倒序、先到先得），
// 与账单区域分布的 legacyCountryRegionIndex 保持同一优先级，保证同一个国家
// 在账单归因和币商订单归因里落进同一个区域（如 MM 同时挂在多个区域时）。
func (s *MongoMoneyRegionSource) RegionIDForCountryCode(ctx context.Context, appCode string, countryCode string) (int64, bool, error) {
	if s == nil || s.collection == nil || appctx.Normalize(s.config.AppCode) != appctx.Normalize(appCode) {
		return 0, false, nil
	}
	countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
	if countryCode == "" {
		return 0, false, nil
	}
	_, regions, _, err := s.ListMoneyMasterData(ctx, repository.MoneyAccess{All: true})
	if err != nil {
		return 0, false, err
	}
	return regionIDForCountryCodeInRegions(regions, countryCode)
}

func regionIDForCountryCodeInRegions(regions []moneyRegionDTO, countryCode string) (int64, bool, error) {
	for _, region := range regions {
		for _, item := range region.Countries {
			if item == countryCode {
				return region.RegionID, true, nil
			}
		}
	}
	return 0, false, nil
}

// RegionIDForCountryCode 在多个区域源中解析国家码归属；一个 App 只会命中一个源。
func RegionIDForCountryCode(ctx context.Context, sources []MoneyRegionSource, appCode string, countryCode string) (int64, bool, error) {
	for _, source := range sources {
		resolver, ok := source.(interface {
			RegionIDForCountryCode(ctx context.Context, appCode string, countryCode string) (int64, bool, error)
		})
		if !ok {
			continue
		}
		regionID, found, err := resolver.RegionIDForCountryCode(ctx, appCode, countryCode)
		if err != nil {
			return 0, false, err
		}
		if found {
			return regionID, true, nil
		}
	}
	return 0, false, nil
}

// RegionCatalogEntry 是给其他模块（社交 BI 等）消费的 legacy 区域目录导出形态；
// RegionID 与财务范围/大屏过滤使用同一套合成 int64 口径。
type RegionCatalogEntry struct {
	AppCode    string
	AppName    string
	RegionID   int64
	RegionCode string
	RegionName string
	Countries  []string
}

func (s *MongoMoneyRegionSource) RegionCatalog(ctx context.Context) ([]RegionCatalogEntry, error) {
	if s == nil || s.collection == nil {
		return nil, nil
	}
	_, regions, _, err := s.ListMoneyMasterData(ctx, repository.MoneyAccess{All: true})
	if err != nil {
		return nil, err
	}
	appName := strings.TrimSpace(s.config.AppName)
	entries := make([]RegionCatalogEntry, 0, len(regions))
	for _, region := range regions {
		entries = append(entries, RegionCatalogEntry{
			AppCode:    region.AppCode,
			AppName:    appName,
			RegionID:   region.RegionID,
			RegionCode: region.RegionCode,
			RegionName: region.Name,
			Countries:  region.Countries,
		})
	}
	return entries, nil
}

func legacyRegionDocumentsToMoneyData(sourceConfig config.MoneyRegionSourceConfig, documents []legacyRegionDocument, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error) {
	appCode := appctx.Normalize(sourceConfig.AppCode)
	if appCode == "" || !moneyAccessAllowsApp(access, appCode) {
		return nil, nil, nil, nil
	}
	appName := strings.TrimSpace(sourceConfig.AppName)
	if appName == "" {
		appName = appCode
	}
	sysOrigin := strings.ToUpper(strings.TrimSpace(sourceConfig.SysOrigin))
	regions := []moneyRegionDTO{}
	countries := []moneyCountryDTO{}
	seenRegionIDs := map[int64]string{}
	for index, document := range documents {
		if document.Del || sysOrigin != "" && !strings.EqualFold(strings.TrimSpace(document.SysOrigin), sysOrigin) {
			continue
		}
		regionID, rawRegionID, err := legacyMoneyRegionID(sourceConfig, document)
		if err != nil {
			return nil, nil, nil, err
		}
		if previousRawID, ok := seenRegionIDs[regionID]; ok && previousRawID != rawRegionID {
			return nil, nil, nil, fmt.Errorf("legacy money region id collision for %s: %s and %s", appCode, previousRawID, rawRegionID)
		}
		seenRegionIDs[regionID] = rawRegionID
		if !access.All && !access.Allows(appCode, regionID) {
			continue
		}
		regionCode := strings.TrimSpace(document.RegionCode)
		if regionCode == "" {
			regionCode = strconv.FormatInt(regionID, 10)
		}
		regionName := strings.TrimSpace(document.RegionName)
		if regionName == "" {
			regionName = regionCode
		}
		countryCodes := splitLegacyCountryCodes(document.CountryCodes)
		regions = append(regions, moneyRegionDTO{
			AppCode:    appCode,
			RegionID:   regionID,
			RegionCode: regionCode,
			Name:       regionName,
			Status:     "active",
			// 旧文档数组本身是展示顺序；分配递减权重后仍满足全后台“大值靠前”的统一契约。
			SortOrder: len(documents) - index,
			Countries: countryCodes,
		})
		for countryIndex, countryCode := range countryCodes {
			countries = append(countries, moneyCountryDTO{
				AppCode:            appCode,
				CountryCode:        countryCode,
				CountryName:        countryCode,
				CountryDisplayName: countryCode,
				Enabled:            true,
				RegionID:           regionID,
				SortOrder:          len(countryCodes) - countryIndex,
			})
		}
	}
	return []moneyAppDTO{{
		AppCode: appCode,
		AppName: appName,
		// Legacy App 没有 apps 行；其 Logo 与区域源一起配置，避免为了 UI 展示伪造活跃租户。
		LogoURL:  strings.TrimSpace(sourceConfig.LogoURL),
		Platform: "legacy",
		Status:   "active",
	}}, regions, countries, nil
}

func legacyMoneyRegionID(sourceConfig config.MoneyRegionSourceConfig, document legacyRegionDocument) (int64, string, error) {
	appCode := appctx.Normalize(sourceConfig.AppCode)
	rawID := legacyRawRegionID(document.ID)
	if rawID == "" {
		return 0, "", fmt.Errorf("legacy money region %s has empty id", appCode)
	}
	if numericID, err := strconv.ParseInt(rawID, 10, 64); err == nil && numericID > 0 {
		return numericID, rawID, nil
	}
	// Java legacy 数据里历史区域多数是雪花数字字符串，但线上已存在 Mongo ObjectId；admin 授权表只能存 int64，
	// 因此把 appCode + 原始 _id 哈希到 9e18 以上的保留正整数段，保证同一 legacy 区域跨进程、跨部署稳定。
	hash := fnv.New64a()
	_, _ = hash.Write([]byte(appCode))
	_, _ = hash.Write([]byte{0})
	_, _ = hash.Write([]byte(rawID))
	return legacySyntheticRegionIDBase + int64(hash.Sum64()%legacySyntheticRegionIDRange), rawID, nil
}

func legacyRawRegionID(value any) string {
	switch typed := value.(type) {
	case nil:
		return ""
	case string:
		return strings.TrimSpace(typed)
	case bson.ObjectID:
		return typed.Hex()
	case fmt.Stringer:
		return strings.TrimSpace(typed.String())
	default:
		return strings.TrimSpace(fmt.Sprint(typed))
	}
}

func (h *Handler) listMoneyRegionSources(ctx context.Context, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error) {
	if h == nil || len(h.moneyRegionSources) == 0 {
		return nil, nil, nil, nil
	}
	apps := []moneyAppDTO{}
	regions := []moneyRegionDTO{}
	countries := []moneyCountryDTO{}
	for _, source := range h.moneyRegionSources {
		sourceApps, sourceRegions, sourceCountries, err := source.ListMoneyMasterData(ctx, access)
		if err != nil {
			return nil, nil, nil, err
		}
		apps = appendUniqueMoneyApps(apps, sourceApps)
		regions = appendUniqueMoneyRegions(regions, sourceRegions)
		countries = appendUniqueMoneyCountries(countries, sourceCountries)
	}
	return apps, regions, countries, nil
}

func splitLegacyCountryCodes(value string) []string {
	parts := strings.FieldsFunc(value, func(r rune) bool {
		return r == ',' || r == ';' || r == ' ' || r == '\n' || r == '\t' || r == '\r'
	})
	out := make([]string, 0, len(parts))
	seen := map[string]struct{}{}
	for _, part := range parts {
		code := strings.ToUpper(strings.TrimSpace(part))
		if code == "" {
			continue
		}
		if _, ok := seen[code]; ok {
			continue
		}
		seen[code] = struct{}{}
		out = append(out, code)
	}
	return out
}

func appendUniqueMoneyApps(base []moneyAppDTO, extra []moneyAppDTO) []moneyAppDTO {
	seen := map[string]struct{}{}
	for _, item := range base {
		seen[appctx.Normalize(item.AppCode)] = struct{}{}
	}
	for _, item := range extra {
		key := appctx.Normalize(item.AppCode)
		if key == "" {
			continue
		}
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		base = append(base, item)
	}
	return base
}

func appendUniqueMoneyRegions(base []moneyRegionDTO, extra []moneyRegionDTO) []moneyRegionDTO {
	seen := map[string]struct{}{}
	for _, item := range base {
		seen[fmt.Sprintf("%s:%d", appctx.Normalize(item.AppCode), item.RegionID)] = struct{}{}
	}
	for _, item := range extra {
		key := fmt.Sprintf("%s:%d", appctx.Normalize(item.AppCode), item.RegionID)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		base = append(base, item)
	}
	return base
}

func appendUniqueMoneyCountries(base []moneyCountryDTO, extra []moneyCountryDTO) []moneyCountryDTO {
	seen := map[string]struct{}{}
	for _, item := range base {
		seen[fmt.Sprintf("%s:%d:%s", appctx.Normalize(item.AppCode), item.RegionID, strings.ToUpper(item.CountryCode))] = struct{}{}
	}
	for _, item := range extra {
		key := fmt.Sprintf("%s:%d:%s", appctx.Normalize(item.AppCode), item.RegionID, strings.ToUpper(item.CountryCode))
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		base = append(base, item)
	}
	return base
}
