package callbackapi

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"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"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/tencentrtc"
	"hyapp/pkg/xerr"
)

const (
	tencentRTCEventGroupRoom  = 1
	tencentRTCEventGroupMedia = 2
	tencentRTCEventRoomExit   = 104
	tencentRTCEventStartAudio = 203
	tencentRTCEventStopAudio  = 204
)

// tencentRTCCallbackBody 覆盖当前语音房需要消费的腾讯 RTC 房间/媒体回调字段。
type tencentRTCCallbackBody struct {
	EventGroupID int                     `json:"EventGroupId"`
	EventType    int                     `json:"EventType"`
	CallbackTs   int64                   `json:"CallbackTs"`
	EventInfo    tencentRTCCallbackEvent `json:"EventInfo"`
}

type tencentRTCCallbackEvent struct {
	RoomID    json.RawMessage `json:"RoomId"`
	UserID    string          `json:"UserId"`
	EventTs   int64           `json:"EventTs"`
	EventMsTs int64           `json:"EventMsTs"`
	Reason    int             `json:"Reason"`
}

type tencentRTCCallbackResponse struct {
	Code    int    `json:"code"`
	Message string `json:"message,omitempty"`
}

// handleTencentRTCCallback 处理腾讯 RTC 服务端事件回调。
func (h *Handler) handleTencentRTCCallback(writer http.ResponseWriter, request *http.Request) {
	if request.Method != http.MethodPost {
		writeTencentRTCCallback(writer, http.StatusMethodNotAllowed, 1, "method not allowed")
		return
	}

	payload, err := io.ReadAll(io.LimitReader(request.Body, 1<<20))
	if err != nil || len(bytes.TrimSpace(payload)) == 0 {
		writeTencentRTCCallback(writer, http.StatusBadRequest, 1, "invalid callback body")
		return
	}
	if !h.tencentRTCSDKAppIDMatched(request) {
		writeTencentRTCCallback(writer, http.StatusForbidden, 1, "sdk_app_id mismatch")
		return
	}
	if !tencentrtc.VerifyCallbackSignature(h.tencentRTC.CallbackSignKey, payload, request.Header.Get(tencentrtc.CallbackHeaderSign)) {
		// 腾讯 RTC 回调签名使用原始 body；配置缺失或签名缺失都必须 fail-closed。
		writeTencentRTCCallback(writer, http.StatusForbidden, 1, "callback signature invalid")
		return
	}

	var body tencentRTCCallbackBody
	if err := json.Unmarshal(payload, &body); err != nil {
		writeTencentRTCCallback(writer, http.StatusBadRequest, 1, "invalid callback body")
		return
	}

	eventType, known := normalizeTencentRTCEventType(body.EventGroupID, body.EventType)
	if !known {
		// 白名单外事件只做验签和 SDKAppID 校验，避免腾讯新增事件造成重试风暴。
		writeTencentRTCCallback(writer, http.StatusOK, 0, "")
		return
	}

	roomID, ok := parseTencentRTCRoomID(body.EventInfo.RoomID)
	userID, userOK := parseTencentIMUserID(body.EventInfo.UserID)
	eventTimeMS := tencentRTCEventTimeMS(body)
	logCtx := logx.With(request.Context(), slog.String("request_id", httpkit.RequestIDFromContext(request.Context())))
	if !ok || !userOK || eventTimeMS <= 0 || h.roomClient == nil || h.roomGuardClient == nil {
		logx.Warn(logCtx, "gateway_rtc_callback_ignored",
			slog.String("provider", "tencent"),
			slog.Int("event_group", body.EventGroupID),
			slog.Int("event_type", body.EventType),
			slog.String("room_id", roomID),
			slog.String("external_user_id", body.EventInfo.UserID),
			slog.String("reason", "input_invalid"),
		)
		writeTencentRTCCallback(writer, http.StatusOK, 0, "")
		return
	}

	// 腾讯回调只携带 SDKAppID 和 room_id；线上当前多个 App 共用腾讯应用，URL app_code 无法代表真实房间租户。
	// 房间归属必须回到 room-service 的持久事实解析，避免 Fami 回调被固定 lalu 参数写入错误 Room Cell。
	appResp, err := h.roomGuardClient.ResolveRoomAppCode(request.Context(), &roomv1.ResolveRoomAppCodeRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		RoomId:    roomID,
	})
	if err != nil {
		if tencentRTCCallbackCanAckBusinessError(err) {
			logx.Warn(logCtx, "gateway_rtc_callback_room_app_ignored", slog.String("room_id", roomID), slog.String("reason", string(xerr.ReasonFromGRPC(err))))
			writeTencentRTCCallback(writer, http.StatusOK, 0, "")
			return
		}
		logx.Error(logCtx, "gateway_rtc_callback_room_app_error", err, slog.String("room_id", roomID))
		writeTencentRTCCallback(writer, http.StatusInternalServerError, 1, "room service error")
		return
	}
	resolvedAppCode := appcode.Normalize(appResp.GetAppCode())
	if strings.TrimSpace(appResp.GetAppCode()) == "" {
		logx.Warn(logCtx, "gateway_rtc_callback_room_app_ignored", slog.String("room_id", roomID), slog.String("reason", "app_code_empty"))
		writeTencentRTCCallback(writer, http.StatusOK, 0, "")
		return
	}
	logCtx = logx.With(logCtx, slog.String("app_code", resolvedAppCode))

	resp, err := h.roomClient.ApplyRTCEvent(request.Context(), &roomv1.ApplyRTCEventRequest{
		Meta: &roomv1.RequestMeta{
			RequestId:     httpkit.RequestIDFromContext(request.Context()),
			CommandId:     tencentRTCCallbackCommandID(body, roomID, userID, eventTimeMS),
			ActorUserId:   userID,
			RoomId:        roomID,
			AppCode:       resolvedAppCode,
			GatewayNodeId: "gateway-local",
			SessionId:     fmt.Sprintf("tencent-rtc-%d", body.EventType),
			SentAtMs:      time.Now().UnixMilli(),
		},
		TargetUserId:    userID,
		EventType:       eventType,
		EventTimeMs:     eventTimeMS,
		Reason:          tencentRTCCallbackReason(body),
		Source:          "tencent_rtc_callback",
		ExternalEventId: tencentRTCCallbackExternalEventID(body, roomID, userID, eventTimeMS),
	})
	if err != nil {
		if tencentRTCCallbackCanAckBusinessError(err) {
			logx.Warn(logCtx, "gateway_rtc_callback_business_ignored",
				slog.String("provider", "tencent"),
				slog.String("room_id", roomID),
				slog.Int64("user_id", userID),
				slog.Int("event_group", body.EventGroupID),
				slog.Int("event_type", body.EventType),
				slog.String("reason", string(xerr.ReasonFromGRPC(err))),
			)
			writeTencentRTCCallback(writer, http.StatusOK, 0, "")
			return
		}
		logx.Error(logCtx, "gateway_rtc_callback_room_service_error", err,
			slog.String("provider", "tencent"),
			slog.String("room_id", roomID),
			slog.Int64("user_id", userID),
			slog.Int("event_group", body.EventGroupID),
			slog.Int("event_type", body.EventType),
		)
		writeTencentRTCCallback(writer, http.StatusInternalServerError, 1, "room service error")
		return
	}

	logx.Info(logCtx, "gateway_rtc_callback_accepted",
		slog.String("provider", "tencent"),
		slog.String("room_id", roomID),
		slog.Int64("user_id", userID),
		slog.Int("event_group", body.EventGroupID),
		slog.Int("event_type", body.EventType),
		slog.Int64("room_version", resp.GetResult().GetRoomVersion()),
		slog.Bool("applied", resp.GetResult().GetApplied()),
	)
	writeTencentRTCCallback(writer, http.StatusOK, 0, "")
}

func (h *Handler) tencentRTCSDKAppIDMatched(request *http.Request) bool {
	if h.tencentRTC.SDKAppID <= 0 {
		return false
	}

	raw := strings.TrimSpace(request.Header.Get(tencentrtc.CallbackHeaderSDKAppID))
	value, err := strconv.ParseInt(raw, 10, 64)
	return err == nil && value == h.tencentRTC.SDKAppID
}

func normalizeTencentRTCEventType(groupID int, eventType int) (string, bool) {
	switch {
	case groupID == tencentRTCEventGroupMedia && eventType == tencentRTCEventStartAudio:
		return "audio_started", true
	case groupID == tencentRTCEventGroupMedia && eventType == tencentRTCEventStopAudio:
		return "audio_stopped", true
	case groupID == tencentRTCEventGroupRoom && eventType == tencentRTCEventRoomExit:
		return "room_exited", true
	default:
		return "", false
	}
}

func parseTencentRTCRoomID(raw json.RawMessage) (string, bool) {
	raw = bytes.TrimSpace(raw)
	if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
		return "", false
	}

	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return "", false
	}

	switch typed := value.(type) {
	case string:
		roomID := strings.TrimSpace(typed)
		return roomID, roomID != ""
	case json.Number:
		roomID := strings.TrimSpace(typed.String())
		return roomID, roomID != ""
	default:
		return "", false
	}
}

func tencentRTCEventTimeMS(body tencentRTCCallbackBody) int64 {
	if body.EventInfo.EventMsTs > 0 {
		return body.EventInfo.EventMsTs
	}
	if body.EventInfo.EventTs > 0 {
		return body.EventInfo.EventTs * 1000
	}

	return body.CallbackTs
}

func tencentRTCCallbackReason(body tencentRTCCallbackBody) string {
	switch body.EventType {
	case tencentRTCEventRoomExit:
		return fmt.Sprintf("rtc_room_exited:%d", body.EventInfo.Reason)
	case tencentRTCEventStopAudio:
		return fmt.Sprintf("rtc_audio_stopped:%d", body.EventInfo.Reason)
	case tencentRTCEventStartAudio:
		return "rtc_audio_started"
	default:
		return fmt.Sprintf("rtc_event:%d:%d", body.EventGroupID, body.EventType)
	}
}

func tencentRTCCallbackCommandID(body tencentRTCCallbackBody, roomID string, userID int64, eventTimeMS int64) string {
	return "cmd_" + tencentRTCCallbackDigest(body, roomID, userID, eventTimeMS)
}

func tencentRTCCallbackExternalEventID(body tencentRTCCallbackBody, roomID string, userID int64, eventTimeMS int64) string {
	return "rtc_evt_" + tencentRTCCallbackDigest(body, roomID, userID, eventTimeMS)
}

func tencentRTCCallbackDigest(body tencentRTCCallbackBody, roomID string, userID int64, eventTimeMS int64) string {
	seed := fmt.Sprintf("%d:%d:%s:%d:%d:%d", body.EventGroupID, body.EventType, roomID, userID, eventTimeMS, body.EventInfo.Reason)
	sum := sha256.Sum256([]byte(seed))
	return hex.EncodeToString(sum[:16])
}

func tencentRTCCallbackCanAckBusinessError(err error) bool {
	reason := xerr.ReasonFromGRPC(err)
	return reason == xerr.NotFound || reason == xerr.InvalidArgument || reason == xerr.RoomClosed || reason == xerr.PermissionDenied
}

func writeTencentRTCCallback(writer http.ResponseWriter, statusCode int, code int, message string) {
	writer.Header().Set("Content-Type", "application/json")
	writer.WriteHeader(statusCode)
	_ = json.NewEncoder(writer).Encode(tencentRTCCallbackResponse{
		Code:    code,
		Message: message,
	})
}
