package appuser

import (
	"database/sql"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"strconv"
	"strings"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/config"
	"hyapp-admin-server/internal/integration/activityclient"
	"hyapp-admin-server/internal/integration/userclient"
	"hyapp-admin-server/internal/middleware"
	"hyapp-admin-server/internal/modules/shared"
	"hyapp-admin-server/internal/response"

	"github.com/gin-gonic/gin"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type Handler struct {
	service *Service
	cfg     config.Config
	audit   shared.OperationLogger
}

func New(userClient userclient.Client, activityClient activityclient.Client, adminDB *sql.DB, userDB *sql.DB, walletDB *sql.DB, cfg config.Config, audit shared.OperationLogger, opts ...ServiceOption) *Handler {
	return &Handler{
		service: NewService(userClient, activityClient, adminDB, userDB, walletDB, opts...),
		cfg:     cfg,
		audit:   audit,
	}
}

// ExternalTeamClient exposes only the portal-specific owner-service contract to the
// external route assembler; generic App user handlers retain their existing Client.
func (h *Handler) ExternalTeamClient() userclient.ExternalTeamClient {
	if h == nil || h.service == nil {
		return nil
	}
	client, _ := h.service.userClient.(userclient.ExternalTeamClient)
	return client
}

func (h *Handler) ListUsers(c *gin.Context) {
	query := parseListQuery(c)
	items, total, err := h.service.ListUsers(c.Request.Context(), query)
	if err != nil {
		response.ServerError(c, "获取 App 用户列表失败")
		return
	}
	response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
}

func (h *Handler) ListLoginLogs(c *gin.Context) {
	query := parseLoginLogQuery(c)
	items, total, err := h.service.ListLoginLogs(c.Request.Context(), query)
	if err != nil {
		response.ServerError(c, "获取登录日志失败")
		return
	}
	response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
}

func (h *Handler) ListBannedUsers(c *gin.Context) {
	query := parseBanListQuery(c)
	items, total, err := h.service.ListBannedUsers(c.Request.Context(), query)
	if err != nil {
		response.ServerError(c, "获取封禁列表失败")
		return
	}
	response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
}

func (h *Handler) ListUserContentBlacklist(c *gin.Context) {
	options := shared.ListOptions(c)
	items, total, err := h.service.ListUserContentBlacklist(
		c.Request.Context(),
		options.Page,
		options.PageSize,
		middleware.CurrentRequestID(c),
	)
	if err != nil {
		writeReadError(c, err, "获取黑名单屏蔽用户失败")
		return
	}
	response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: total})
}

func (h *Handler) ResolveUserContentBlacklistCandidate(c *gin.Context) {
	identifier := firstQuery(c, "user_id", "userId", "identifier")
	candidate, err := h.service.ResolveUserContentBlacklistCandidate(c.Request.Context(), identifier, middleware.CurrentRequestID(c))
	if err != nil {
		// 搜索输入和未找到都需要给管理员可操作的反馈；gRPC 业务错误按原状态映射。
		writeMutationError(c, err, "搜索 App 用户失败")
		return
	}
	response.OK(c, candidate)
}

func (h *Handler) AddUserContentBlacklist(c *gin.Context) {
	var req contentBlacklistMutationRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "黑名单用户参数不正确")
		return
	}
	item, err := h.service.AddUserContentBlacklist(
		c.Request.Context(),
		req.UserIdentifier,
		int64(middleware.CurrentUserID(c)),
		req.Reason,
		middleware.CurrentRequestID(c),
	)
	if err != nil {
		writeMutationError(c, err, "添加黑名单屏蔽用户失败")
		return
	}
	userID, _ := strconv.ParseInt(item.Target.UserID, 10, 64)
	writeAppUserAuditLog(c, h.audit, "add-user-content-blacklist", "user_content_blacklist", userID, "success", strings.TrimSpace(item.Reason))
	response.OK(c, item)
}

func (h *Handler) RemoveUserContentBlacklist(c *gin.Context) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	var req contentBlacklistMutationRequest
	if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
		response.BadRequest(c, "移除黑名单参数不正确")
		return
	}
	reason := strings.TrimSpace(req.Reason)
	if reason == "" {
		reason = "admin_remove_content_blacklist"
	}
	removed, err := h.service.RemoveUserContentBlacklist(
		c.Request.Context(),
		userID,
		int64(middleware.CurrentUserID(c)),
		reason,
		middleware.CurrentRequestID(c),
	)
	if err != nil {
		writeMutationError(c, err, "移除黑名单屏蔽用户失败")
		return
	}
	writeAppUserAuditLog(c, h.audit, "remove-user-content-blacklist", "user_content_blacklist", userID, "success", reason)
	response.OK(c, gin.H{"removed": removed})
}

func (h *Handler) ListUserLoginLogs(c *gin.Context) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	query := parseLoginLogQuery(c)
	query.UserID = userID
	items, total, err := h.service.ListLoginLogs(c.Request.Context(), query)
	if err != nil {
		response.ServerError(c, "获取用户登录日志失败")
		return
	}
	response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
}

func (h *Handler) GetUser(c *gin.Context) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	user, err := h.service.GetUser(c.Request.Context(), userID)
	if err != nil {
		writeReadError(c, err, "获取 App 用户失败")
		return
	}
	response.OK(c, user)
}

func (h *Handler) UpdateUser(c *gin.Context) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	var req updateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "用户参数不正确")
		return
	}
	if _, external := appctx.ExternalScopeFromContext(c.Request.Context()); external && req.Country != nil {
		// Country is the canonical input for user-service region reassignment. Portal
		// accounts may edit profile fields, but must never move a target across their
		// server-derived region boundary through the shared main-admin handler.
		response.Forbidden(c, "外管不能修改用户所属国家或区域")
		return
	}
	user, err := h.service.UpdateUser(c.Request.Context(), userID, int64(middleware.CurrentUserID(c)), middleware.CurrentRequestID(c), req)
	if err != nil {
		writeMutationError(c, err, "更新 App 用户失败")
		return
	}
	detail := "update profile"
	if req.Country != nil {
		detail = fmt.Sprintf("update country=%s region_id=%d", user.Country, user.RegionID)
	}
	writeAppUserAuditLog(c, h.audit, "update-app-user", "app_users", userID, "success", detail)
	response.OK(c, user)
}

func (h *Handler) BanUser(c *gin.Context) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	var req banUserRequest
	if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
		response.BadRequest(c, "封禁参数不正确")
		return
	}
	result, err := h.service.AdminBanUser(c.Request.Context(), userID, int64(middleware.CurrentUserID(c)), middleware.CurrentRequestID(c), req)
	if err != nil {
		writeMutationError(c, err, "封禁 App 用户失败")
		return
	}
	detail := "permanent"
	if req.ExpiresAtMs > 0 {
		detail = fmt.Sprintf("expires_at_ms=%d", req.ExpiresAtMs)
	}
	writeAppUserAuditLog(c, h.audit, "ban-app-user", "app_users", userID, "success", detail)
	response.OK(c, result)
}

func (h *Handler) UnbanUser(c *gin.Context) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	var req unbanUserRequest
	if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
		response.BadRequest(c, "解封参数不正确")
		return
	}
	result, err := h.service.AdminUnbanUser(c.Request.Context(), userID, int64(middleware.CurrentUserID(c)), middleware.CurrentRequestID(c), req)
	if err != nil {
		writeMutationError(c, err, "解封 App 用户失败")
		return
	}
	writeAppUserAuditLog(c, h.audit, "unban-app-user", "app_users", userID, "success", strings.TrimSpace(req.Reason))
	response.OK(c, result)
}

func (h *Handler) AdjustTemporaryLevels(c *gin.Context) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	var req adjustTemporaryLevelsRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "等级调整参数不正确")
		return
	}
	user, err := h.service.AdjustTemporaryLevels(c.Request.Context(), userID, int64(middleware.CurrentUserID(c)), middleware.CurrentRequestID(c), req)
	if err != nil {
		writeMutationError(c, err, "调整用户等级失败")
		return
	}
	writeAppUserAuditLog(c, h.audit, "adjust-app-user-level", "app_users", userID, "success", fmt.Sprintf("tracks=%d", len(req.Adjustments)))
	response.OK(c, user)
}

func (h *Handler) SetPassword(c *gin.Context) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	var req setPasswordRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "密码参数不正确")
		return
	}
	if err := h.service.SetPassword(c.Request.Context(), userID, req.Password); err != nil {
		writeMutationError(c, err, "设置 App 用户密码失败")
		return
	}
	writeAppUserAuditLog(c, h.audit, "set-app-user-password", "app_users", userID, "success", "set password")
	response.OK(c, gin.H{"passwordSet": true})
}

func (h *Handler) IssueAccessToken(c *gin.Context) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	result, err := h.service.IssueAccessToken(c.Request.Context(), userID)
	if err != nil {
		if grpcStatus, ok := status.FromError(err); ok && grpcStatus.Code() == codes.NotFound {
			// 无活跃会话是明确业务事实：token 是无状态 JWT，没有 active session 就没有可重签对象。
			response.NotFound(c, "该用户当前没有活跃会话")
			return
		}
		writeMutationError(c, err, "获取用户 Token 失败")
		return
	}
	// 重签出的 token 等价于账号接管能力，且 user-service 侧刻意不写 login_audit——
	// 后台操作日志是唯一审计痕迹，所以这里 fail-closed：审计落库失败就不返回 token。
	if err := shared.OperationLogWithResourceIDStrict(c, h.audit, "issue-app-user-token", "app_users", fmt.Sprintf("%d", userID), "success", fmt.Sprintf("session_id=%s", result.SessionID)); err != nil {
		response.ServerError(c, "操作日志写入失败，未返回 Token")
		return
	}
	response.OK(c, result)
}

func (h *Handler) setUserStatus(c *gin.Context, status string, action string, successMessage string) {
	userID, ok := parseInt64ID(c, "id")
	if !ok {
		return
	}
	result, err := h.service.SetUserStatus(c.Request.Context(), userID, status, int64(middleware.CurrentUserID(c)), middleware.CurrentRequestID(c))
	if err != nil {
		writeMutationError(c, err, successMessage+"失败")
		return
	}
	writeAppUserAuditLog(c, h.audit, action, "app_users", userID, "success", fmt.Sprintf("status=%s", status))
	response.OK(c, result)
}

func parseListQuery(c *gin.Context) listQuery {
	options := shared.ListOptions(c)
	regionID, regionIDSet := queryOptionalInt64(c, "region_id", "regionId")
	return normalizeListQuery(listQuery{
		Page:          options.Page,
		PageSize:      options.PageSize,
		Country:       firstQuery(c, "country", "country_code", "countryCode"),
		Keyword:       options.Keyword,
		UserFilter:    shared.UserIdentityFilterFromQuery(c, ""),
		RegionID:      regionID,
		RegionIDSet:   regionIDSet,
		Status:        options.Status,
		SortBy:        firstQuery(c, "sort_by", "sortBy"),
		SortDirection: firstQuery(c, "sort_direction", "sortDirection", "order"),
		StartMs:       queryInt64(c, "start_ms", "startMs", "start_at_ms", "startAtMs"),
		EndMs:         queryInt64(c, "end_ms", "endMs", "end_at_ms", "endAtMs"),
	})
}

func parseLoginLogQuery(c *gin.Context) loginLogQuery {
	options := shared.ListOptions(c)
	regionID, regionIDSet := queryOptionalInt64(c, "region_id", "regionId")
	return normalizeLoginLogQuery(loginLogQuery{
		Page:          options.Page,
		PageSize:      options.PageSize,
		Keyword:       options.Keyword,
		UserFilter:    shared.UserIdentityFilterFromQuery(c, ""),
		UserID:        queryInt64(c, "user_id", "userId"),
		DisplayUserID: firstQuery(c, "display_user_id", "displayUserId"),
		IP:            firstQuery(c, "ip"),
		IPCountryCode: firstQuery(c, "ip_country_code", "ipCountryCode"),
		Channel:       firstQuery(c, "channel"),
		Platform:      firstQuery(c, "platform"),
		Result:        firstQuery(c, "result"),
		Blocked:       firstQuery(c, "blocked"),
		RegionID:      regionID,
		RegionIDSet:   regionIDSet,
		StartMs:       queryInt64(c, "start_ms", "startMs"),
		EndMs:         queryInt64(c, "end_ms", "endMs"),
	})
}

func parseBanListQuery(c *gin.Context) banListQuery {
	options := shared.ListOptions(c)
	return normalizeBanListQuery(banListQuery{
		Page:                 options.Page,
		PageSize:             options.PageSize,
		TargetKeyword:        firstQuery(c, "target_keyword", "targetKeyword", "keyword"),
		TargetFilter:         shared.UserIdentityFilterFromQuery(c, "target"),
		OperatorKeyword:      firstQuery(c, "operator_keyword", "operatorKeyword"),
		OperatorFilter:       shared.UserIdentityFilterFromQuery(c, "operator"),
		OperatorAdminKeyword: firstQuery(c, "operator_admin_keyword", "operatorAdminKeyword"),
		StartMs:              queryInt64(c, "start_ms", "startMs"),
		EndMs:                queryInt64(c, "end_ms", "endMs"),
	})
}

func firstQuery(c *gin.Context, names ...string) string {
	for _, name := range names {
		if value := strings.TrimSpace(c.Query(name)); value != "" {
			return value
		}
	}
	return ""
}

func queryInt64(c *gin.Context, names ...string) int64 {
	value := firstQuery(c, names...)
	if value == "" {
		return 0
	}
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil || parsed < 0 {
		return 0
	}
	return parsed
}

func queryOptionalInt64(c *gin.Context, names ...string) (int64, bool) {
	value := firstQuery(c, names...)
	if value == "" {
		return 0, false
	}
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil || parsed < 0 {
		return 0, false
	}
	return parsed, true
}

func parseInt64ID(c *gin.Context, name string) (int64, bool) {
	raw := strings.TrimSpace(c.Param(name))
	id, err := strconv.ParseInt(raw, 10, 64)
	if err != nil || id <= 0 {
		response.BadRequest(c, "ID 参数不正确")
		return 0, false
	}
	return id, true
}

func writeReadError(c *gin.Context, err error, fallback string) {
	if errors.Is(err, ErrNotFound) {
		response.NotFound(c, "App 用户不存在")
		return
	}
	// 读路径对外只回兜底文案、不透出内部细节；原始错误必须落日志，否则详情页 500 无从排查。
	slog.ErrorContext(c.Request.Context(), "admin_app_user_read_failed",
		slog.String("path", c.Request.URL.Path),
		slog.String("error", err.Error()),
	)
	response.ServerError(c, fallback)
}

func writeMutationError(c *gin.Context, err error, fallback string) {
	if errors.Is(err, ErrNotFound) {
		response.NotFound(c, "App 用户不存在")
		return
	}
	if errors.Is(err, ErrInvalidArgument) {
		response.BadRequest(c, err.Error())
		return
	}
	if grpcStatus, ok := status.FromError(err); ok {
		message := strings.TrimSpace(grpcStatus.Message())
		if message == "" {
			message = fallback
		}
		switch grpcStatus.Code() {
		case codes.InvalidArgument, codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
			// 等级目标不高于真实等级、规则缺失或仍有其他封禁都属于可修正的业务拒绝，不能伪装成 500。
			response.BadRequest(c, message)
		case codes.NotFound:
			response.NotFound(c, message)
		default:
			response.ServerError(c, fallback)
		}
		return
	}
	response.ServerError(c, fallback)
}

func writeAppUserAuditLog(c *gin.Context, logger shared.OperationLogger, action string, resource string, resourceID int64, status string, detail string) {
	shared.OperationLogWithResourceID(c, logger, action, resource, fmt.Sprintf("%d", resourceID), status, detail)
}
