package feedback

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

	"ymhut-box/server/unified-management/internal/config"
	"ymhut-box/server/unified-management/internal/db"
	feedbackmail "ymhut-box/server/unified-management/internal/mail"
)

const PackageMagic = "YMHUTFB1"

const (
	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"
	ErrorServerConfig            = "SERVER_CONFIG"
)

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

type requestContextKey string

const duplicateContextKey requestContextKey = "ymhut.feedback.duplicate"

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

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) *Service {
	return &Service{cfg: cfg, store: store}
}

func (s *Service) Submit(r *http.Request) (db.Feedback, error) {
	contentType := r.Header.Get("Content-Type")
	var item db.Feedback
	var err error
	if strings.Contains(contentType, "multipart/form-data") {
		if item, err = s.submitMultipart(r); err == nil {
			if !DuplicateSubmission(r) && s.NotifyFeedback(item) == nil {
				item.MailSent = true
			}
			return item, nil
		} else if hasSignedFields(r) || !strings.Contains(strings.ToLower(err.Error()), "signed multipart fields are required") {
			return db.Feedback{}, err
		}
	}
	item, err = s.submitSimple(r)
	if err == nil && s.NotifyFeedback(item) == nil {
		item.MailSent = true
	}
	return item, err
}

func (s *Service) RetryMail(code string) error {
	item, err := s.store.GetFeedback(NormalizeCode(code))
	if err != nil {
		return err
	}
	return s.NotifyFeedback(item)
}

func (s *Service) NotifyFeedback(item db.Feedback) error {
	message, err := feedbackmail.BuildFeedbackMessage(s.cfg, item)
	if err != nil {
		_, _ = s.store.InsertMailRecord(db.LegacyMailRecord{
			FeedbackCode: item.Code,
			Kind:         "feedback",
			Status:       "failed",
			Subject:      "反馈邮件未发送",
			ErrorMessage: err.Error(),
			CreatedAt:    db.Now(),
		})
		_ = s.store.UpdateFeedbackMailState(item.Code, false)
		return err
	}
	mailID, err := s.store.InsertMailRecord(db.LegacyMailRecord{
		FeedbackCode:   item.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 {
		return err
	}
	if err := feedbackmail.Send(s.cfg, message); err != nil {
		_ = s.store.UpdateMailState(mailID, "failed", err.Error())
		_ = s.store.UpdateFeedbackMailState(item.Code, false)
		return err
	}
	_ = s.store.UpdateMailState(mailID, "sent", "")
	_ = s.store.UpdateFeedbackMailState(item.Code, true)
	return nil
}

func (s *Service) submitSimple(r *http.Request) (db.Feedback, error) {
	if strings.Contains(r.Header.Get("Content-Type"), "application/json") {
		var payload map[string]any
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			return db.Feedback{}, err
		}
		item := db.Feedback{
			Code:       db.NewFeedbackCode(),
			Title:      value(payload, "title", "客户端反馈"),
			Type:       value(payload, "type", "issue"),
			Severity:   value(payload, "severity", "normal"),
			Contact:    value(payload, "contact", ""),
			Body:       value(payload, "body", value(payload, "message", "")),
			Status:     "new",
			RemoteAddr: r.RemoteAddr,
		}
		if strings.TrimSpace(item.Body) == "" {
			item.Body = "No feedback body provided."
		}
		return item, s.store.InsertFeedback(item)
	}
	if err := r.ParseMultipartForm(32 << 20); err != nil {
		_ = r.ParseForm()
	}
	item := db.Feedback{
		Code:       db.NewFeedbackCode(),
		Title:      firstNonEmpty(r.FormValue("title"), r.FormValue("subject"), "客户端反馈"),
		Type:       firstNonEmpty(r.FormValue("type"), r.FormValue("category"), "issue"),
		Severity:   firstNonEmpty(r.FormValue("severity"), r.FormValue("priority"), "normal"),
		Contact:    firstNonEmpty(r.FormValue("contact"), r.FormValue("email")),
		Body:       firstNonEmpty(r.FormValue("body"), r.FormValue("message"), r.FormValue("description")),
		Status:     "new",
		RemoteAddr: r.RemoteAddr,
	}
	if strings.TrimSpace(item.Body) == "" {
		item.Body = "No feedback body provided."
	}
	return item, s.store.InsertFeedback(item)
}

func (s *Service) submitMultipart(r *http.Request) (db.Feedback, error) {
	if s.cfg.MaxRequestBytes > 0 {
		if r.ContentLength > s.cfg.MaxRequestBytes {
			return db.Feedback{}, errors.New("request is too large")
		}
		r.Body = http.MaxBytesReader(nilResponseWriter{}, r.Body, s.cfg.MaxRequestBytes)
	}
	if err := r.ParseMultipartForm(s.cfg.MaxRequestBytes); err != nil {
		return db.Feedback{}, err
	}
	payloadText := strings.TrimSpace(r.FormValue("payload"))
	timestamp := strings.TrimSpace(r.FormValue("timestamp"))
	nonce := strings.TrimSpace(r.FormValue("nonce"))
	packageSha256 := strings.ToLower(strings.TrimSpace(r.FormValue("packageSha256")))
	signature := strings.ToLower(strings.TrimSpace(r.FormValue("signature")))
	if payloadText == "" || timestamp == "" || nonce == "" || packageSha256 == "" || signature == "" {
		return db.Feedback{}, errors.New("signed multipart fields are required")
	}
	if !validTimestamp(timestamp, s.cfg.TimestampWindowSeconds) {
		return db.Feedback{}, errors.New("timestamp outside accepted window")
	}
	if !isHexSHA256(packageSha256) {
		return db.Feedback{}, errors.New("invalid package hash")
	}
	expected := SignWithKey(s.cfg.ClientSignatureKey, timestamp, nonce, packageSha256, payloadText)
	if !hmac.Equal([]byte(expected), []byte(signature)) {
		return db.Feedback{}, errors.New("invalid request signature")
	}
	var payload submissionPayload
	if err := json.Unmarshal([]byte(payloadText), &payload); err != nil {
		return db.Feedback{}, err
	}
	if err := validatePayload(payload); err != nil {
		return db.Feedback{}, err
	}
	code := NormalizeCode(payload.FeedbackCode)
	if code == "" {
		code = db.NewFeedbackCode()
	}
	if existing, err := s.store.GetFeedback(code); err == nil {
		setDuplicateSubmission(r, true)
		return existing, nil
	}
	file, _, err := r.FormFile("package")
	if err != nil {
		return db.Feedback{}, errors.New("missing package file")
	}
	defer file.Close()
	data, err := readUploadedPackage(file, s.cfg.MaxPackageBytes)
	if err != nil {
		return db.Feedback{}, err
	}
	if !bytes.HasPrefix(data, []byte(PackageMagic)) {
		return db.Feedback{}, errors.New("encrypted package format is invalid")
	}
	if !hmac.Equal([]byte(sha256Hex(data)), []byte(packageSha256)) {
		return db.Feedback{}, errors.New("package hash mismatch")
	}
	plain, err := DecryptPackage(data, s.cfg.PackageEncryptionKey)
	if err != nil {
		return db.Feedback{}, err
	}
	if !isZipBytes(plain) {
		return db.Feedback{}, errors.New("decrypted package is not a zip")
	}
	if payload.PlainPackageSha256 != "" && isHexSHA256(payload.PlainPackageSha256) {
		if !hmac.Equal([]byte(sha256Hex(plain)), []byte(strings.ToLower(payload.PlainPackageSha256))) {
			return db.Feedback{}, errors.New("decrypted package hash mismatch")
		}
	}
	info, err := ReadFeedbackPackageWithGuard(plain, s.cfg.UploadGuard)
	if err != nil {
		return db.Feedback{}, err
	}
	dir := filepath.Join(s.cfg.StorageDir, "feedback")
	if err := os.MkdirAll(dir, 0o750); err != nil {
		return db.Feedback{}, err
	}
	encryptedPath := filepath.Join(dir, code+".ymfb")
	packagePath := filepath.Join(dir, code+".zip")
	if err := os.WriteFile(encryptedPath, data, 0o640); err != nil {
		return db.Feedback{}, err
	}
	if err := os.WriteFile(packagePath, plain, 0o640); err != nil {
		return db.Feedback{}, err
	}
	item := buildRecord(code, payload, info, encryptedPath, packagePath, packageSha256, strings.ToLower(payload.PlainPackageSha256), r.RemoteAddr)
	setDuplicateSubmission(r, false)
	return item, s.store.InsertFeedback(item)
}

func DuplicateSubmission(r *http.Request) bool {
	duplicate, _ := r.Context().Value(duplicateContextKey).(bool)
	return duplicate
}

func setDuplicateSubmission(r *http.Request, duplicate bool) {
	*r = *r.WithContext(context.WithValue(r.Context(), duplicateContextKey, duplicate))
}

func LegacyError(err error) (string, int) {
	if err == nil {
		return "", http.StatusOK
	}
	lower := strings.ToLower(err.Error())
	switch {
	case strings.Contains(lower, "too large"):
		return ErrorTooLarge, http.StatusRequestEntityTooLarge
	case strings.Contains(lower, "signed multipart fields") || strings.Contains(lower, "missing package"):
		return ErrorMissingField, http.StatusBadRequest
	case strings.Contains(lower, "timestamp outside"):
		return ErrorInvalidTimestamp, http.StatusBadRequest
	case strings.Contains(lower, "invalid request signature"):
		return ErrorInvalidSignature, http.StatusUnauthorized
	case strings.Contains(lower, "hash mismatch") || strings.Contains(lower, "invalid package hash"):
		return ErrorHashMismatch, http.StatusBadRequest
	case strings.Contains(lower, "encrypted package format") || strings.Contains(lower, "encrypted package is required"):
		return ErrorInvalidEncryptedPackage, http.StatusBadRequest
	case strings.Contains(lower, "message authentication failed") || strings.Contains(lower, "decrypt"):
		return ErrorDecryptFailed, http.StatusBadRequest
	case strings.Contains(lower, "payload") || strings.Contains(lower, "json"):
		return ErrorInvalidPayload, http.StatusBadRequest
	case strings.Contains(lower, "zip") || strings.Contains(lower, "package"):
		return ErrorInvalidPackage, http.StatusBadRequest
	default:
		return ErrorServerConfig, http.StatusBadRequest
	}
}

func hasSignedFields(r *http.Request) bool {
	if r.MultipartForm == nil {
		return false
	}
	for _, key := range []string{"payload", "timestamp", "nonce", "packageSha256", "signature"} {
		if strings.TrimSpace(r.FormValue(key)) == "" {
			return false
		}
	}
	return true
}

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(payload submissionPayload) error {
	if payload.Title == "" || payload.Type == "" || payload.Severity == "" {
		return errors.New("payload title, type and severity are required")
	}
	if payload.PackageBytes <= 0 || payload.PackageSha256 == "" || payload.PlainPackageSha256 == "" {
		return errors.New("payload package hashes are required")
	}
	if !payload.PackageEncrypted || payload.Encryption != PackageMagic {
		return errors.New("encrypted package is required")
	}
	return nil
}

func readUploadedPackage(file multipart.File, maxBytes int64) ([]byte, error) {
	limit := maxBytes + 1
	if limit <= 1 {
		limit = 10*1024*1024 + 1
	}
	data, err := io.ReadAll(io.LimitReader(file, limit))
	if err != nil {
		return nil, err
	}
	if maxBytes > 0 && int64(len(data)) > maxBytes {
		return nil, errors.New("feedback package is too large")
	}
	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 := append(append([]byte{}, ciphertext...), tag...)
	return gcm.Open(nil, nonce, combined, []byte(PackageMagic))
}

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
	}
	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 && float64(entry.UncompressedSize64)/float64(entry.CompressedSize64) > 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
			} else {
				request = parsed
			}
		}
	}
	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.Feedback {
	title := firstNonEmpty(textFromMap(info.Request, "title"), payload.Title, "未命名反馈")
	typ := firstNonEmpty(textFromMap(info.Request, "type"), payload.Type, "issue")
	severity := firstNonEmpty(textFromMap(info.Request, "severity"), payload.Severity, "normal")
	contact := firstNonEmpty(textFromMap(info.Request, "contact"), payload.Contact)
	body := firstNonEmpty(textFromMap(info.Request, "body"), info.Summary)
	return db.Feedback{
		Code:                 code,
		Title:                title,
		Type:                 typ,
		Severity:             severity,
		Contact:              contact,
		Body:                 body,
		Status:               "new",
		StatusDetail:         "反馈已接收，等待后台处理。",
		SourceChannel:        "winui",
		PackagePath:          packagePath,
		EncryptedPackagePath: encryptedPath,
		PackageSha256:        packageSha256,
		PlainPackageSha256:   plainPackageSha256,
		SummaryText:          info.Summary,
		IncludedFiles:        strings.Join(info.Files, ", "),
		RemoteAddr:           remoteAddr,
	}
}

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

func value(payload map[string]any, key, fallback string) string {
	if raw, ok := payload[key].(string); ok && strings.TrimSpace(raw) != "" {
		return strings.TrimSpace(raw)
	}
	return fallback
}

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

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

type nilResponseWriter struct{}

func (nilResponseWriter) Header() http.Header       { return http.Header{} }
func (nilResponseWriter) Write([]byte) (int, error) { return 0, nil }
func (nilResponseWriter) WriteHeader(int)           {}
