// Package feedback 用户反馈：文字 + 图片（≤3 张，magic bytes 校验）+ 诊断信息。
// Storage 抽象对象存储：生产为阿里云 OSS，开发期 LocalStorage 落本地盘。
// 设计见 doc/backend-architecture.html 3.5。
package feedback

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"gorm.io/datatypes"
	"gorm.io/gorm"

	"dudu/server/internal/auth"
	"dudu/server/internal/store"
	"dudu/server/pkg/protocol"
)

type Storage interface {
	Save(ctx context.Context, key string, data []byte, contentType string) error
}

// LocalStorage 开发期实现：写入本地目录（生产替换为 OSS 实现）。
type LocalStorage struct{ Dir string }

func (s LocalStorage) Save(_ context.Context, key string, data []byte, _ string) error {
	path := filepath.Join(s.Dir, key)
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	return os.WriteFile(path, data, 0o644)
}

// sniffImage magic bytes 验真实格式（伪造扩展名拒绝）。
func sniffImage(data []byte) (ext, contentType string, ok bool) {
	switch {
	case bytes.HasPrefix(data, []byte{0xFF, 0xD8, 0xFF}):
		return "jpg", "image/jpeg", true
	case bytes.HasPrefix(data, []byte{0x89, 'P', 'N', 'G'}):
		return "png", "image/png", true
	case len(data) > 12 && bytes.Equal(data[0:4], []byte("RIFF")) && bytes.Equal(data[8:12], []byte("WEBP")):
		return "webp", "image/webp", true
	}
	return "", "", false
}

type Handlers struct {
	DB      *gorm.DB
	RDB     *redis.Client
	Storage Storage
}

// Submit POST /v1/feedback（multipart，需登录）。
func (h *Handlers) Submit(c *gin.Context) {
	uid := auth.UserID(c)

	// 限频：每用户 10 条/天
	day := store.Day(time.Now())
	n, err := store.IncrDailyCounter(c, h.RDB, store.KeyRateFb(uid, day))
	if err != nil {
		c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
		return
	}
	if n > protocol.FeedbackDailyLimit {
		c.JSON(http.StatusTooManyRequests, protocol.NewAPIError(protocol.ErrFeedbackRateLimited))
		return
	}

	content := strings.TrimSpace(c.PostForm("content"))
	if l := len([]rune(content)); l == 0 || l > protocol.FeedbackMaxContentLen {
		c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
		return
	}

	form, err := c.MultipartForm()
	if err != nil {
		c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
		return
	}
	files := form.File["images[]"]
	if len(files) == 0 {
		files = form.File["images"]
	}
	if len(files) > protocol.FeedbackMaxImages {
		c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
		return
	}

	id := "fb" + strings.ReplaceAll(uuid.NewString(), "-", "")[:22]
	var keys []string
	for i, fh := range files {
		if fh.Size > protocol.FeedbackMaxImageBytes {
			c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
			return
		}
		f, err := fh.Open()
		if err != nil {
			c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
			return
		}
		data, err := io.ReadAll(io.LimitReader(f, protocol.FeedbackMaxImageBytes+1))
		f.Close()
		if err != nil || len(data) > protocol.FeedbackMaxImageBytes {
			c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
			return
		}
		ext, ct, ok := sniffImage(data)
		if !ok {
			c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
			return
		}
		key := fmt.Sprintf("fb/%s/%d.%s", id, i+1, ext)
		if err := h.Storage.Save(c, key, data, ct); err != nil {
			c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
			return
		}
		keys = append(keys, key)
	}

	imagesJSON, _ := json.Marshal(keys)
	var diagJSON datatypes.JSON
	if d := c.PostForm("diagnostics"); d != "" && len(d) < 64<<10 && json.Valid([]byte(d)) {
		diagJSON = datatypes.JSON(d)
	}
	fb := store.Feedback{
		ID: id, UserID: uid, Content: content,
		Images: datatypes.JSON(imagesJSON), Diagnostics: diagJSON,
		Platform: c.GetHeader("X-Platform"), AppVersion: c.GetHeader("X-App-Version"),
		Status: store.FeedbackNew,
	}
	if err := h.DB.Create(&fb).Error; err != nil {
		c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
		return
	}
	c.JSON(http.StatusOK, protocol.FeedbackResponse{FeedbackID: id})
}
