package callbackapi

import (
	"context"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
	"io"
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/imgroup"
	"hyapp/pkg/xerr"
)

const (
	tencentCallbackJoinGroup = "Group.CallbackBeforeApplyJoinGroup"
	tencentCallbackSendMsg   = "Group.CallbackBeforeSendMsg"
	tencentCallbackC2CSent   = "C2C.CallbackAfterSendMsg"
)

// tencentIMCallbackBody 覆盖本阶段需要处理的腾讯云 IM 群回调字段。
// 字段名保持腾讯云原始大小写，避免在 transport 层引入二次映射歧义。
type tencentIMCallbackBody struct {
	CallbackCommand string `json:"CallbackCommand"`
	GroupID         string `json:"GroupId"`
	Requestor       string `json:"Requestor_Account"`
	FromAccount     string `json:"From_Account"`
	OperatorAccount string `json:"Operator_Account"`
	ToAccount       string `json:"To_Account"`
	MsgKey          string `json:"MsgKey"`
	MsgSeq          int64  `json:"MsgSeq"`
	MsgRandom       int64  `json:"MsgRandom"`
	MsgTime         int64  `json:"MsgTime"`
}

// tencentIMCallbackResponse 是腾讯云 IM 回调要求的原生响应结构。
// 该接口不走项目 envelope，因为腾讯云只识别 ActionStatus/ErrorCode/ErrorInfo。
type tencentIMCallbackResponse struct {
	ActionStatus string `json:"ActionStatus"`
	ErrorInfo    string `json:"ErrorInfo"`
	ErrorCode    int    `json:"ErrorCode"`
}

// handleTencentIMCallback 处理腾讯云 IM 服务端回调。
// gateway 只校验来源、解析腾讯字段并回查 room-service guard，不在本地保存房间状态。
func (h *Handler) handleTencentIMCallback(writer http.ResponseWriter, request *http.Request) {
	if request.Method != http.MethodPost {
		writeTencentCallback(writer, http.StatusMethodNotAllowed, 1, "method not allowed")
		return
	}

	if !h.tencentIMCallbackAuthenticated(request) {
		// 回调鉴权失败必须 fail-closed，不能让未知来源绕过房间 presence 守卫。
		writeTencentCallback(writer, http.StatusOK, 1, "callback authentication failed")
		return
	}

	if !h.tencentIMSDKAppIDMatched(request) {
		// SDKAppID 不匹配代表回调不属于当前应用，直接拒绝该次腾讯云动作。
		writeTencentCallback(writer, http.StatusOK, 1, "sdk_app_id mismatch")
		return
	}

	queryCommand := strings.TrimSpace(request.URL.Query().Get("CallbackCommand"))
	body, bodyValid := decodeTencentIMCallbackBody(request.Body)
	if !bodyValid {
		if queryCommand != "" && !knownTencentIMCallback(queryCommand) {
			// 未知回调按计划放行；即使 body 不是本服务认识的结构，也不阻断腾讯云新增能力。
			writeTencentCallback(writer, http.StatusOK, 0, "")
			return
		}
		writeTencentCallback(writer, http.StatusOK, 1, "invalid callback body")
		return
	}

	command, ok := resolveTencentIMCallbackCommand(queryCommand, body.CallbackCommand)
	if !ok {
		writeTencentCallback(writer, http.StatusOK, 1, "invalid callback command")
		return
	}
	if !knownTencentIMCallback(command) {
		// 白名单外回调只做来源校验和 SDKAppID 校验，避免未知回调阻断腾讯云 IM 主链路。
		writeTencentCallback(writer, http.StatusOK, 0, "")
		return
	}

	switch command {
	case tencentCallbackJoinGroup:
		h.handleTencentIMJoinCallback(writer, request, body)
	case tencentCallbackSendMsg:
		// 公屏禁言已经改为 room_outbox 异步调用腾讯 IM forbid_send_msg。
		// 即使控制台误开启旧回调，gateway 也必须纯透传，不能再引入 room-service/数据库同步依赖。
		writeTencentCallback(writer, http.StatusOK, 0, "")
	case tencentCallbackC2CSent:
		h.handleTencentIMC2CSentCallback(writer, request, body)
	default:
		writeTencentCallback(writer, http.StatusOK, 0, "")
	}
}

func (h *Handler) handleTencentIMC2CSentCallback(writer http.ResponseWriter, request *http.Request, body tencentIMCallbackBody) {
	// 服务端系统通知不属于主播“收到私信的人数”，否则批量余额/VIP 通知会污染运营指标。
	if h.tencentIMCallbackSenderIsServer(body) {
		writeTencentCallback(writer, http.StatusOK, 0, "")
		return
	}
	senderUserID, senderOK := parseTencentIMUserID(body.FromAccount)
	targetUserID, targetOK := parseTencentIMUserID(body.ToAccount)
	callbackAppCode := appcode.Normalize(request.URL.Query().Get("app_code"))
	if callbackAppCode == "" {
		callbackAppCode = appcode.FromContext(request.Context())
	}
	recorder, recorderOK := h.userHostClient.(interface {
		RecordPrivateMessageEvent(context.Context, *userv1.RecordPrivateMessageEventRequest) (*userv1.RecordPrivateMessageEventResponse, error)
	})
	if !senderOK || !targetOK || senderUserID == targetUserID || !recorderOK {
		// 统计旁路不能阻断用户已经发送成功的私信；缺配置只记告警，腾讯仍收到成功响应。
		slog.Warn("tencent_im_private_message_stats_skipped", "app_code", callbackAppCode, "from", body.FromAccount, "to", body.ToAccount)
		writeTencentCallback(writer, http.StatusOK, 0, "")
		return
	}
	occurredAtMS := body.MsgTime * 1000
	if occurredAtMS <= 0 {
		occurredAtMS = time.Now().UTC().UnixMilli()
	}
	eventID := strings.TrimSpace(body.MsgKey)
	if eventID == "" {
		eventID = fmt.Sprintf("c2c:%d:%d:%d:%d:%d", senderUserID, targetUserID, body.MsgSeq, body.MsgRandom, occurredAtMS)
	}
	_, err := recorder.RecordPrivateMessageEvent(appcode.WithContext(request.Context(), callbackAppCode), &userv1.RecordPrivateMessageEventRequest{
		Meta: &userv1.RequestMeta{
			RequestId: httpkit.RequestIDFromContext(request.Context()),
			Caller:    "gateway-service",
			SentAtMs:  time.Now().UTC().UnixMilli(),
			AppCode:   callbackAppCode,
		},
		EventId:      eventID,
		SenderUserId: senderUserID,
		TargetUserId: targetUserID,
		OccurredAtMs: occurredAtMS,
	})
	if err != nil {
		slog.Warn("tencent_im_private_message_stats_failed", "app_code", callbackAppCode, "event_id", eventID, "error", err)
	}
	writeTencentCallback(writer, http.StatusOK, 0, "")
}

func (h *Handler) handleTencentIMJoinCallback(writer http.ResponseWriter, request *http.Request, body tencentIMCallbackBody) {
	parsed := imgroup.ParseWithPrefix(body.GroupID, h.tencentIM.GroupIDPrefix)
	switch parsed.Kind {
	case imgroup.KindGlobalBroadcast, imgroup.KindRegionBroadcast:
		userID, ok := parseTencentIMUserID(body.Requestor)
		if !ok {
			writeTencentCallback(writer, http.StatusOK, 1, "join guard input invalid")
			return
		}
		// 播报群不属于 Room Cell，准入只看 user-service 的用户状态、资料完成度、app_code 和区域归属。
		h.handleTencentIMBroadcastJoinCallback(writer, request, parsed, userID)
		return
	case imgroup.KindRoom:
		callbackAppCode, managed, err := h.resolveTencentIMRoomOwnership(request, parsed.RoomID)
		if err != nil {
			h.logTencentIMRoomCallbackDecision(request, tencentCallbackJoinGroup, body.GroupID, "rejected", "room_owner_unavailable", "")
			writeTencentCallback(writer, http.StatusOK, 1, "room guard unavailable")
			return
		}
		if !managed {
			// 腾讯 IM 回调按 SDKAppID 全局生效；共享 SDKAppID 下，非本服务房间必须透传给实际项目。
			h.logTencentIMRoomCallbackDecision(request, tencentCallbackJoinGroup, body.GroupID, "passthrough", "room_not_managed", "")
			writeTencentCallback(writer, http.StatusOK, 0, "")
			return
		}
		userID, ok := parseTencentIMUserID(body.Requestor)
		if !ok {
			writeTencentCallback(writer, http.StatusOK, 1, "join guard input invalid")
			return
		}
		// 原始 room GroupID 没有租户前缀，必须先由 room-service 持久事实解析真实 app_code 再校验 presence。
		h.handleTencentIMRoomJoinCallback(writer, request, parsed.RoomID, userID, callbackAppCode)
		return
	default:
		if h.tencentIMGroupIDReserved(body.GroupID) {
			h.logTencentIMRoomCallbackDecision(request, tencentCallbackJoinGroup, body.GroupID, "rejected", "reserved_group_invalid", "")
			writeTencentCallback(writer, http.StatusOK, 1, "group_id is invalid")
			return
		}
		// 不属于当前环境 HyApp 保留命名空间的群由共享 SDKAppID 中的其它项目负责，当前回调不能阻断。
		h.logTencentIMRoomCallbackDecision(request, tencentCallbackJoinGroup, body.GroupID, "passthrough", "group_not_managed", "")
		writeTencentCallback(writer, http.StatusOK, 0, "")
		return
	}
}

func (h *Handler) handleTencentIMRoomJoinCallback(writer http.ResponseWriter, request *http.Request, roomID string, userID int64, callbackAppCode string) {
	if roomID == "" || h.roomGuardClient == nil {
		writeTencentCallback(writer, http.StatusOK, 1, "join guard input invalid")
		return
	}
	resp, err := h.roomGuardClient.VerifyRoomPresence(appcode.WithContext(request.Context(), callbackAppCode), &roomv1.VerifyRoomPresenceRequest{
		RoomId:    roomID,
		UserId:    userID,
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   callbackAppCode,
	})
	if err != nil || !resp.GetPresent() {
		h.logTencentIMRoomCallbackDecision(request, tencentCallbackJoinGroup, roomID, "rejected", tencentGuardErrorInfo(err, resp.GetReason()), callbackAppCode)
		writeTencentCallback(writer, http.StatusOK, 1, tencentGuardErrorInfo(err, resp.GetReason()))
		return
	}

	h.logTencentIMRoomCallbackDecision(request, tencentCallbackJoinGroup, roomID, "allowed", "presence_verified", callbackAppCode)
	writeTencentCallback(writer, http.StatusOK, 0, "")
}

func (h *Handler) handleTencentIMBroadcastJoinCallback(writer http.ResponseWriter, request *http.Request, parsed imgroup.ParsedGroup, userID int64) {
	user, err := h.tencentIMCallbackUser(request, parsed.AppCode, userID)
	if err != nil {
		writeTencentCallback(writer, http.StatusOK, 1, err.Error())
		return
	}
	if parsed.Kind == imgroup.KindRegionBroadcast && user.GetRegionId() != parsed.RegionID {
		// 区域群只允许加入当前 user-service 归属区域；客户端缓存、IP 或自报参数都不可信。
		writeTencentCallback(writer, http.StatusOK, 1, "region mismatch")
		return
	}

	writeTencentCallback(writer, http.StatusOK, 0, "")
}

// resolveTencentIMRoomOwnership 只把 room-service 持久表中存在的 room_id 认作本项目房间。
// NOT_FOUND 是共享 SDKAppID 的正常外部流量；其余错误可能隐藏真实 HyApp 房间，必须保持 fail-closed。
func (h *Handler) resolveTencentIMRoomOwnership(request *http.Request, roomID string) (string, bool, error) {
	if h.roomGuardClient == nil {
		return "", false, fmt.Errorf("room guard unavailable")
	}
	resp, err := h.roomGuardClient.ResolveRoomAppCode(request.Context(), &roomv1.ResolveRoomAppCodeRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		RoomId:    roomID,
	})
	if err != nil {
		if xerr.ReasonFromGRPC(err) == xerr.NotFound {
			return "", false, nil
		}
		return "", false, err
	}
	callbackAppCode := appcode.Normalize(resp.GetAppCode())
	if strings.TrimSpace(resp.GetAppCode()) == "" {
		return "", false, fmt.Errorf("room app_code is empty")
	}
	return callbackAppCode, true, nil
}

// tencentIMGroupIDReserved 只保留当前环境自己的播报群命名空间。
// 配置了环境前缀时，未携带该前缀的同名群属于其它环境或项目，不能被当前回调拒绝。
func (h *Handler) tencentIMGroupIDReserved(groupID string) bool {
	groupID = strings.TrimSpace(groupID)
	prefix := strings.TrimSpace(h.tencentIM.GroupIDPrefix)
	if prefix != "" {
		var ok bool
		groupID, ok = strings.CutPrefix(groupID, prefix)
		if !ok {
			return false
		}
	}
	return strings.HasPrefix(groupID, "hy_") && strings.Contains(groupID, "_bc_")
}

// logTencentIMRoomCallbackDecision 记录可审计但不含签名、消息正文和账号的路由结果。
// HTTP access log 只能看到 200；该业务日志用于区分腾讯响应体中的允许、拒绝和共享项目透传。
func (h *Handler) logTencentIMRoomCallbackDecision(request *http.Request, command string, groupID string, decision string, reason string, callbackAppCode string) {
	loggedAppCode := strings.TrimSpace(callbackAppCode)
	if loggedAppCode != "" {
		loggedAppCode = appcode.Normalize(loggedAppCode)
	}
	slog.LogAttrs(request.Context(), slog.LevelInfo, "tencent_im_room_callback_decision",
		slog.String("request_id", httpkit.RequestIDFromContext(request.Context())),
		slog.String("callback_command", command),
		slog.String("group_id", strings.TrimSpace(groupID)),
		slog.String("app_code", loggedAppCode),
		slog.String("decision", decision),
		slog.String("reason", strings.TrimSpace(reason)),
		slog.String("opt_platform", strings.TrimSpace(request.URL.Query().Get("OptPlatform"))),
	)
}

func (h *Handler) tencentIMCallbackUser(request *http.Request, callbackAppCode string, userID int64) (*userv1.User, error) {
	if h.userProfileClient == nil {
		return nil, callbackError("user-service unavailable")
	}
	// 回调路径不信任客户端 join 参数，每次都回查 user-service 当前用户事实。
	resp, err := h.userProfileClient.GetUser(appcode.WithContext(request.Context(), callbackAppCode), &userv1.GetUserRequest{
		Meta: &userv1.RequestMeta{
			RequestId: httpkit.RequestIDFromContext(request.Context()),
			Caller:    "gateway-service",
			SentAtMs:  time.Now().UnixMilli(),
			AppCode:   callbackAppCode,
		},
		UserId: userID,
	})
	if err != nil || resp.GetUser() == nil {
		return nil, callbackError("user-service unavailable")
	}
	user := resp.GetUser()
	if !user.GetProfileCompleted() {
		return nil, callbackError("profile required")
	}
	if user.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE {
		return nil, callbackError("user is not active")
	}
	if user.GetAppCode() != "" && appcode.Normalize(user.GetAppCode()) != callbackAppCode {
		// callbackAppCode 来自 GroupID 解析结果，用户 app_code 不匹配时拒绝跨租户入群。
		return nil, callbackError("app_code mismatch")
	}
	return user, nil
}

func (h *Handler) tencentIMCallbackSenderIsServer(body tencentIMCallbackBody) bool {
	admin := strings.TrimSpace(h.tencentIM.AdminIdentifier)
	if admin == "" {
		return false
	}
	return subtleStringEqual(body.FromAccount, admin) || subtleStringEqual(body.OperatorAccount, admin)
}

type callbackError string

func (e callbackError) Error() string {
	return string(e)
}

func (h *Handler) tencentIMCallbackAuthenticated(request *http.Request) bool {
	token := strings.TrimSpace(h.tencentIM.CallbackAuthToken)
	if token == "" {
		// 本地未配置回调鉴权 token 时允许调试，线上配置非空后自动 fail-closed。
		return true
	}

	if subtleStringEqual(request.URL.Query().Get("CallbackAuthToken"), token) ||
		subtleStringEqual(request.Header.Get("X-Tencent-IM-Callback-Token"), token) {
		// 保留既有直传 token 协议，避免已接入的内部探针在控制台切换鉴权方式时失效。
		return true
	}

	requestTimeRaw := strings.TrimSpace(request.URL.Query().Get("RequestTime"))
	requestTime, err := strconv.ParseInt(requestTimeRaw, 10, 64)
	if err != nil {
		return false
	}
	if deltaSeconds := time.Now().Unix() - requestTime; deltaSeconds < -60 || deltaSeconds > 60 {
		// 腾讯云签名只绑定秒级时间戳；限制一分钟窗口可阻止已截获回调被长期重放。
		return false
	}

	digest := sha256.Sum256([]byte(token + requestTimeRaw))
	expectedSign := fmt.Sprintf("%x", digest)
	return subtleStringEqual(request.URL.Query().Get("Sign"), expectedSign)
}

func (h *Handler) tencentIMSDKAppIDMatched(request *http.Request) bool {
	if h.tencentIM.SDKAppID <= 0 {
		// 没有 SDKAppID 时无法证明回调属于当前应用，必须拒绝。
		return false
	}

	raw := strings.TrimSpace(request.URL.Query().Get("SdkAppid"))
	value, err := strconv.ParseInt(raw, 10, 64)
	return err == nil && value == h.tencentIM.SDKAppID
}

func decodeTencentIMCallbackBody(reader io.Reader) (tencentIMCallbackBody, bool) {
	payload, err := io.ReadAll(reader)
	if err != nil {
		return tencentIMCallbackBody{}, false
	}
	if len(strings.TrimSpace(string(payload))) == 0 {
		return tencentIMCallbackBody{}, true
	}

	var body tencentIMCallbackBody
	if err := json.Unmarshal(payload, &body); err != nil {
		return tencentIMCallbackBody{}, false
	}

	return body, true
}

func resolveTencentIMCallbackCommand(queryCommand string, bodyCommand string) (string, bool) {
	queryCommand = strings.TrimSpace(queryCommand)
	bodyCommand = strings.TrimSpace(bodyCommand)
	if queryCommand != "" && bodyCommand != "" && queryCommand != bodyCommand {
		// query 和 body 不一致时拒绝，避免中间层或伪造请求混淆回调语义。
		return "", false
	}
	if queryCommand != "" {
		return queryCommand, true
	}
	if bodyCommand != "" {
		return bodyCommand, true
	}

	return "", false
}

func knownTencentIMCallback(command string) bool {
	return command == tencentCallbackJoinGroup || command == tencentCallbackSendMsg || command == tencentCallbackC2CSent
}

func parseTencentIMUserID(value string) (int64, bool) {
	userID, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
	return userID, err == nil && userID > 0
}

func tencentGuardErrorInfo(err error, reason string) string {
	if err != nil {
		return "room guard unavailable"
	}
	if strings.TrimSpace(reason) != "" {
		return reason
	}

	return "permission denied"
}

func subtleStringEqual(left string, right string) bool {
	left = strings.TrimSpace(left)
	right = strings.TrimSpace(right)
	if len(left) != len(right) {
		return false
	}

	var diff byte
	for index := range left {
		diff |= left[index] ^ right[index]
	}

	return diff == 0
}

func writeTencentCallback(writer http.ResponseWriter, statusCode int, errorCode int, errorInfo string) {
	writer.Header().Set("Content-Type", "application/json")
	writer.WriteHeader(statusCode)

	response := tencentIMCallbackResponse{
		ActionStatus: "OK",
		ErrorInfo:    errorInfo,
		ErrorCode:    errorCode,
	}
	if errorCode != 0 && errorInfo == "" {
		response.ErrorInfo = "rejected"
	}

	_ = json.NewEncoder(writer).Encode(response)
}
