package callbackapi

import (
	"encoding/json"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
	"io"
	"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"
)

const (
	tencentCallbackJoinGroup = "Group.CallbackBeforeApplyJoinGroup"
	tencentCallbackSendMsg   = "Group.CallbackBeforeSendMsg"
)

// 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"`
}

// 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:
		h.handleTencentIMSendCallback(writer, request, body)
	default:
		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)
	userID, ok := parseTencentIMUserID(body.Requestor)
	if !ok {
		writeTencentCallback(writer, http.StatusOK, 1, "join guard input invalid")
		return
	}

	switch parsed.Kind {
	case imgroup.KindGlobalBroadcast, imgroup.KindRegionBroadcast:
		// 播报群不属于 Room Cell，准入只看 user-service 的用户状态、资料完成度、app_code 和区域归属。
		h.handleTencentIMBroadcastJoinCallback(writer, request, parsed, userID)
		return
	case imgroup.KindRoom:
		// 房间群仍走 room-service guard，保证只有当前房间 presence 用户能加入/留在房间 IM 群。
		h.handleTencentIMRoomJoinCallback(writer, request, parsed.RoomID, userID)
		return
	default:
		writeTencentCallback(writer, http.StatusOK, 1, "group_id is invalid")
		return
	}
}

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

	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, "")
}

func (h *Handler) handleTencentIMSendCallback(writer http.ResponseWriter, request *http.Request, body tencentIMCallbackBody) {
	parsed := imgroup.ParseWithPrefix(body.GroupID, h.tencentIM.GroupIDPrefix)
	switch parsed.Kind {
	case imgroup.KindGlobalBroadcast, imgroup.KindRegionBroadcast:
		if h.tencentIMCallbackSenderIsServer(body) {
			// 播报群是服务端只写通道；管理员账号发送的 REST 消息由腾讯回调放行。
			writeTencentCallback(writer, http.StatusOK, 0, "")
			return
		}
		writeTencentCallback(writer, http.StatusOK, 1, "broadcast group is server-only")
		return
	case imgroup.KindRoom:
		userID, ok := parseTencentIMUserID(body.FromAccount)
		if !ok {
			writeTencentCallback(writer, http.StatusOK, 1, "send guard input invalid")
			return
		}
		h.handleTencentIMRoomSendCallback(writer, request, parsed.RoomID, userID)
		return
	default:
		writeTencentCallback(writer, http.StatusOK, 1, "group_id is invalid")
		return
	}
}

func (h *Handler) handleTencentIMRoomSendCallback(writer http.ResponseWriter, request *http.Request, roomID string, userID int64) {
	if roomID == "" || h.roomGuardClient == nil {
		writeTencentCallback(writer, http.StatusOK, 1, "send guard input invalid")
		return
	}

	resp, err := h.roomGuardClient.CheckSpeakPermission(request.Context(), &roomv1.CheckSpeakPermissionRequest{
		RoomId:  roomID,
		UserId:  userID,
		AppCode: appcode.FromContext(request.Context()),
	})
	if err != nil || !resp.GetAllowed() {
		writeTencentCallback(writer, http.StatusOK, 1, tencentGuardErrorInfo(err, resp.GetReason()))
		return
	}

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

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
	}

	return subtleStringEqual(request.URL.Query().Get("CallbackAuthToken"), token) ||
		subtleStringEqual(request.Header.Get("X-Tencent-IM-Callback-Token"), token)
}

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
}

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)
}
