package appapi

import (
	"bytes"
	"fmt"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
	"io"
	"mime/multipart"
	"net/http"
	"path"
	"strings"
	"time"
	"unicode"

	"hyapp/pkg/idgen"
	"hyapp/services/gateway-service/internal/auth"
)

const (
	// uploadMultipartOverheadLimit 给 multipart 边界、字段头和少量表单字段预留空间。
	uploadMultipartOverheadLimit = 1 << 20
	// avatarUploadMaxBytes 限制头像体积，避免注册入口被大文件拖垮。
	avatarUploadMaxBytes int64 = 5 << 20
	// fileUploadMaxBytes 是 App 通用文件入口的单文件上限。
	fileUploadMaxBytes int64 = 20 << 20
	// uploadSniffBytes 是 net/http 内容嗅探算法使用的最大字节数。
	uploadSniffBytes = 512
)

type uploadPolicy struct {
	Kind              string
	MaxBytes          int64
	RequireImage      bool
	AllowedExtensions map[string]struct{}
}

var (
	avatarUploadPolicy = uploadPolicy{
		Kind:         "avatars",
		MaxBytes:     avatarUploadMaxBytes,
		RequireImage: true,
		AllowedExtensions: map[string]struct{}{
			".jpg": {}, ".jpeg": {}, ".png": {}, ".webp": {},
		},
	}
	fileUploadPolicy = uploadPolicy{
		Kind:     "files",
		MaxBytes: fileUploadMaxBytes,
	}
)

type uploadResponse struct {
	URL         string `json:"url"`
	ObjectKey   string `json:"object_key"`
	ContentType string `json:"content_type"`
	SizeBytes   int64  `json:"size_bytes"`
}

// uploadAvatar 上传用户头像文件，只接受常见静态图片格式。
func (h *Handler) uploadAvatar(writer http.ResponseWriter, request *http.Request) {
	h.uploadObject(writer, request, avatarUploadPolicy, fmt.Sprintf("%d", auth.UserIDFromContext(request.Context())))
}

// uploadRegistrationAvatar 上传注册页临时头像，供尚未拿到后端 session 的三方注册流程使用。
func (h *Handler) uploadRegistrationAvatar(writer http.ResponseWriter, request *http.Request) {
	h.uploadObject(writer, request, avatarUploadPolicy, "registration")
}

// uploadFile 上传 App 通用文件，不绑定用户资料更新语义。
func (h *Handler) uploadFile(writer http.ResponseWriter, request *http.Request) {
	h.uploadObject(writer, request, fileUploadPolicy, fmt.Sprintf("%d", auth.UserIDFromContext(request.Context())))
}

func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request, policy uploadPolicy, ownerSegment string) {
	if request.Method != http.MethodPost {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if h.objectUploader == nil {
		// 上传入口必须 fail-closed，避免客户端拿到伪造 URL 后继续写用户资料。
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	file, header, ok := parseUploadFile(writer, request, policy.MaxBytes)
	if !ok {
		return
	}
	defer file.Close()
	defer func() {
		if request.MultipartForm != nil {
			_ = request.MultipartForm.RemoveAll()
		}
	}()

	if header.Size <= 0 || header.Size > policy.MaxBytes {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	head, contentType, detectedContentType, ok := sniffUploadContent(writer, request, file, header)
	if !ok {
		return
	}
	if isRasterUploadContentType(detectedContentType) {
		// 通用文件入口也可能承接房间背景和资料图片。只要文件头能确定为受支持的
		// 栅格图片，就使用真实字节类型覆盖客户端 MIME，后续对象后缀也据此规范化。
		contentType = detectedContentType
	}
	if policy.RequireImage {
		// 头像用文件头嗅探结果做安全判断，不信任客户端传入的 multipart Content-Type。
		contentType = detectedContentType
		if animatedWebPHeader(head) {
			// 注册和旧头像入口只允许静态图片；动态 WebP 的 MIME 与静态相同，必须读取 VP8X 动画位拦截。
			httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
			return
		}
	}
	if !validateUploadContent(policy, header.Filename, contentType) {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	objectKey := buildUploadObjectKey(policy, ownerSegment, header.Filename, contentType, time.Now().UTC())
	objectURL, err := h.objectUploader.PutObject(request.Context(), objectKey, io.MultiReader(bytes.NewReader(head), file), header.Size, contentType)
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	httpkit.WriteOK(writer, request, uploadResponse{
		URL:         objectURL,
		ObjectKey:   objectKey,
		ContentType: contentType,
		SizeBytes:   header.Size,
	})
}

func parseUploadFile(writer http.ResponseWriter, request *http.Request, maxBytes int64) (multipart.File, *multipart.FileHeader, bool) {
	request.Body = http.MaxBytesReader(writer, request.Body, maxBytes+uploadMultipartOverheadLimit)
	if err := request.ParseMultipartForm(1 << 20); err != nil {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return nil, nil, false
	}

	for _, field := range []string{"file", "avatar"} {
		file, header, err := request.FormFile(field)
		if err == nil {
			return file, header, true
		}
	}

	httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
	return nil, nil, false
}

func sniffUploadContent(writer http.ResponseWriter, request *http.Request, file multipart.File, header *multipart.FileHeader) ([]byte, string, string, bool) {
	head := make([]byte, uploadSniffBytes)
	n, err := io.ReadFull(file, head)
	if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return nil, "", "", false
	}
	head = head[:n]

	detectedContentType := http.DetectContentType(head)
	contentType := strings.TrimSpace(header.Header.Get("Content-Type"))
	if contentType == "" || contentType == "application/octet-stream" {
		contentType = detectedContentType
	}

	return head, contentType, detectedContentType, true
}

func validateUploadContent(policy uploadPolicy, filename string, contentType string) bool {
	contentType = strings.ToLower(strings.TrimSpace(contentType))
	if policy.RequireImage && !isAvatarContentType(contentType) {
		return false
	}
	if len(policy.AllowedExtensions) == 0 {
		return true
	}

	extension := normalizedUploadExtension(filename, contentType)
	_, ok := policy.AllowedExtensions[extension]
	return ok
}

func isAvatarContentType(contentType string) bool {
	switch strings.ToLower(strings.TrimSpace(contentType)) {
	case "image/jpeg", "image/png", "image/webp":
		return true
	default:
		return false
	}
}

func isRasterUploadContentType(contentType string) bool {
	switch strings.ToLower(strings.TrimSpace(contentType)) {
	case "image/jpeg", "image/png", "image/webp", "image/gif":
		return true
	default:
		return false
	}
}

func animatedWebPHeader(head []byte) bool {
	// 有动画的合法 WebP 必须以 VP8X 扩展头声明 ANIMATION flag；该字段位于前 21 字节内。
	return len(head) >= 21 && string(head[:4]) == "RIFF" && string(head[8:12]) == "WEBP" &&
		string(head[12:16]) == "VP8X" && head[20]&0x02 != 0
}

func buildUploadObjectKey(policy uploadPolicy, ownerSegment string, filename string, contentType string, now time.Time) string {
	date := now.UTC().Format("20060102")
	extension := normalizedUploadExtension(filename, contentType)
	if extension == "" && policy.RequireImage {
		extension = ".jpg"
	}

	idPrefix := strings.TrimSuffix(policy.Kind, "s")
	ownerSegment = strings.Trim(ownerSegment, "/")
	if ownerSegment == "" {
		// 调用方必须明确文件归属分区；空分区会让不同入口对象混在一起，直接回落到 unknown 便于排查。
		ownerSegment = "unknown"
	}
	return fmt.Sprintf("app/%s/%s/%s/%s%s", policy.Kind, ownerSegment, date, idgen.New(idPrefix), extension)
}

func normalizedUploadExtension(filename string, contentType string) string {
	// 受支持图片的规范后缀只能来自嗅探后的真实 Content-Type，不能让相册缓存文件名
	// 把 WebP/GIF 对象错误保存成 .jpg；非图片文件仍保留原始安全后缀。
	switch strings.ToLower(strings.TrimSpace(contentType)) {
	case "image/jpeg":
		return ".jpg"
	case "image/png":
		return ".png"
	case "image/webp":
		return ".webp"
	case "image/gif":
		return ".gif"
	}
	if extension := cleanUploadExtension(path.Ext(filename)); extension != "" {
		return extension
	}

	switch strings.ToLower(strings.TrimSpace(contentType)) {
	case "application/pdf":
		return ".pdf"
	default:
		return ""
	}
}

func cleanUploadExtension(extension string) string {
	extension = strings.ToLower(strings.TrimSpace(extension))
	if len(extension) < 2 || len(extension) > 10 || extension[0] != '.' {
		return ""
	}
	for _, char := range extension[1:] {
		if !unicode.IsDigit(char) && (char < 'a' || char > 'z') {
			return ""
		}
	}

	return extension
}
