package web

import (
	"encoding/base64"
	"encoding/csv"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

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

func NewRouter(cfg *config.Config, store *db.Store, authService *auth.Service) *gin.Engine {
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()
	router.Use(gin.Logger(), gin.Recovery())

	limiter := newRateLimitSet(cfg)
	hooks := webhook.NewDispatcher(cfg, store)
	feedbackService := feedback.NewService(cfg, store, hooks)
	distDir := filepath.Join(cfg.BaseDir, "web", "dist")

	router.GET("/", func(c *gin.Context) {
		if c.Query("api") == "status" {
			if !limiter.allow("status", c.ClientIP()) {
				tooManyRequests(c)
				return
			}
			feedbackService.HandleStatus(c)
			return
		}
		serveAppIndex(c, distDir)
	})
	router.POST("/", limiter.middleware("submission"), feedbackService.HandleSubmission)

	api := router.Group("/api")
	api.GET("/auth/captcha", limiter.middleware("captcha"), func(c *gin.Context) {
		captcha, err := authService.NewCaptcha()
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "CAPTCHA_FAILED", "message": "Unable to create captcha"})
			return
		}
		c.JSON(http.StatusOK, gin.H{
			"ok":        true,
			"captchaId": captcha.ID,
			"image":     "data:image/png;base64," + base64.StdEncoding.EncodeToString(captcha.ImagePNG),
		})
	})
	api.POST("/auth/login", limiter.middleware("login"), func(c *gin.Context) {
		var body struct {
			Password      string `json:"password"`
			CaptchaID     string `json:"captchaId"`
			CaptchaAnswer string `json:"captcha"`
		}
		if err := c.ShouldBindJSON(&body); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid login payload"})
			return
		}
		sessionID, csrf, ok := authService.Login(body.Password, body.CaptchaID, body.CaptchaAnswer)
		if !ok {
			_ = audit(c, store, "admin", "login.failed", "", "Login failed")
			c.JSON(http.StatusUnauthorized, gin.H{"ok": false, "error": "LOGIN_FAILED", "message": "Password or captcha is incorrect"})
			return
		}
		authService.SetSessionCookie(c, sessionID)
		_ = audit(c, store, "admin", "login.success", "", "Login succeeded")
		c.JSON(http.StatusOK, gin.H{"ok": true, "csrfToken": csrf})
	})

	api.POST("/auth/logout", authService.RequireAuth, authService.RequireCSRF, func(c *gin.Context) {
		_ = audit(c, store, "admin", "logout", "", "Logout")
		authService.Logout(c)
		c.JSON(http.StatusOK, gin.H{"ok": true})
	})

	admin := api.Group("/admin", limiter.adminMiddleware(), authService.RequireAuth, authService.RequireCSRF)
	admin.GET("/overview", func(c *gin.Context) {
		overview, err := store.Overview()
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to load overview"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"ok": true, "overview": overview})
	})
	admin.GET("/config", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"ok": true, "config": buildConfigHealth(cfg, store)})
	})
	admin.PATCH("/config", func(c *gin.Context) {
		handleConfigUpdate(c, cfg, store)
	})
	admin.POST("/config/test/database", func(c *gin.Context) {
		var body config.DatabaseConfig
		if err := c.ShouldBindJSON(&body); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid database config"})
			return
		}
		if body.Password == "" {
			body.Password = cfg.Database.Password
		}
		testCfg := *cfg
		testCfg.Database = body
		if err := db.TestDatabase(testCfg.Database, cfg.BaseDir); err != nil {
			_ = audit(c, store, "admin", "config.database_test.failed", "", err.Error())
			c.JSON(http.StatusBadGateway, gin.H{"ok": false, "error": "DATABASE_TEST_FAILED", "message": err.Error()})
			return
		}
		_ = audit(c, store, "admin", "config.database_test.ok", "", "Database connection test succeeded")
		c.JSON(http.StatusOK, gin.H{"ok": true})
	})
	admin.POST("/config/test/webhook", func(c *gin.Context) {
		var hook config.WebhookConfig
		if err := c.ShouldBindJSON(&hook); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid webhook config"})
			return
		}
		if hook.Secret == "" {
			for _, existing := range cfg.Webhooks {
				if existing.Name == hook.Name || existing.URL == hook.URL {
					hook.Secret = existing.Secret
					break
				}
			}
		}
		temp := *cfg
		temp.Webhooks = []config.WebhookConfig{hook}
		count := webhook.NewDispatcher(&temp, store).DispatchTest(gin.H{"test": true, "sentAt": db.Now()})
		_ = audit(c, store, "admin", "config.webhook_test", hook.Name, fmt.Sprintf("Triggered %d webhook tests", count))
		c.JSON(http.StatusOK, gin.H{"ok": true, "sent": count})
	})
	admin.GET("/database/status", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"ok": true, "database": store.Status()})
	})
	admin.POST("/database/import", func(c *gin.Context) {
		result, err := store.ImportSQLiteToRemote()
		if err != nil {
			_ = audit(c, store, "admin", "database.import.failed", "", err.Error())
			c.JSON(http.StatusBadGateway, gin.H{"ok": false, "error": "DATABASE_IMPORT_FAILED", "message": err.Error()})
			return
		}
		_ = audit(c, store, "admin", "database.import", "", "SQLite imported to remote database")
		c.JSON(http.StatusOK, gin.H{"ok": true, "result": result})
	})
	admin.POST("/database/sync", func(c *gin.Context) {
		result, err := store.SyncNow()
		if err != nil {
			_ = audit(c, store, "admin", "database.sync.failed", "", err.Error())
			c.JSON(http.StatusBadGateway, gin.H{"ok": false, "error": "DATABASE_SYNC_FAILED", "message": err.Error()})
			return
		}
		_ = audit(c, store, "admin", "database.sync", "", "Database sync completed")
		c.JSON(http.StatusOK, gin.H{"ok": true, "result": result})
	})
	admin.GET("/feedbacks", func(c *gin.Context) {
		page := queryInt(c, "page", 1)
		perPage := queryInt(c, "perPage", 20)
		result, err := store.ListFeedbacks(page, perPage, feedbackFiltersFromQuery(c))
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to load feedbacks"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"ok": true, "page": result})
	})
	admin.GET("/feedbacks/export", func(c *gin.Context) {
		records, err := store.ExportFeedbacks(feedbackFiltersFromQuery(c), 10000)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to export feedbacks"})
			return
		}
		_ = audit(c, store, "admin", "feedback.export", "", fmt.Sprintf("Exported %d feedbacks", len(records)))
		writeFeedbackCSV(c, records)
	})
	admin.GET("/feedbacks/summary", func(c *gin.Context) {
		summary, err := store.FeedbackSummary()
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to load feedback summary"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"ok": true, "summary": summary})
	})
	admin.GET("/feedbacks/:code", func(c *gin.Context) {
		code := feedback.NormalizeCode(c.Param("code"))
		if code == "" {
			c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid feedback code"})
			return
		}
		detail, err := store.GetFeedbackDetail(code)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to load feedback detail"})
			return
		}
		if detail == nil {
			c.JSON(http.StatusNotFound, gin.H{"ok": false, "error": "NOT_FOUND", "message": "Feedback not found"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"ok": true, "feedback": detail})
	})
	admin.PATCH("/feedbacks/bulk", func(c *gin.Context) {
		handleBulkFeedbackUpdate(c, store, hooks)
	})
	admin.PATCH("/feedbacks/:code", func(c *gin.Context) {
		handleFeedbackUpdate(c, store, hooks)
	})
	admin.POST("/feedbacks/:code/comments", func(c *gin.Context) {
		handleFeedbackComment(c, store, hooks)
	})

	admin.GET("/mails", func(c *gin.Context) {
		page := queryInt(c, "page", 1)
		perPage := queryInt(c, "perPage", 20)
		status := strings.TrimSpace(c.Query("status"))
		result, err := store.ListMails(page, perPage, status)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to load mail records"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"ok": true, "page": result})
	})
	admin.POST("/mails/test", func(c *gin.Context) {
		message, err := feedbackmail.BuildTestMessage(cfg)
		if err != nil {
			c.JSON(http.StatusBadGateway, gin.H{"ok": false, "error": "MAIL_FAILED", "message": err.Error()})
			return
		}
		id, err := store.InsertMail(db.MailRecord{
			FeedbackCode:   "",
			Kind:           "test",
			Status:         "pending",
			ToAddress:      message.To,
			Subject:        message.Subject,
			PlainBody:      message.PlainBody,
			HTMLBody:       message.HTMLBody,
			AttachmentPath: "",
			AttachmentName: "",
			CreatedAt:      db.Now(),
		})
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to retain mail record"})
			return
		}
		if err := feedbackmail.Send(cfg, message); err != nil {
			_ = store.UpdateMailState(id, "failed", limitText(err.Error(), 1000))
			hooks.Dispatch("mail.failed", gin.H{"mailId": id, "kind": "test", "error": err.Error()})
			_ = audit(c, store, "admin", "mail.test.failed", "", err.Error())
			c.JSON(http.StatusBadGateway, gin.H{"ok": false, "error": "MAIL_FAILED", "message": err.Error()})
			return
		}
		_ = store.UpdateMailState(id, "sent", "")
		_ = audit(c, store, "admin", "mail.test.sent", "", "Test mail sent")
		c.JSON(http.StatusOK, gin.H{"ok": true})
	})

	admin.GET("/audit-logs", func(c *gin.Context) {
		page, err := store.ListAuditLogs(queryInt(c, "page", 1), queryInt(c, "perPage", 20), c.Query("actor"), c.Query("type"))
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to load audit logs"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"ok": true, "page": page})
	})
	admin.GET("/webhooks/deliveries", func(c *gin.Context) {
		page, err := store.ListWebhookDeliveries(queryInt(c, "page", 1), queryInt(c, "perPage", 20), c.Query("status"))
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to load webhook deliveries"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"ok": true, "page": page, "webhooks": webhookSummaries(cfg)})
	})
	admin.POST("/webhooks/test", func(c *gin.Context) {
		count := hooks.DispatchTest(gin.H{"test": true, "sentAt": db.Now()})
		_ = audit(c, store, "admin", "webhook.test", "", fmt.Sprintf("Triggered %d webhook tests", count))
		c.JSON(http.StatusOK, gin.H{"ok": true, "sent": count})
	})

	admin.POST("/backups/database", func(c *gin.Context) {
		name := "feedback-" + time.Now().UTC().Format("20060102-150405") + ".sqlite"
		path := filepath.Join(cfg.Backup.Dir, name)
		if !isInside(cfg.Backup.Dir, path) {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Invalid backup path"})
			return
		}
		if err := store.BackupDatabase(path); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to create backup"})
			return
		}
		_ = audit(c, store, "admin", "backup.created", name, "Database backup created")
		c.JSON(http.StatusOK, gin.H{"ok": true, "backup": backupInfo(path)})
	})
	admin.GET("/backups", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"ok": true, "backups": listBackups(cfg.Backup.Dir)})
	})
	admin.GET("/backups/:name", func(c *gin.Context) {
		name := c.Param("name")
		if !safeBackupName(name) {
			c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid backup name"})
			return
		}
		path := filepath.Join(cfg.Backup.Dir, name)
		if !isInside(cfg.Backup.Dir, path) || !fileExists(path) {
			c.JSON(http.StatusNotFound, gin.H{"ok": false, "error": "NOT_FOUND", "message": "Backup not found"})
			return
		}
		_ = audit(c, store, "admin", "backup.download", name, "Database backup downloaded")
		c.FileAttachment(path, name)
	})

	mountAdmin(router, distDir)
	return router
}

func mountAdmin(router *gin.Engine, distDir string) {
	router.GET("/admin", func(c *gin.Context) {
		c.Redirect(http.StatusMovedPermanently, "/admin/")
	})
	router.GET("/admin/*path", func(c *gin.Context) {
		requested := strings.TrimPrefix(c.Param("path"), "/")
		if requested != "" {
			path := filepath.Join(distDir, filepath.FromSlash(requested))
			if isInside(distDir, path) {
				if info, err := os.Stat(path); err == nil && !info.IsDir() {
					c.File(path)
					return
				}
			}
		}
		index := filepath.Join(distDir, "index.html")
		if info, err := os.Stat(index); err == nil && !info.IsDir() {
			c.File(index)
			return
		}
		c.Header("Content-Type", "text/html; charset=utf-8")
		c.String(http.StatusOK, "<!doctype html><meta charset=\"utf-8\"><title>YMhut Box Feedback</title><main style=\"font-family:system-ui;padding:32px\"><h1>YMhut Box Feedback</h1><p>Admin frontend is not built. Run npm --prefix admin-web run build.</p></main>")
	})
}

func serveAppIndex(c *gin.Context, distDir string) {
	index := filepath.Join(distDir, "index.html")
	if info, err := os.Stat(index); err == nil && !info.IsDir() {
		c.File(index)
		return
	}
	c.Header("Content-Type", "text/html; charset=utf-8")
	c.String(http.StatusOK, "<!doctype html><meta charset=\"utf-8\"><title>YMhut Box Feedback</title><main style=\"font-family:system-ui;padding:32px\"><h1>YMhut Box Feedback</h1><p>Frontend is not built. Run npm --prefix admin-web run build.</p></main>")
}

func handleFeedbackUpdate(c *gin.Context, store *db.Store, hooks *webhook.Dispatcher) {
	code := feedback.NormalizeCode(c.Param("code"))
	if code == "" {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid feedback code"})
		return
	}
	current, err := store.GetFeedback(code)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to load feedback"})
		return
	}
	if current == nil {
		c.JSON(http.StatusNotFound, gin.H{"ok": false, "error": "NOT_FOUND", "message": "Feedback not found"})
		return
	}
	var body struct {
		Status       string   `json:"status"`
		Category     string   `json:"category"`
		Priority     string   `json:"priority"`
		StatusDetail string   `json:"statusDetail"`
		HandledBy    string   `json:"handledBy"`
		Assignee     string   `json:"assignee"`
		DueAt        string   `json:"dueAt"`
		SLALevel     string   `json:"slaLevel"`
		Resolution   string   `json:"resolution"`
		Note         string   `json:"note"`
		PublicReply  string   `json:"publicReply"`
		Tags         []string `json:"tags"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid feedback payload"})
		return
	}
	if !allowedStatus(body.Status) || !allowedCategory(body.Category) || !allowedPriority(body.Priority) || !allowedSLA(body.SLALevel) {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid workflow values"})
		return
	}
	if body.DueAt != "" && !validOptionalTime(body.DueAt) {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid dueAt"})
		return
	}
	update := db.FeedbackUpdate{
		Status:       body.Status,
		Category:     body.Category,
		Priority:     body.Priority,
		StatusDetail: limitText(body.StatusDetail, 1000),
		HandledBy:    limitText(body.HandledBy, 120),
		Assignee:     limitText(body.Assignee, 120),
		DueAt:        limitText(body.DueAt, 80),
		SLALevel:     body.SLALevel,
		Resolution:   limitText(body.Resolution, 2000),
		Note:         limitText(body.Note, 3000),
		PublicReply:  limitText(body.PublicReply, 3000),
		Actor:        firstNonEmpty(body.HandledBy, body.Assignee, "admin"),
		Tags:         body.Tags,
	}
	if err := store.UpdateFeedback(code, update); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to update feedback"})
		return
	}
	detail, _ := store.GetFeedbackDetail(code)
	_ = audit(c, store, update.Actor, "feedback.updated", code, "Feedback updated")
	hooks.Dispatch("feedback.updated", detail)
	if current.Status != body.Status {
		hooks.Dispatch("feedback.status_changed", gin.H{"code": code, "from": current.Status, "to": body.Status, "feedback": detail})
	}
	c.JSON(http.StatusOK, gin.H{"ok": true, "feedback": detail})
}

func handleBulkFeedbackUpdate(c *gin.Context, store *db.Store, hooks *webhook.Dispatcher) {
	var body struct {
		Codes        []string `json:"codes"`
		Status       string   `json:"status"`
		Category     string   `json:"category"`
		Priority     string   `json:"priority"`
		StatusDetail string   `json:"statusDetail"`
		HandledBy    string   `json:"handledBy"`
		Assignee     string   `json:"assignee"`
		DueAt        string   `json:"dueAt"`
		SLALevel     string   `json:"slaLevel"`
		Resolution   string   `json:"resolution"`
		Note         string   `json:"note"`
		PublicReply  string   `json:"publicReply"`
		Tags         []string `json:"tags"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || len(body.Codes) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid bulk payload"})
		return
	}
	if body.Status != "" && !allowedStatus(body.Status) {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid feedback status"})
		return
	}
	if !allowedCategory(body.Category) || !allowedPriority(body.Priority) || !allowedSLA(body.SLALevel) {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid workflow values"})
		return
	}
	if body.DueAt != "" && !validOptionalTime(body.DueAt) {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid dueAt"})
		return
	}

	codes := make([]string, 0, len(body.Codes))
	for _, code := range body.Codes {
		if normalized := feedback.NormalizeCode(code); normalized != "" {
			codes = append(codes, normalized)
		}
	}
	if len(codes) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "No valid feedback codes"})
		return
	}
	actor := firstNonEmpty(body.HandledBy, body.Assignee, "admin")
	update := db.FeedbackUpdate{
		Status:       body.Status,
		Category:     body.Category,
		Priority:     body.Priority,
		StatusDetail: limitText(body.StatusDetail, 1000),
		HandledBy:    limitText(body.HandledBy, 120),
		Assignee:     limitText(body.Assignee, 120),
		DueAt:        limitText(body.DueAt, 80),
		SLALevel:     body.SLALevel,
		Resolution:   limitText(body.Resolution, 2000),
		Note:         limitText(body.Note, 3000),
		PublicReply:  limitText(body.PublicReply, 3000),
		Actor:        actor,
		Tags:         body.Tags,
	}
	if err := store.BulkUpdateFeedback(codes, update); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "SERVER_CONFIG", "message": "Unable to update feedbacks"})
		return
	}
	_ = audit(c, store, actor, "feedback.bulk_updated", strings.Join(codes, ","), fmt.Sprintf("Bulk updated %d feedbacks", len(codes)))
	for _, code := range codes {
		detail, _ := store.GetFeedbackDetail(code)
		hooks.Dispatch("feedback.updated", detail)
	}
	c.JSON(http.StatusOK, gin.H{"ok": true, "updated": len(codes)})
}

func handleFeedbackComment(c *gin.Context, store *db.Store, hooks *webhook.Dispatcher) {
	code := feedback.NormalizeCode(c.Param("code"))
	if code == "" {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid feedback code"})
		return
	}
	var body struct {
		Author   string `json:"author"`
		Body     string `json:"body"`
		Internal bool   `json:"internal"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid comment payload"})
		return
	}
	comment, err := store.InsertComment(db.FeedbackComment{
		FeedbackCode: code,
		Author:       limitText(firstNonEmpty(body.Author, "admin"), 120),
		Body:         limitText(body.Body, 3000),
		Internal:     body.Internal,
	})
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Unable to add comment"})
		return
	}
	_ = audit(c, store, comment.Author, "feedback.comment_created", code, "Comment created")
	hooks.Dispatch("feedback.comment_created", comment)
	c.JSON(http.StatusOK, gin.H{"ok": true, "comment": comment})
}

func feedbackFiltersFromQuery(c *gin.Context) db.FeedbackFilters {
	return db.FeedbackFilters{
		Status:   strings.TrimSpace(c.Query("status")),
		Category: strings.TrimSpace(c.Query("category")),
		Priority: strings.TrimSpace(c.Query("priority")),
		Mail:     strings.TrimSpace(c.Query("mail")),
		Query:    strings.TrimSpace(c.Query("q")),
		Assignee: strings.TrimSpace(c.Query("assignee")),
		Tag:      strings.TrimSpace(c.Query("tag")),
		SLA:      strings.TrimSpace(c.Query("sla")),
		Overdue:  strings.TrimSpace(c.Query("overdue")),
		Sort:     strings.TrimSpace(c.Query("sort")),
	}
}

func writeFeedbackCSV(c *gin.Context, records []db.FeedbackRecord) {
	c.Header("Content-Type", "text/csv; charset=utf-8")
	c.Header("Content-Disposition", `attachment; filename="feedbacks.csv"`)
	writer := csv.NewWriter(c.Writer)
	_ = writer.Write([]string{"code", "received_at", "title", "status", "category", "priority", "sla", "assignee", "due_at", "contact", "mail_sent", "tags", "status_detail", "public_reply", "resolution"})
	for _, item := range records {
		_ = writer.Write([]string{
			item.Code,
			item.ReceivedAt,
			item.Title,
			item.Status,
			item.Category,
			item.Priority,
			item.SLALevel,
			item.Assignee,
			item.DueAt,
			item.Contact,
			strconv.FormatBool(item.MailSent),
			strings.Join(item.Tags, "|"),
			item.StatusDetail,
			item.PublicReply,
			item.Resolution,
		})
	}
	writer.Flush()
}

func handleConfigUpdate(c *gin.Context, cfg *config.Config, store *db.Store) {
	var body struct {
		Listen      *string                   `json:"listen"`
		Database    *config.DatabaseConfig    `json:"database"`
		Mail        *config.MailConfig        `json:"mail"`
		Webhooks    *[]config.WebhookConfig   `json:"webhooks"`
		Backup      *config.BackupConfig      `json:"backup"`
		RateLimit   *config.RateLimitConfig   `json:"rateLimit"`
		UploadGuard *config.UploadGuardConfig `json:"uploadGuard"`
		Security    *struct {
			AdminPasswordHash    string `json:"adminPasswordHash"`
			AdminPassword        string `json:"adminPassword"`
			ClientSignatureKey   string `json:"clientSignatureKey"`
			PackageEncryptionKey string `json:"packageEncryptionKey"`
		} `json:"security"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_PAYLOAD", "message": "Invalid config payload"})
		return
	}

	next := *cfg
	if body.Listen != nil {
		next.Listen = strings.TrimSpace(*body.Listen)
	}
	if body.Database != nil {
		database := *body.Database
		if strings.TrimSpace(database.Password) == "" {
			database.Password = cfg.Database.Password
		}
		next.Database = database
	}
	if body.Mail != nil {
		mail := *body.Mail
		if strings.TrimSpace(mail.Password) == "" {
			mail.Password = cfg.Mail.Password
		}
		next.Mail = mail
	}
	if body.Webhooks != nil {
		next.Webhooks = mergeWebhookSecrets(*body.Webhooks, cfg.Webhooks)
	}
	if body.Backup != nil {
		next.Backup = *body.Backup
	}
	if body.RateLimit != nil {
		next.RateLimit = *body.RateLimit
	}
	if body.UploadGuard != nil {
		next.UploadGuard = *body.UploadGuard
	}
	if body.Security != nil {
		if strings.TrimSpace(body.Security.AdminPasswordHash) != "" {
			next.AdminPasswordHash = body.Security.AdminPasswordHash
		}
		if strings.TrimSpace(body.Security.AdminPassword) != "" {
			next.AdminPassword = body.Security.AdminPassword
		}
		if strings.TrimSpace(body.Security.ClientSignatureKey) != "" {
			next.ClientSignatureKey = body.Security.ClientSignatureKey
		}
		if strings.TrimSpace(body.Security.PackageEncryptionKey) != "" {
			next.PackageEncryptionKey = body.Security.PackageEncryptionKey
		}
	}

	if body.Database != nil {
		testDatabase := next.Database
		if err := db.TestDatabase(testDatabase, next.BaseDir); err != nil {
			_ = audit(c, store, "admin", "config.save.database_failed", "", err.Error())
			c.JSON(http.StatusBadGateway, gin.H{"ok": false, "error": "DATABASE_TEST_FAILED", "message": err.Error()})
			return
		}
	}

	backupDir := resolveServicePath(&next, firstNonEmpty(next.Backup.Dir, cfg.Backup.Dir))
	backupName := "feedback-before-config-" + time.Now().UTC().Format("20060102-150405") + ".sqlite"
	_ = store.BackupDatabase(filepath.Join(backupDir, backupName))
	oldListen := cfg.Listen
	if err := config.Save(&next); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": "CONFIG_SAVE_FAILED", "message": err.Error()})
		return
	}
	*cfg = next
	if body.Database != nil {
		if err := store.ApplyDatabaseConfig(cfg.Database); err != nil {
			_ = audit(c, store, "admin", "config.save.database_degraded", "", err.Error())
		}
	}
	_ = audit(c, store, "admin", "config.save", "", "Config saved")
	c.JSON(http.StatusOK, gin.H{"ok": true, "restartRequired": oldListen != cfg.Listen, "config": buildConfigHealth(cfg, store)})
}

func mergeWebhookSecrets(next, existing []config.WebhookConfig) []config.WebhookConfig {
	for index := range next {
		if strings.TrimSpace(next[index].Secret) != "" {
			continue
		}
		for _, current := range existing {
			if (next[index].Name != "" && current.Name == next[index].Name) || (next[index].URL != "" && current.URL == next[index].URL) {
				next[index].Secret = current.Secret
				break
			}
		}
	}
	return next
}

func buildConfigHealth(cfg *config.Config, store *db.Store) gin.H {
	distDir := filepath.Join(cfg.BaseDir, "web", "dist")
	indexPath := filepath.Join(distDir, "index.html")
	assetsPath := filepath.Join(distDir, "assets")
	serviceBase := displayPath(cfg, cfg.BaseDir)
	storagePath := displayPath(cfg, cfg.StorageDir)
	databasePath := displayPath(cfg, cfg.DatabasePath)
	sqlitePath := displayPath(cfg, cfg.Database.SQLitePath)
	backupDir := displayPath(cfg, cfg.Backup.Dir)
	distPath := displayPath(cfg, distDir)
	storageExists := dirExists(cfg.StorageDir)
	databaseExists := fileExists(cfg.DatabasePath)
	indexExists := fileExists(indexPath)
	assetsExists := dirExists(assetsPath)
	adminPasswordConfigured := strings.TrimSpace(cfg.AdminPasswordHash) != "" || (strings.TrimSpace(cfg.AdminPassword) != "" && cfg.AdminPassword != "CHANGE_ME_ADMIN_PASSWORD")
	mailConfigured := cfg.Mail.Host != "" && cfg.Mail.FromAddress != "" && cfg.Mail.DeveloperAddress != ""
	clientKeyConfigured := strings.TrimSpace(cfg.ClientSignatureKey) != ""
	packageKeyConfigured := strings.TrimSpace(cfg.PackageEncryptionKey) != ""
	backups := listBackups(cfg.Backup.Dir)
	lastBackup := ""
	if len(backups) > 0 {
		lastBackup, _ = backups[0]["name"].(string)
	}

	return gin.H{
		"generatedAt": db.Now(),
		"service": gin.H{
			"listen":                 cfg.Listen,
			"baseDir":                serviceBase,
			"timestampWindowSeconds": cfg.TimestampWindowSeconds,
			"maxRequestBytes":        cfg.MaxRequestBytes,
			"maxPackageBytes":        cfg.MaxPackageBytes,
		},
		"security": gin.H{
			"adminPasswordConfigured":        adminPasswordConfigured,
			"adminPasswordHashConfigured":    strings.TrimSpace(cfg.AdminPasswordHash) != "",
			"clientSignatureKeyConfigured":   clientKeyConfigured,
			"packageEncryptionKeyConfigured": packageKeyConfigured,
			"rateLimit":                      cfg.RateLimit,
		},
		"uploadGuard": cfg.UploadGuard,
		"storage": gin.H{
			"path":   storagePath,
			"bytes":  dirSize(cfg.StorageDir),
			"exists": storageExists,
		},
		"database": gin.H{
			"provider":   cfg.Database.Provider,
			"path":       sqlitePath,
			"bytes":      fileSize(cfg.DatabasePath),
			"exists":     databaseExists,
			"walMode":    store.WALMode(),
			"backupDir":  backupDir,
			"lastBackup": lastBackup,
			"runtime":    store.Status(),
		},
		"mail": gin.H{
			"configured":         mailConfigured,
			"host":               cfg.Mail.Host,
			"port":               cfg.Mail.Port,
			"secure":             cfg.Mail.Secure,
			"fromAddress":        cfg.Mail.FromAddress,
			"developerAddress":   cfg.Mail.DeveloperAddress,
			"usernameConfigured": strings.TrimSpace(cfg.Mail.Username) != "",
			"passwordConfigured": strings.TrimSpace(cfg.Mail.Password) != "" && cfg.Mail.Password != "CHANGE_ME_MAIL_PASSWORD",
			"timeoutSeconds":     cfg.Mail.TimeoutSeconds,
		},
		"webhooks": webhookSummaries(cfg),
		"frontend": gin.H{
			"distPath":     distPath,
			"indexExists":  indexExists,
			"assetsExists": assetsExists,
		},
		"editable": editableConfig(cfg),
		"checks": []gin.H{
			{"key": "storage", "label": "存储目录", "status": healthStatus(storageExists), "detail": storagePath},
			{"key": "database", "label": "SQLite 数据库", "status": healthStatus(databaseExists), "detail": databasePath},
			{"key": "wal", "label": "SQLite WAL", "status": healthStatus(strings.EqualFold(store.WALMode(), "wal")), "detail": store.WALMode()},
			{"key": "frontend", "label": "前端构建", "status": healthStatus(indexExists && assetsExists), "detail": distPath},
			{"key": "mail", "label": "SMTP 通知", "status": healthStatus(mailConfigured), "detail": cfg.Mail.Host},
			{"key": "admin", "label": "后台密码", "status": healthStatus(adminPasswordConfigured), "detail": passwordDetail(adminPasswordConfigured, cfg.AdminPasswordHash != "")},
			{"key": "signature", "label": "客户端签名 key", "status": healthStatus(clientKeyConfigured), "detail": configuredDetail(clientKeyConfigured)},
			{"key": "package", "label": "反馈包加密 key", "status": healthStatus(packageKeyConfigured), "detail": configuredDetail(packageKeyConfigured)},
		},
	}
}

func editableConfig(cfg *config.Config) gin.H {
	webhooks := []gin.H{}
	for _, hook := range cfg.Webhooks {
		webhooks = append(webhooks, gin.H{
			"name":             hook.Name,
			"url":              hook.URL,
			"secret":           "",
			"secretConfigured": strings.TrimSpace(hook.Secret) != "",
			"enabled":          hook.Enabled,
			"events":           hook.Events,
			"timeoutSeconds":   hook.TimeoutSeconds,
			"maxRetries":       hook.MaxRetries,
		})
	}
	return gin.H{
		"listen": cfg.Listen,
		"database": gin.H{
			"provider":               cfg.Database.Provider,
			"sqlitePath":             displayPath(cfg, cfg.Database.SQLitePath),
			"host":                   cfg.Database.Host,
			"port":                   cfg.Database.Port,
			"name":                   cfg.Database.Name,
			"user":                   cfg.Database.User,
			"password":               "",
			"passwordConfigured":     strings.TrimSpace(cfg.Database.Password) != "",
			"dsn":                    maskDSN(cfg.Database.DSN),
			"dsnConfigured":          strings.TrimSpace(cfg.Database.DSN) != "",
			"sslMode":                cfg.Database.SSLMode,
			"maxOpenConns":           cfg.Database.MaxOpenConns,
			"maxIdleConns":           cfg.Database.MaxIdleConns,
			"connMaxLifetimeSeconds": cfg.Database.ConnMaxLifetimeSeconds,
			"failoverEnabled":        cfg.Database.FailoverEnabled,
			"healthIntervalSeconds":  cfg.Database.HealthIntervalSeconds,
			"sync":                   cfg.Database.Sync,
		},
		"mail": gin.H{
			"host":               cfg.Mail.Host,
			"port":               cfg.Mail.Port,
			"secure":             cfg.Mail.Secure,
			"username":           cfg.Mail.Username,
			"password":           "",
			"passwordConfigured": strings.TrimSpace(cfg.Mail.Password) != "" && cfg.Mail.Password != "CHANGE_ME_MAIL_PASSWORD",
			"fromAddress":        cfg.Mail.FromAddress,
			"fromName":           cfg.Mail.FromName,
			"developerAddress":   cfg.Mail.DeveloperAddress,
			"timeoutSeconds":     cfg.Mail.TimeoutSeconds,
		},
		"backup":      gin.H{"dir": displayPath(cfg, cfg.Backup.Dir)},
		"rateLimit":   cfg.RateLimit,
		"uploadGuard": cfg.UploadGuard,
		"webhooks":    webhooks,
		"security": gin.H{
			"adminPasswordHashConfigured":    strings.TrimSpace(cfg.AdminPasswordHash) != "",
			"adminPasswordConfigured":        strings.TrimSpace(cfg.AdminPassword) != "" && cfg.AdminPassword != "CHANGE_ME_ADMIN_PASSWORD",
			"clientSignatureKeyConfigured":   strings.TrimSpace(cfg.ClientSignatureKey) != "",
			"packageEncryptionKeyConfigured": strings.TrimSpace(cfg.PackageEncryptionKey) != "",
		},
	}
}

func displayPath(cfg *config.Config, value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return value
	}
	if cfg == nil || strings.TrimSpace(cfg.BaseDir) == "" {
		return filepath.ToSlash(value)
	}
	absBase, err := filepath.Abs(cfg.BaseDir)
	if err != nil {
		return filepath.ToSlash(value)
	}
	absValue, err := filepath.Abs(value)
	if err != nil {
		return filepath.ToSlash(value)
	}
	rel, err := filepath.Rel(absBase, absValue)
	if err != nil {
		return filepath.ToSlash(value)
	}
	if rel == "." {
		return "."
	}
	if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) {
		return filepath.ToSlash(value)
	}
	return filepath.ToSlash(rel)
}

func resolveServicePath(cfg *config.Config, value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return value
	}
	if filepath.IsAbs(value) {
		return filepath.Clean(value)
	}
	if cfg == nil || strings.TrimSpace(cfg.BaseDir) == "" {
		return filepath.Clean(value)
	}
	return filepath.Clean(filepath.Join(cfg.BaseDir, value))
}

func maskDSN(value string) string {
	if strings.TrimSpace(value) == "" {
		return ""
	}
	parsed, err := url.Parse(value)
	if err != nil || parsed.User == nil {
		return ""
	}
	if _, ok := parsed.User.Password(); ok {
		parsed.User = url.UserPassword(parsed.User.Username(), "***")
	}
	return parsed.String()
}

func webhookSummaries(cfg *config.Config) []gin.H {
	items := []gin.H{}
	for _, hook := range cfg.Webhooks {
		host := ""
		if parsed, err := url.Parse(hook.URL); err == nil {
			host = parsed.Host
		}
		items = append(items, gin.H{
			"name":             hook.Name,
			"host":             host,
			"enabled":          hook.Enabled,
			"events":           hook.Events,
			"secretConfigured": strings.TrimSpace(hook.Secret) != "",
			"timeoutSeconds":   hook.TimeoutSeconds,
			"maxRetries":       hook.MaxRetries,
		})
	}
	return items
}

func healthStatus(ok bool) string {
	if ok {
		return "ok"
	}
	return "missing"
}

func configuredDetail(ok bool) string {
	if ok {
		return "已配置"
	}
	return "未配置"
}

func passwordDetail(ok, hashed bool) string {
	if !ok {
		return "未配置"
	}
	if hashed {
		return "已使用哈希密码"
	}
	return "已配置明文密码"
}

func listBackups(dir string) []gin.H {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return []gin.H{}
	}
	items := []gin.H{}
	for _, entry := range entries {
		if entry.IsDir() || !safeBackupName(entry.Name()) {
			continue
		}
		path := filepath.Join(dir, entry.Name())
		items = append(items, backupInfo(path))
	}
	sort.Slice(items, func(i, j int) bool {
		left, _ := items[i]["createdAt"].(string)
		right, _ := items[j]["createdAt"].(string)
		return left > right
	})
	return items
}

func backupInfo(path string) gin.H {
	info, err := os.Stat(path)
	createdAt := ""
	size := int64(0)
	if err == nil {
		createdAt = info.ModTime().UTC().Format(time.RFC3339)
		size = info.Size()
	}
	return gin.H{"name": filepath.Base(path), "bytes": size, "createdAt": createdAt}
}

func safeBackupName(name string) bool {
	return regexp.MustCompile(`^feedback-[0-9]{8}-[0-9]{6}\.sqlite$`).MatchString(name)
}

func audit(c *gin.Context, store *db.Store, actor, typ, target, message string) error {
	return store.InsertAudit(db.AuditLog{
		Actor:     firstNonEmpty(actor, "admin"),
		Type:      typ,
		Target:    target,
		Message:   message,
		IP:        c.ClientIP(),
		UserAgent: c.Request.UserAgent(),
	})
}

func fileExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && !info.IsDir()
}

func dirExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.IsDir()
}

func fileSize(path string) int64 {
	info, err := os.Stat(path)
	if err != nil || info.IsDir() {
		return 0
	}
	return info.Size()
}

func dirSize(path string) int64 {
	var total int64
	_ = filepath.WalkDir(path, func(_ string, entry os.DirEntry, err error) error {
		if err != nil || entry.IsDir() {
			return nil
		}
		if info, err := entry.Info(); err == nil {
			total += info.Size()
		}
		return nil
	})
	return total
}

func queryInt(c *gin.Context, key string, fallback int) int {
	value, err := strconv.Atoi(c.Query(key))
	if err != nil {
		return fallback
	}
	return value
}

func allowedStatus(value string) bool {
	switch value {
	case "", "new", "triaged", "investigating", "resolved", "archived":
		return true
	default:
		return false
	}
}

func allowedCategory(value string) bool {
	switch value {
	case "", "issue", "suggestion", "ui", "other":
		return true
	default:
		return false
	}
}

func allowedPriority(value string) bool {
	switch value {
	case "", "normal", "major", "blocking":
		return true
	default:
		return false
	}
}

func allowedSLA(value string) bool {
	switch value {
	case "", "standard", "elevated", "urgent":
		return true
	default:
		return false
	}
}

func validOptionalTime(value string) bool {
	if value == "" {
		return true
	}
	_, err := time.Parse(time.RFC3339, value)
	return err == nil
}

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

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

func isInside(root, path string) bool {
	rootAbs, err := filepath.Abs(root)
	if err != nil {
		return false
	}
	pathAbs, err := filepath.Abs(path)
	if err != nil {
		return false
	}
	rel, err := filepath.Rel(rootAbs, pathAbs)
	if err != nil {
		return false
	}
	return rel == "." || (!strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel))
}
