package feedback

import (
	"archive/zip"
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"ymhut-box/server/feedback-mailer/internal/config"
	"ymhut-box/server/feedback-mailer/internal/db"
	feedbackmail "ymhut-box/server/feedback-mailer/internal/mail"
	"ymhut-box/server/feedback-mailer/internal/webhook"
)

const (
	ErrorMethodNotAllowed        = "METHOD_NOT_ALLOWED"
	ErrorTooLarge                = "TOO_LARGE"
	ErrorMissingField            = "MISSING_FIELD"
	ErrorInvalidPayload          = "INVALID_PAYLOAD"
	ErrorInvalidTimestamp        = "INVALID_TIMESTAMP"
	ErrorInvalidSignature        = "INVALID_SIGNATURE"
	ErrorInvalidPackage          = "INVALID_PACKAGE"
	ErrorInvalidEncryptedPackage = "INVALID_ENCRYPTED_PACKAGE"
	ErrorDecryptFailed           = "DECRYPT_FAILED"
	ErrorHashMismatch            = "HASH_MISMATCH"
	ErrorMailFailed              = "MAIL_FAILED"
	ErrorServerConfig            = "SERVER_CONFIG"
	ErrorNotFound                = "NOT_FOUND"

	PackageMagic = "YMHUTFB1"
)

var feedbackCodePattern = regexp.MustCompile(`^FB-[0-9]{8}-[A-F0-9]{6}$`)

type Service struct {
	cfg   *config.Config
	store *db.Store
	hooks *webhook.Dispatcher
}

type StatusPayload struct {
	OK           bool   `json:"ok"`
	Code         string `json:"code"`
	Status       string `json:"status"`
	StatusLabel  string `json:"statusLabel"`
	StatusDetail string `json:"statusDetail"`
	Category     string `json:"category"`
	Priority     string `json:"priority"`
	HasReply     bool   `json:"hasReply"`
	Reply        string `json:"reply"`
	ReceivedAt   string `json:"receivedAt"`
	UpdatedAt    string `json:"updatedAt"`
	MailSent     bool   `json:"mailSent"`
	Duplicate    bool   `json:"duplicate,omitempty"`
}

type submissionPayload struct {
	FeedbackCode       string          `json:"feedbackCode"`
	Title              string          `json:"title"`
	Type               string          `json:"type"`
	Severity           string          `json:"severity"`
	Contact            string          `json:"contact"`
	BodyLength         int             `json:"bodyLength"`
	PackageEncrypted   bool            `json:"packageEncrypted"`
	Encryption         string          `json:"encryption"`
	PackageBytes       int64           `json:"packageBytes"`
	PackageSha256      string          `json:"packageSha256"`
	PlainPackageBytes  int64           `json:"plainPackageBytes"`
	PlainPackageSha256 string          `json:"plainPackageSha256"`
	CreatedAt          json.RawMessage `json:"createdAt"`
}

type packageInfo struct {
	Request map[string]any
	Summary string
	Files   []string
}

func NewService(cfg *config.Config, store *db.Store, hooks *webhook.Dispatcher) *Service {
	return &Service{cfg: cfg, store: store, hooks: hooks}
}

func (s *Service) HandleSubmission(c *gin.Context) {
	if c.Request.Method != http.MethodPost {
		Fail(c, ErrorMethodNotAllowed, http.StatusMethodNotAllowed, "POST required")
		return
	}

	if s.cfg.MaxRequestBytes > 0 {
		if c.Request.ContentLength > s.cfg.MaxRequestBytes {
			Fail(c, ErrorTooLarge, http.StatusRequestEntityTooLarge, "Request is too large")
			return
		}
		c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, s.cfg.MaxRequestBytes)
	}

	payloadText, ok := requireForm(c, "payload")
	if !ok {
		return
	}
	timestamp, ok := requireForm(c, "timestamp")
	if !ok {
		return
	}
	nonce, ok := requireForm(c, "nonce")
	if !ok {
		return
	}
	packageSha256, ok := requireForm(c, "packageSha256")
	if !ok {
		return
	}
	signature, ok := requireForm(c, "signature")
	if !ok {
		return
	}

	var payload submissionPayload
	if err := json.Unmarshal([]byte(payloadText), &payload); err != nil {
		Fail(c, ErrorInvalidPayload, http.StatusBadRequest, "Invalid payload JSON")
		return
	}
	if err := validatePayload(payloadText, payload); err != nil {
		Fail(c, errCode(err), http.StatusBadRequest, err.Error())
		return
	}
	if !validTimestamp(timestamp, s.cfg.TimestampWindowSeconds) {
		Fail(c, ErrorInvalidTimestamp, http.StatusBadRequest, "Timestamp outside accepted window")
		return
	}

	packageSha256 = strings.ToLower(strings.TrimSpace(packageSha256))
	signature = strings.ToLower(strings.TrimSpace(signature))
	if !isHexSHA256(packageSha256) {
		Fail(c, ErrorHashMismatch, http.StatusBadRequest, "Invalid package hash")
		return
	}
	if s.cfg.ClientSignatureKey == "" {
		Fail(c, ErrorServerConfig, http.StatusInternalServerError, "Missing client signature key")
		return
	}
	expected := SignWithKey(s.cfg.ClientSignatureKey, timestamp, nonce, packageSha256, payloadText)
	if !hmac.Equal([]byte(expected), []byte(signature)) {
		Fail(c, ErrorInvalidSignature, http.StatusUnauthorized, "Invalid request signature")
		return
	}

	code := NormalizeCode(payload.FeedbackCode)
	if code == "" {
		code = s.generateCode()
	}

	existing, err := s.store.FetchStatus(code)
	if err != nil {
		Fail(c, ErrorServerConfig, http.StatusInternalServerError, "Server error")
		return
	}
	if existing != nil {
		response := StatusFromRow(*existing)
		response.Duplicate = true
		c.JSON(http.StatusOK, response)
		return
	}

	file, err := c.FormFile("package")
	if err != nil {
		Fail(c, ErrorMissingField, http.StatusBadRequest, "Missing package file")
		return
	}
	data, err := readUploadedPackage(file, s.cfg.MaxPackageBytes)
	if err != nil {
		if errors.Is(err, errUploadTooLarge) {
			Fail(c, ErrorTooLarge, http.StatusRequestEntityTooLarge, "Feedback package is too large")
		} else {
			Fail(c, ErrorInvalidPackage, http.StatusBadRequest, "Package upload failed")
		}
		return
	}
	if !bytes.HasPrefix(data, []byte(PackageMagic)) {
		Fail(c, ErrorInvalidEncryptedPackage, http.StatusBadRequest, "Encrypted package format is invalid")
		return
	}
	actual := sha256Hex(data)
	if !hmac.Equal([]byte(actual), []byte(packageSha256)) {
		Fail(c, ErrorHashMismatch, http.StatusBadRequest, "Package hash mismatch")
		return
	}

	encryptedPath := filepath.Join(s.cfg.StorageDir, code+".ymfb")
	packagePath := filepath.Join(s.cfg.StorageDir, code+".zip")
	if err := os.WriteFile(encryptedPath, data, 0o640); err != nil {
		Fail(c, ErrorInvalidPackage, http.StatusInternalServerError, "Unable to save package")
		return
	}

	plain, err := DecryptPackage(data, s.cfg.PackageEncryptionKey)
	if err != nil {
		Fail(c, ErrorDecryptFailed, http.StatusBadRequest, "Unable to decrypt package")
		return
	}
	if !isZipBytes(plain) {
		Fail(c, ErrorInvalidPackage, http.StatusBadRequest, "Decrypted package is not a zip file")
		return
	}
	if payload.PlainPackageSha256 != "" && isHexSHA256(payload.PlainPackageSha256) {
		if !hmac.Equal([]byte(sha256Hex(plain)), []byte(strings.ToLower(payload.PlainPackageSha256))) {
			Fail(c, ErrorHashMismatch, http.StatusBadRequest, "Decrypted package hash mismatch")
			return
		}
	}

	info, err := ReadFeedbackPackageWithGuard(plain, s.cfg.UploadGuard)
	if err != nil {
		Fail(c, ErrorInvalidPackage, http.StatusBadRequest, "Unable to read feedback package")
		return
	}
	if err := os.WriteFile(packagePath, plain, 0o640); err != nil {
		Fail(c, ErrorServerConfig, http.StatusInternalServerError, "Unable to write decrypted package")
		return
	}

	record := buildRecord(code, payload, info, encryptedPath, packagePath, packageSha256, strings.ToLower(payload.PlainPackageSha256), c.ClientIP())
	if err := s.store.InsertFeedback(record); err != nil {
		Fail(c, ErrorServerConfig, http.StatusInternalServerError, "Unable to retain feedback")
		return
	}
	s.hooks.Dispatch("feedback.created", record)

	message, err := feedbackmail.BuildFeedbackMessage(s.cfg, record, packagePath)
	if err != nil {
		s.hooks.Dispatch("mail.failed", gin.H{"feedbackCode": record.Code, "error": err.Error()})
		Fail(c, ErrorMailFailed, http.StatusBadGateway, "Mail delivery failed; feedback record was retained")
		return
	}
	mailID, err := s.store.InsertMail(db.MailRecord{
		FeedbackCode:   record.Code,
		Kind:           "feedback",
		Status:         "pending",
		ToAddress:      message.To,
		Subject:        message.Subject,
		PlainBody:      message.PlainBody,
		HTMLBody:       message.HTMLBody,
		AttachmentPath: message.AttachmentPath,
		AttachmentName: message.AttachmentName,
		CreatedAt:      db.Now(),
	})
	if err != nil {
		Fail(c, ErrorServerConfig, http.StatusInternalServerError, "Unable to retain mail record")
		return
	}
	if err := feedbackmail.Send(s.cfg, message); err != nil {
		_ = s.store.UpdateMailState(mailID, "failed", shortenPlain(err.Error(), 1000))
		_ = s.store.UpdateFeedbackMailState(code, false)
		s.hooks.Dispatch("mail.failed", gin.H{"feedbackCode": record.Code, "mailId": mailID, "error": err.Error()})
		Fail(c, ErrorMailFailed, http.StatusBadGateway, "Mail delivery failed; feedback record was retained")
		return
	}
	_ = s.store.UpdateMailState(mailID, "sent", "")
	_ = s.store.UpdateFeedbackMailState(code, true)
	c.JSON(http.StatusOK, gin.H{"ok": true, "code": code})
}

func (s *Service) HandleStatus(c *gin.Context) {
	code := NormalizeCode(c.Query("code"))
	if code == "" {
		Fail(c, ErrorInvalidPayload, http.StatusBadRequest, "Invalid feedback code")
		return
	}
	row, err := s.store.FetchStatus(code)
	if err != nil {
		Fail(c, ErrorServerConfig, http.StatusInternalServerError, "Server error")
		return
	}
	if row == nil {
		Fail(c, ErrorNotFound, http.StatusNotFound, "Feedback not found")
		return
	}
	c.JSON(http.StatusOK, StatusFromRow(*row))
}

func StatusFromRow(row db.StatusRow) StatusPayload {
	reply := strings.TrimSpace(row.PublicReply)
	return StatusPayload{
		OK:           true,
		Code:         row.Code,
		Status:       row.Status,
		StatusLabel:  StatusLabel(row.Status),
		StatusDetail: row.StatusDetail,
		Category:     row.Category,
		Priority:     row.Priority,
		HasReply:     reply != "",
		Reply:        reply,
		ReceivedAt:   row.ReceivedAt,
		UpdatedAt:    row.UpdatedAt,
		MailSent:     row.MailSent,
	}
}

func NormalizeCode(code string) string {
	code = strings.ToUpper(strings.TrimSpace(code))
	if feedbackCodePattern.MatchString(code) {
		return code
	}
	return ""
}

func SignWithKey(key, timestamp, nonce, packageSha256, payload string) string {
	material := timestamp + "\n" + nonce + "\n" + packageSha256 + "\n" + payload
	mac := hmac.New(sha256.New, []byte(key))
	_, _ = mac.Write([]byte(material))
	return hex.EncodeToString(mac.Sum(nil))
}

func validTimestamp(value string, windowSeconds int64) bool {
	if !regexp.MustCompile(`^[0-9]{10,}$`).MatchString(value) {
		return false
	}
	seconds, err := time.ParseDuration(value + "s")
	if err != nil {
		return false
	}
	delta := time.Now().Unix() - int64(seconds.Seconds())
	if delta < 0 {
		delta = -delta
	}
	return delta <= windowSeconds
}

func validatePayload(payloadText string, payload submissionPayload) error {
	var raw map[string]json.RawMessage
	if err := json.Unmarshal([]byte(payloadText), &raw); err != nil {
		return codeError{code: ErrorInvalidPayload, message: "Invalid payload JSON"}
	}
	for _, field := range []string{"title", "type", "severity", "bodyLength", "packageBytes", "packageSha256", "plainPackageSha256", "createdAt"} {
		if _, ok := raw[field]; !ok {
			return fieldError("Payload missing field: " + field)
		}
	}
	if payload.Title == "" {
		return fieldError("Payload missing field: title")
	}
	if payload.Type == "" {
		return fieldError("Payload missing field: type")
	}
	if payload.Severity == "" {
		return fieldError("Payload missing field: severity")
	}
	if payload.BodyLength < 0 {
		return fieldError("Payload missing field: bodyLength")
	}
	if payload.PackageBytes <= 0 {
		return fieldError("Payload missing field: packageBytes")
	}
	if payload.PackageSha256 == "" {
		return fieldError("Payload missing field: packageSha256")
	}
	if payload.PlainPackageSha256 == "" {
		return fieldError("Payload missing field: plainPackageSha256")
	}
	if len(payload.CreatedAt) == 0 {
		return fieldError("Payload missing field: createdAt")
	}
	if !payload.PackageEncrypted || payload.Encryption != PackageMagic {
		return codeError{code: ErrorInvalidEncryptedPackage, message: "Encrypted package is required"}
	}
	return nil
}

type fieldError string

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

type codeError struct {
	code    string
	message string
}

func (e codeError) Error() string { return e.message }

func errCode(err error) string {
	var coded codeError
	if errors.As(err, &coded) {
		return coded.code
	}
	return ErrorMissingField
}

func requireForm(c *gin.Context, key string) (string, bool) {
	value := strings.TrimSpace(c.PostForm(key))
	if value == "" {
		Fail(c, ErrorMissingField, http.StatusBadRequest, "Missing field: "+key)
		return "", false
	}
	return value, true
}

var errUploadTooLarge = errors.New("upload too large")

func readUploadedPackage(file *multipart.FileHeader, maxBytes int64) ([]byte, error) {
	if maxBytes > 0 && file.Size > maxBytes {
		return nil, errUploadTooLarge
	}
	stream, err := file.Open()
	if err != nil {
		return nil, err
	}
	defer stream.Close()

	limit := maxBytes + 1
	if limit <= 1 {
		limit = 10*1024*1024 + 1
	}
	data, err := io.ReadAll(io.LimitReader(stream, limit))
	if err != nil {
		return nil, err
	}
	if maxBytes > 0 && int64(len(data)) > maxBytes {
		return nil, errUploadTooLarge
	}
	return data, nil
}

func DecryptPackage(data []byte, keyMaterial string) ([]byte, error) {
	if len(data) < len(PackageMagic)+12+16 || !bytes.HasPrefix(data, []byte(PackageMagic)) {
		return nil, errors.New("encrypted package format is invalid")
	}
	if keyMaterial == "" {
		keyMaterial = "ymhut-box-feedback-package-v1"
	}
	key := sha256.Sum256([]byte(keyMaterial))
	block, err := aes.NewCipher(key[:])
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	offset := len(PackageMagic)
	nonce := data[offset : offset+12]
	offset += 12
	tag := data[offset : offset+16]
	offset += 16
	ciphertext := data[offset:]
	combined := make([]byte, 0, len(ciphertext)+len(tag))
	combined = append(combined, ciphertext...)
	combined = append(combined, tag...)
	return gcm.Open(nil, nonce, combined, []byte(PackageMagic))
}

func ReadFeedbackPackage(plain []byte) (packageInfo, error) {
	return ReadFeedbackPackageWithGuard(plain, config.Default(".").UploadGuard)
}

func ReadFeedbackPackageWithGuard(plain []byte, guard config.UploadGuardConfig) (packageInfo, error) {
	reader, err := zip.NewReader(bytes.NewReader(plain), int64(len(plain)))
	if err != nil {
		return packageInfo{}, err
	}
	if guard.MaxZipFiles <= 0 {
		guard.MaxZipFiles = 80
	}
	if guard.MaxDecompressedBytes <= 0 {
		guard.MaxDecompressedBytes = 30 * 1024 * 1024
	}
	if guard.MaxSingleFileBytes <= 0 {
		guard.MaxSingleFileBytes = 8 * 1024 * 1024
	}
	if guard.MaxCompressionRatio <= 0 {
		guard.MaxCompressionRatio = 120
	}
	if guard.MaxReadableTextBytes <= 0 {
		guard.MaxReadableTextBytes = 256 * 1024
	}

	files := []string{}
	texts := map[string]string{}
	var total uint64
	for _, entry := range reader.File {
		if entry.FileInfo().IsDir() {
			continue
		}
		cleanName, err := safeZipName(entry.Name)
		if err != nil {
			return packageInfo{}, err
		}
		if len(files)+1 > guard.MaxZipFiles {
			return packageInfo{}, errors.New("zip contains too many files")
		}
		if entry.UncompressedSize64 > uint64(guard.MaxSingleFileBytes) {
			return packageInfo{}, errors.New("zip entry is too large")
		}
		total += entry.UncompressedSize64
		if total > uint64(guard.MaxDecompressedBytes) {
			return packageInfo{}, errors.New("zip decompressed size is too large")
		}
		if entry.CompressedSize64 == 0 && entry.UncompressedSize64 > 0 {
			return packageInfo{}, errors.New("zip entry has invalid compression metadata")
		}
		if entry.CompressedSize64 > 0 {
			ratio := float64(entry.UncompressedSize64) / float64(entry.CompressedSize64)
			if ratio > guard.MaxCompressionRatio {
				return packageInfo{}, errors.New("zip compression ratio is suspicious")
			}
		}

		files = append(files, cleanName)
		if cleanName != "feedback.json" && cleanName != "summary.txt" {
			if !guard.AllowUnexpectedZipFiles && !strings.HasPrefix(cleanName, "attachments/") {
				return packageInfo{}, errors.New("zip contains unexpected file")
			}
			continue
		}
		text, err := readZipText(entry, guard.MaxReadableTextBytes)
		if err != nil {
			return packageInfo{}, err
		}
		texts[cleanName] = text
	}

	request := map[string]any{}
	if raw := texts["feedback.json"]; raw != "" {
		var parsed map[string]any
		if err := json.Unmarshal([]byte(raw), &parsed); err == nil {
			if nested, ok := parsed["request"].(map[string]any); ok {
				request = nested
			}
		}
	}
	if len(request) == 0 && texts["feedback.json"] == "" {
		return packageInfo{}, errors.New("feedback.json is missing")
	}
	return packageInfo{Request: request, Summary: texts["summary.txt"], Files: files}, nil
}

func safeZipName(name string) (string, error) {
	name = strings.ReplaceAll(name, "\\", "/")
	name = strings.TrimSpace(name)
	if name == "" || strings.Contains(name, "\x00") || strings.HasPrefix(name, "/") {
		return "", errors.New("unsafe zip entry name")
	}
	clean := path.Clean(name)
	if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") {
		return "", errors.New("unsafe zip entry path")
	}
	return clean, nil
}

func readZipText(entry *zip.File, maxBytes int64) (string, error) {
	if int64(entry.UncompressedSize64) > maxBytes {
		return "", nil
	}
	reader, err := entry.Open()
	if err != nil {
		return "", err
	}
	defer reader.Close()
	data, err := io.ReadAll(io.LimitReader(reader, maxBytes+1))
	if err != nil {
		return "", err
	}
	if int64(len(data)) > maxBytes {
		return "", nil
	}
	return string(data), nil
}

func buildRecord(code string, payload submissionPayload, info packageInfo, encryptedPath, packagePath, packageSha256, plainPackageSha256, remoteAddr string) db.FeedbackRecord {
	now := db.Now()
	title := firstText(textFromMap(info.Request, "title"), payload.Title, "未命名反馈")
	typ := firstText(textFromMap(info.Request, "type"), payload.Type, "issue")
	severity := firstText(textFromMap(info.Request, "severity"), payload.Severity, "normal")
	contact := firstText(textFromMap(info.Request, "contact"), payload.Contact, "")
	body := firstText(textFromMap(info.Request, "body"), "", "")
	priority := normalizePriority(severity)
	return db.FeedbackRecord{
		Code:                 code,
		ReceivedAt:           now,
		Title:                shortenPlain(title, 240),
		Type:                 shortenPlain(typ, 80),
		Severity:             shortenPlain(severity, 80),
		Category:             normalizeCategory(typ),
		Priority:             priority,
		Contact:              shortenPlain(contact, 240),
		Body:                 shortenPlain(body, 5000),
		Status:               "new",
		StatusDetail:         "反馈已接收，等待后台处理。",
		Note:                 "",
		PublicReply:          "",
		HandledBy:            "",
		Assignee:             "",
		DueAt:                "",
		ResolvedAt:           "",
		ArchivedAt:           "",
		SLALevel:             defaultSLA(priority),
		SourceChannel:        "winui",
		RiskScore:            defaultRisk(priority),
		Resolution:           "",
		PackagePath:          packagePath,
		EncryptedPackagePath: encryptedPath,
		PackageSha256:        packageSha256,
		PlainPackageSha256:   plainPackageSha256,
		RemoteAddr:           shortenPlain(remoteAddr, 80),
		SummaryText:          shortenPlain(info.Summary, 6000),
		IncludedFiles:        strings.Join(info.Files, ", "),
		MailSent:             false,
		UpdatedAt:            now,
		LastActivityAt:       now,
	}
}

func (s *Service) generateCode() string {
	for {
		random := make([]byte, 3)
		if _, err := rand.Read(random); err != nil {
			panic(err)
		}
		code := "FB-" + time.Now().UTC().Format("20060102") + "-" + strings.ToUpper(hex.EncodeToString(random))
		existing, _ := s.store.FetchStatus(code)
		if existing == nil {
			return code
		}
	}
}

func StatusLabel(status string) string {
	switch status {
	case "triaged":
		return "已归类"
	case "investigating":
		return "处理中"
	case "resolved":
		return "已解决"
	case "archived":
		return "已归档"
	default:
		return "新反馈"
	}
}

func TypeLabel(value string) string {
	switch strings.ToLower(value) {
	case "suggestion":
		return "建议"
	case "ui":
		return "界面反馈"
	case "other":
		return "其他"
	default:
		return "问题"
	}
}

func SeverityLabel(value string) string {
	switch strings.ToLower(value) {
	case "major":
		return "影响使用"
	case "blocking":
		return "阻塞"
	default:
		return "普通"
	}
}

func Fail(c *gin.Context, code string, status int, message string) {
	c.JSON(status, gin.H{
		"ok":      false,
		"error":   code,
		"message": message,
	})
}

func firstText(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func textFromMap(values map[string]any, key string) string {
	if value, ok := values[key].(string); ok {
		return value
	}
	return ""
}

func shortenPlain(value string, max int) string {
	value = strings.TrimSpace(strings.ReplaceAll(value, "\x00", ""))
	if max <= 0 || len([]rune(value)) <= max {
		return value
	}
	runes := []rune(value)
	return string(runes[:max])
}

func isHexSHA256(value string) bool {
	value = strings.ToLower(strings.TrimSpace(value))
	if len(value) != 64 {
		return false
	}
	_, err := hex.DecodeString(value)
	return err == nil
}

func sha256Hex(data []byte) string {
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:])
}

func isZipBytes(data []byte) bool {
	return bytes.HasPrefix(data, []byte("PK\x03\x04")) ||
		bytes.HasPrefix(data, []byte("PK\x05\x06")) ||
		bytes.HasPrefix(data, []byte("PK\x07\x08"))
}

func normalizeCategory(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "suggestion", "ui", "other":
		return strings.ToLower(strings.TrimSpace(value))
	default:
		return "issue"
	}
}

func normalizePriority(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "major", "blocking":
		return strings.ToLower(strings.TrimSpace(value))
	default:
		return "normal"
	}
}

func defaultSLA(priority string) string {
	switch normalizePriority(priority) {
	case "blocking":
		return "urgent"
	case "major":
		return "elevated"
	default:
		return "standard"
	}
}

func defaultRisk(priority string) int {
	switch normalizePriority(priority) {
	case "blocking":
		return 90
	case "major":
		return 65
	default:
		return 30
	}
}

func DebugEncryptPackageForTest(plain []byte, keyMaterial string, nonce []byte) ([]byte, error) {
	if len(nonce) != 12 {
		return nil, fmt.Errorf("nonce must be 12 bytes")
	}
	if keyMaterial == "" {
		keyMaterial = "ymhut-box-feedback-package-v1"
	}
	key := sha256.Sum256([]byte(keyMaterial))
	block, err := aes.NewCipher(key[:])
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	sealed := gcm.Seal(nil, nonce, plain, []byte(PackageMagic))
	ciphertext := sealed[:len(sealed)-gcm.Overhead()]
	tag := sealed[len(sealed)-gcm.Overhead():]
	out := []byte(PackageMagic)
	out = append(out, nonce...)
	out = append(out, tag...)
	out = append(out, ciphertext...)
	return out, nil
}
