package managerapi

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"sync"
	"time"

	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
)

// managerDashboardCacheTTL 是团队工资统计的展示缓存时长。
// 统计跨 user-service 组织树和 wallet-service 周期账户两次聚合，产品接受分钟级延迟展示，
// 用短 TTL 换掉每次进入经理中心都全量聚合的成本；缓存是单节点内存态，不做跨副本一致性。
const managerDashboardCacheTTL = 2 * time.Minute

// managerDashboardFailureCacheTTL 是聚合失败的负缓存时长：
// 下游持续故障时避免每个 overview 请求都重跑双聚合打崩上游，同时保证故障恢复后半分钟内回到正常展示。
const managerDashboardFailureCacheTTL = 30 * time.Second

// managerTeamAgencyPageSize 是一次拉取经理团队 Agency 的上限，与 user-service 服务端上限保持一致。
const managerTeamAgencyPageSize = 5000

type managerDashboardCacheEntry struct {
	data      map[string]any
	expiresAt time.Time
}

type managerDashboardCache struct {
	mu      sync.Mutex
	entries map[string]managerDashboardCacheEntry
}

func (c *managerDashboardCache) get(key string, now time.Time) (map[string]any, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	entry, ok := c.entries[key]
	if !ok {
		return nil, false
	}
	if now.After(entry.expiresAt) {
		delete(c.entries, key)
		return nil, false
	}
	// data 为 nil 表示负缓存命中：负缓存期内直接按“统计不可用”降级，不再重跑聚合。
	return entry.data, true
}

func (c *managerDashboardCache) set(key string, data map[string]any, now time.Time, ttl time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.entries == nil {
		c.entries = map[string]managerDashboardCacheEntry{}
	}
	// 顺手清理已过期条目；经理数量有限，线性清理成本可忽略，避免长期运行只进不出。
	for cacheKey, entry := range c.entries {
		if now.After(entry.expiresAt) {
			delete(c.entries, cacheKey)
		}
	}
	c.entries[key] = managerDashboardCacheEntry{data: data, expiresAt: now.Add(ttl)}
}

// managerDashboardAggregateTimeout 是团队统计两次聚合 RPC 的总预算。
// 概览主链路（身份+权限）已有十余次串行 RPC，统计只是增强信息，超时直接降级而不是拖垮整个 overview。
const managerDashboardAggregateTimeout = 5 * time.Second

// managerTeamDashboard 聚合经理团队工资统计卡片数据：
// user-service 解析经理整棵团队树的 Agency 收款人集合，wallet-service 按收款人聚合主播预收入工资。
// 统计失败时返回 nil 并记录日志，经理中心身份信息照常返回，H5 对缺失字段做占位展示。
func (h *Handler) managerTeamDashboard(request *http.Request, managerUserID int64) map[string]any {
	if h.userHostClient == nil || h.walletClient == nil {
		return nil
	}
	now := time.Now().UTC()
	cacheKey := fmt.Sprintf("%s:%d", appcode.FromContext(request.Context()), managerUserID)
	if cached, ok := h.dashboardCache.get(cacheKey, now); ok {
		return cached
	}
	ctx, cancel := context.WithTimeout(request.Context(), managerDashboardAggregateTimeout)
	defer cancel()
	request = request.WithContext(ctx)

	teamResp, err := h.userHostClient.ListManagerTeamAgencies(request.Context(), &userv1.ListManagerTeamAgenciesRequest{
		Meta:          httpkit.UserMeta(request, ""),
		ManagerUserId: managerUserID,
		PageSize:      managerTeamAgencyPageSize,
	})
	if err != nil {
		logx.Warn(request.Context(), "manager_team_agencies_failed", slog.Int64("manager_user_id", managerUserID))
		h.dashboardCache.set(cacheKey, nil, now, managerDashboardFailureCacheTTL)
		return nil
	}
	ownerIDs := make([]int64, 0, len(teamResp.GetAgencies()))
	seen := map[int64]bool{}
	for _, agency := range teamResp.GetAgencies() {
		if id := agency.GetOwnerUserId(); id > 0 && !seen[id] {
			seen[id] = true
			ownerIDs = append(ownerIDs, id)
		}
	}

	// 工资周期与结算保持同一口径：UTC 月。上月用月初回退一个月计算，避免 AddDate 在月末产生进位漂移。
	monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
	thisCycle := monthStart.Format("2006-01")
	lastCycle := monthStart.AddDate(0, -1, 0).Format("2006-01")
	statsResp, err := h.walletClient.GetTeamHostSalaryStats(request.Context(), &walletv1.GetTeamHostSalaryStatsRequest{
		RequestId:          httpkit.RequestIDFromContext(request.Context()),
		AppCode:            appcode.FromContext(request.Context()),
		AgencyOwnerUserIds: ownerIDs,
		CycleKeys:          []string{thisCycle, lastCycle},
		NowMs:              now.UnixMilli(),
	})
	if err != nil {
		logx.Warn(request.Context(), "manager_team_salary_stats_failed", slog.Int64("manager_user_id", managerUserID))
		h.dashboardCache.set(cacheKey, nil, now, managerDashboardFailureCacheTTL)
		return nil
	}
	statsByCycle := map[string]*walletv1.TeamHostSalaryCycleStat{}
	for _, stat := range statsResp.GetStats() {
		statsByCycle[stat.GetCycleKey()] = stat
	}
	thisStat := statsByCycle[thisCycle]
	lastStat := statsByCycle[lastCycle]

	dashboard := map[string]any{
		"month_label":    monthStart.Format("Jan 2006"),
		"cycle_key":      thisCycle,
		"last_cycle_key": lastCycle,
		// 金额下发美元数值（分转元），H5 formatMoney 负责千分位和货币符号。
		"this_month_salary":           usdMinorToDollars(thisStat.GetEstimatedHostSalaryUsdMinor()),
		"this_month_salary_usd_minor": thisStat.GetEstimatedHostSalaryUsdMinor(),
		"last_month_salary":           usdMinorToDollars(lastStat.GetEstimatedHostSalaryUsdMinor()),
		"last_month_salary_usd_minor": lastStat.GetEstimatedHostSalaryUsdMinor(),
		"active_hosts":                thisStat.GetActiveHostCount(),
		"active_hosts_last_month":     lastStat.GetActiveHostCount(),
		// growth 直接下发带符号百分比字符串，避免 H5 formatPercent 对 0~1 数值的“小数当比例”歧义。
		"growth":         formatGrowthPercent(thisStat.GetEstimatedHostSalaryUsdMinor(), lastStat.GetEstimatedHostSalaryUsdMinor()),
		"team_bd_leaders": teamResp.GetTotalBdLeaders(),
		"team_bds":        teamResp.GetTotalBds(),
		"team_agencies":   len(teamResp.GetAgencies()),
		// truncated 时统计口径不完整（团队 Agency 超过单次拉取上限），透传给前端而不是静默当全量。
		"team_stats_truncated": teamResp.GetTruncated(),
		"updated_at_ms":        now.UnixMilli(),
	}
	h.dashboardCache.set(cacheKey, dashboard, now, managerDashboardCacheTTL)
	return dashboard
}

func usdMinorToDollars(minor int64) float64 {
	return float64(minor) / 100
}

// formatGrowthPercent 计算本月对上月的环比：上月为 0 时有收入记 +100%，无收入记 0%。
func formatGrowthPercent(current int64, previous int64) string {
	if previous <= 0 {
		if current > 0 {
			return "+100%"
		}
		return "0%"
	}
	percent := (float64(current) - float64(previous)) / float64(previous) * 100
	rounded := int64(percent)
	if percent > 0 {
		rounded = int64(percent + 0.5)
		return fmt.Sprintf("+%d%%", rounded)
	}
	if percent < 0 {
		rounded = int64(percent - 0.5)
	}
	return fmt.Sprintf("%d%%", rounded)
}
