package dashboard

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"hyapp-admin-server/internal/config"
	"hyapp-admin-server/internal/integration/userclient"
	"hyapp-admin-server/internal/model"
	"hyapp-admin-server/internal/repository"
)

type DashboardService struct {
	store           *repository.Store
	userDB          *sql.DB
	cfg             config.Config
	httpClient      *http.Client
	user            userclient.Client
	externalSources map[string]ExternalDashboardSource
}

type StatisticsQuery struct {
	AppCode       string
	StatTZ        string
	StartMS       int64
	EndMS         int64
	SeriesStartMS int64
	SeriesEndMS   int64
	RegionID      int64
	CountryID     int64
}

type SelfGameStatisticsQuery struct {
	AppCode   string
	StatTZ    string
	RequestID string
	StartMS   int64
	EndMS     int64
	RegionID  int64
	CountryID int64
	GameID    string
}

type AppTrackingFunnelQuery struct {
	AppCode   string
	StatTZ    string
	StartMS   int64
	EndMS     int64
	RegionID  int64
	RegionIDs []int64
	CountryID int64
}

type SocialRequirementsQuery struct {
	AppCode     string
	StatTZ      string
	StartMS     int64
	EndMS       int64
	RegionID    int64
	RegionIDs   []int64
	CountryID   int64
	Section     string
	UserRole    string
	PayerType   string
	NewUserType string
}

type PlatformGrantStatisticsQuery struct {
	AppCode   string
	StatTZ    string
	RequestID string
	StartMS   int64
	EndMS     int64
	StatDay   string
	RegionID  int64
	CountryID int64
	UserID    int64
	Source    string
	Page      int
	PageSize  int
}

type ExternalDashboardSource interface {
	AppCode() string
	StatisticsOverview(context.Context, StatisticsQuery) (map[string]any, error)
}

type ExternalDashboardRegionResolver interface {
	CountryCodesForRegion(ctx context.Context, appCode string, regionID int64) ([]string, bool, error)
}

type Option func(*DashboardService)

// WithUserDB 注入用户主数据只读连接；Dashboard 用它计算已完成真实用户的人群快照，
// 并从 login_audit 读取最近一次成功登录版本。当天新增与活跃仍沿用 statistics-service 的事实投影。
func WithUserDB(userDB *sql.DB) Option {
	return func(s *DashboardService) {
		s.userDB = userDB
	}
}

func WithExternalDashboardSources(sources ...ExternalDashboardSource) Option {
	return func(s *DashboardService) {
		for _, source := range sources {
			if source == nil {
				continue
			}
			appCode := strings.ToLower(strings.TrimSpace(source.AppCode()))
			if appCode == "" {
				continue
			}
			s.externalSources[appCode] = source
		}
	}
}

func NewService(store *repository.Store, cfg config.Config, user userclient.Client, options ...Option) *DashboardService {
	service := &DashboardService{
		store:           store,
		cfg:             cfg,
		httpClient:      &http.Client{Timeout: cfg.StatisticsService.RequestTimeout},
		user:            user,
		externalSources: map[string]ExternalDashboardSource{},
	}
	for _, option := range options {
		if option != nil {
			option(service)
		}
	}
	return service
}

func (s *DashboardService) Overview() (*repository.DashboardOverview, error) {
	return s.store.DashboardOverview()
}

func (s *DashboardService) StatisticsOverview(ctx context.Context, query StatisticsQuery) (map[string]any, error) {
	if source := s.externalSources[strings.ToLower(strings.TrimSpace(query.AppCode))]; source != nil {
		// Yumi 这类外接 App 不属于 hyapp statistics-service 的事实域；命中外部源时直接读 dashboard-cdc-worker 的预聚合库。
		return source.StatisticsOverview(ctx, query)
	}
	values := url.Values{}
	values.Set("app_code", query.AppCode)
	if statTZ := strings.TrimSpace(query.StatTZ); statTZ != "" {
		values.Set("stat_tz", statTZ)
	}
	if query.StartMS > 0 {
		values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
	}
	if query.EndMS > 0 {
		values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
	}
	if query.SeriesStartMS > 0 {
		values.Set("series_start_ms", strconv.FormatInt(query.SeriesStartMS, 10))
	}
	if query.SeriesEndMS > 0 {
		values.Set("series_end_ms", strconv.FormatInt(query.SeriesEndMS, 10))
	}
	if query.RegionID > 0 {
		values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
	}
	if query.CountryID > 0 {
		values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
	}
	return s.statisticsGET(ctx, "/internal/v1/statistics/overview", values)
}

func (s *DashboardService) SelfGameStatisticsOverview(ctx context.Context, query SelfGameStatisticsQuery) (map[string]any, error) {
	values := url.Values{}
	values.Set("app_code", query.AppCode)
	if statTZ := strings.TrimSpace(query.StatTZ); statTZ != "" {
		values.Set("stat_tz", statTZ)
	}
	if query.StartMS > 0 {
		values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
	}
	if query.EndMS > 0 {
		values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
	}
	if query.RegionID > 0 {
		values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
	}
	if query.CountryID > 0 {
		values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
	}
	if gameID := strings.TrimSpace(query.GameID); gameID != "" && gameID != "all" {
		values.Set("game_id", gameID)
	}
	// 数据大屏只通过 statistics-service 读取自研游戏聚合指标，避免 admin 服务绕过聚合层去扫业务明细表。
	overview, err := s.statisticsGET(ctx, "/internal/v1/statistics/self-games/overview", values)
	if err != nil {
		return nil, err
	}
	// 高风险用户只对 statistics-service 已返回的 topN user_id 做批量资料补全；头像、昵称、短 ID 不进入统计库，避免聚合链路反查用户表。
	_ = s.enrichSelfGameRiskUsers(ctx, query, overview)
	return overview, nil
}

func (s *DashboardService) AppTrackingFunnel(ctx context.Context, query AppTrackingFunnelQuery) (map[string]any, error) {
	values := url.Values{}
	values.Set("app_code", query.AppCode)
	if statTZ := strings.TrimSpace(query.StatTZ); statTZ != "" {
		values.Set("stat_tz", statTZ)
	}
	if query.StartMS > 0 {
		values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
	}
	if query.EndMS > 0 {
		values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
	}
	if query.RegionID > 0 {
		values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
	} else if len(query.RegionIDs) > 0 {
		parts := make([]string, 0, len(query.RegionIDs))
		for _, regionID := range query.RegionIDs {
			if regionID > 0 {
				parts = append(parts, strconv.FormatInt(regionID, 10))
			}
		}
		if len(parts) > 0 {
			values.Set("region_ids", strings.Join(parts, ","))
		}
	}
	if query.CountryID > 0 {
		values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
	}
	return s.statisticsGET(ctx, "/internal/v1/statistics/app/funnel", values)
}

func (s *DashboardService) SocialRequirements(ctx context.Context, query SocialRequirementsQuery) (map[string]any, error) {
	values := url.Values{}
	values.Set("app_code", query.AppCode)
	if statTZ := strings.TrimSpace(query.StatTZ); statTZ != "" {
		values.Set("stat_tz", statTZ)
	}
	if query.StartMS > 0 {
		values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
	}
	if query.EndMS > 0 {
		values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
	}
	if query.RegionID > 0 {
		values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
	} else if len(query.RegionIDs) > 0 {
		parts := make([]string, 0, len(query.RegionIDs))
		for _, regionID := range query.RegionIDs {
			if regionID > 0 {
				parts = append(parts, strconv.FormatInt(regionID, 10))
			}
		}
		if len(parts) > 0 {
			values.Set("region_ids", strings.Join(parts, ","))
		}
	}
	if query.CountryID > 0 {
		values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
	}
	if section := strings.TrimSpace(query.Section); section != "" {
		values.Set("section", section)
	}
	if userRole := strings.TrimSpace(query.UserRole); userRole != "" {
		values.Set("user_role", userRole)
	}
	if payerType := strings.TrimSpace(query.PayerType); payerType != "" {
		values.Set("payer_type", payerType)
	}
	if newUserType := strings.TrimSpace(query.NewUserType); newUserType != "" {
		values.Set("new_user_type", newUserType)
	}
	// Social BI 数据需求只读 statistics-service 聚合投影；admin 不直接拼 owner service 明细或跨库查询。
	return s.statisticsGET(ctx, "/internal/v1/statistics/social/requirements", values)
}

func (s *DashboardService) RecordFinanceCoinSellerRechargeOrder(ctx context.Context, order model.CoinSellerRechargeOrder) error {
	if s == nil {
		return nil
	}
	// statistics-service 只接收已校验订单快照；admin 是财务工单事实 owner，不能让统计服务回查 admin 表。
	payload := map[string]any{
		"app_code":          order.AppCode,
		"event_id":          fmt.Sprintf("finance_coin_seller_recharge_order:%d", order.ID),
		"order_id":          order.ID,
		"verify_status":     order.VerifyStatus,
		"target_user_id":    order.TargetUserID,
		"target_country_id": order.TargetCountryID,
		"target_region_id":  order.TargetRegionID,
		"usd_minor":         order.USDMinorAmount,
		"coin_amount":       order.CoinAmount,
		// 统计切日必须使用运营选择的订单业务时间；历史数据通过模型方法回退到录入时间。
		"created_at_ms": order.BusinessTimeMS(),
	}
	return s.statisticsPOST(ctx, "/internal/v1/statistics/social/coin-seller-recharge-orders", payload)
}

func (s *DashboardService) PlatformGrantUsers(ctx context.Context, query PlatformGrantStatisticsQuery) (map[string]any, error) {
	page, err := s.statisticsGET(ctx, "/internal/v1/statistics/platform-grants/users", platformGrantValues(query, false))
	if err != nil {
		return nil, err
	}
	// 统计服务只返回用户 ID 和金币聚合；admin 层用 user-service 批量补展示资料，避免统计库复制用户主数据。
	_ = s.enrichPlatformGrantUsers(ctx, query, page)
	return page, nil
}

func (s *DashboardService) PlatformGrantRecords(ctx context.Context, query PlatformGrantStatisticsQuery) (map[string]any, error) {
	if query.UserID <= 0 {
		return nil, fmt.Errorf("user_id is required")
	}
	page, err := s.statisticsGET(ctx, "/internal/v1/statistics/platform-grants/records", platformGrantValues(query, true))
	if err != nil {
		return nil, err
	}
	stringifyPageUserIDs(page)
	return page, nil
}

func (s *DashboardService) statisticsGET(ctx context.Context, path string, values url.Values) (map[string]any, error) {
	baseURL := s.cfg.StatisticsService.BaseURL + path
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"?"+values.Encode(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := s.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("statistics service returned status %d", resp.StatusCode)
	}
	var out map[string]any
	decoder := json.NewDecoder(resp.Body)
	decoder.UseNumber()
	if err := decoder.Decode(&out); err != nil {
		return nil, err
	}
	return out, nil
}

func (s *DashboardService) statisticsPOST(ctx context.Context, path string, payload any) error {
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.cfg.StatisticsService.BaseURL+path, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := s.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		var out map[string]any
		_ = json.NewDecoder(resp.Body).Decode(&out)
		return fmt.Errorf("statistics service returned status %d: %v", resp.StatusCode, out["error"])
	}
	return nil
}

func platformGrantValues(query PlatformGrantStatisticsQuery, includeUser bool) url.Values {
	values := url.Values{}
	values.Set("app_code", query.AppCode)
	if statTZ := strings.TrimSpace(query.StatTZ); statTZ != "" {
		values.Set("stat_tz", statTZ)
	}
	if query.StartMS > 0 {
		values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
	}
	if query.EndMS > 0 {
		values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
	}
	if statDay := strings.TrimSpace(query.StatDay); statDay != "" {
		values.Set("stat_day", statDay)
	}
	if query.RegionID > 0 {
		values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
	}
	if query.CountryID > 0 {
		values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
	}
	if includeUser && query.UserID > 0 {
		values.Set("user_id", strconv.FormatInt(query.UserID, 10))
	}
	if source := strings.TrimSpace(query.Source); source != "" && source != "all" {
		values.Set("source", source)
	}
	if query.Page > 0 {
		values.Set("page", strconv.Itoa(query.Page))
	}
	if query.PageSize > 0 {
		values.Set("page_size", strconv.Itoa(query.PageSize))
	}
	return values
}

func (s *DashboardService) enrichPlatformGrantUsers(ctx context.Context, query PlatformGrantStatisticsQuery, page map[string]any) error {
	if s.user == nil || page == nil {
		return nil
	}
	rows, ok := page["items"].([]any)
	if !ok || len(rows) == 0 {
		return nil
	}
	userIDs := make([]int64, 0, len(rows))
	seen := map[int64]struct{}{}
	for _, raw := range rows {
		row, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		userID := anyInt64(row["user_id"])
		if userID <= 0 {
			continue
		}
		// user_id 可能超过 JS 安全整数，admin 统一转字符串输出，前端分页和后续来源查询都使用字符串透传。
		row["user_id"] = strconv.FormatInt(userID, 10)
		if _, exists := seen[userID]; exists {
			continue
		}
		seen[userID] = struct{}{}
		userIDs = append(userIDs, userID)
	}
	if len(userIDs) == 0 {
		return nil
	}
	users, err := s.user.BatchGetUsers(ctx, userclient.BatchGetUsersRequest{
		RequestID: query.RequestID,
		Caller:    "admin-server",
		UserIDs:   userIDs,
	})
	if err != nil {
		return err
	}
	for _, raw := range rows {
		row, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		userID := anyInt64(row["user_id"])
		user := users[userID]
		if user == nil {
			continue
		}
		userPayload := map[string]any{
			"user_id":         strconv.FormatInt(userID, 10),
			"avatar":          user.Avatar,
			"username":        user.Username,
			"display_user_id": user.DisplayUserID,
		}
		row["user"] = userPayload
		row["avatar"] = user.Avatar
		row["nickname"] = user.Username
		row["username"] = user.Username
		row["display_user_id"] = user.DisplayUserID
		row["short_id"] = user.DisplayUserID
	}
	return nil
}

func stringifyPageUserIDs(page map[string]any) {
	rows, ok := page["items"].([]any)
	if !ok {
		return
	}
	for _, raw := range rows {
		row, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		if userID := anyInt64(row["user_id"]); userID > 0 {
			row["user_id"] = strconv.FormatInt(userID, 10)
		}
	}
}

func (s *DashboardService) enrichSelfGameRiskUsers(ctx context.Context, query SelfGameStatisticsQuery, overview map[string]any) error {
	if s.user == nil || overview == nil {
		return nil
	}
	rows, ok := overview["user_risk"].([]any)
	if !ok || len(rows) == 0 {
		return nil
	}
	userIDs := make([]int64, 0, len(rows))
	seen := map[int64]struct{}{}
	for _, raw := range rows {
		row, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		userID := anyInt64(row["user_id"])
		if userID <= 0 {
			continue
		}
		// user_id 是 16 位以上业务 ID，admin 层统一改成字符串透给 H5，避免浏览器 JSON number 精度丢失。
		row["user_id"] = strconv.FormatInt(userID, 10)
		if _, exists := seen[userID]; exists {
			continue
		}
		seen[userID] = struct{}{}
		userIDs = append(userIDs, userID)
	}
	if len(userIDs) == 0 {
		return nil
	}
	users, err := s.user.BatchGetUsers(ctx, userclient.BatchGetUsersRequest{
		RequestID: query.RequestID,
		Caller:    "admin-server",
		UserIDs:   userIDs,
	})
	if err != nil {
		return err
	}
	for _, raw := range rows {
		row, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		user := users[anyInt64(row["user_id"])]
		if user == nil {
			continue
		}
		row["avatar"] = user.Avatar
		row["nickname"] = user.Username
		row["username"] = user.Username
		row["display_user_id"] = user.DisplayUserID
		row["short_id"] = user.DisplayUserID
	}
	return nil
}

func anyInt64(value any) int64 {
	switch typed := value.(type) {
	case int64:
		return typed
	case int:
		return int64(typed)
	case float64:
		return int64(typed)
	case json.Number:
		parsed, _ := typed.Int64()
		return parsed
	case string:
		parsed, _ := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
		return parsed
	default:
		return 0
	}
}
