package handlers

import (
	"archive/zip"
	"fmt"
	"io"
	"math/rand"
	"mime/multipart"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	internalDb "server/internal/db"
	"server/internal/models"
	internalWs "server/internal/websocket"

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"
)

const defaultExamAllowedApps = "chrome,msedge,edge,acrobat,acrord32,foxit,wails,simple_care,client"

func examUploadRoot() string {
	if v := os.Getenv("UPLOAD_DIR"); v != "" {
		return v
	}
	return "uploads"
}

func examRoomDir(roomID uint) string {
	return filepath.Join(examUploadRoot(), "exams", fmt.Sprintf("%d", roomID))
}

func validateExamRoomForPublish(db *gorm.DB, room *models.ExamRoom) []string {
	var issues []string
	now := time.Now()
	if strings.TrimSpace(room.Name) == "" {
		issues = append(issues, "Thiếu tên phòng thi")
	}
	if !room.EndTime.After(room.StartTime) {
		issues = append(issues, "Giờ kết thúc phải sau giờ bắt đầu")
	}
	if !room.EndTime.After(now) {
		issues = append(issues, "Giờ kết thúc phải ở tương lai")
	}
	if strings.TrimSpace(room.AllowedApps) == "" {
		issues = append(issues, "Chưa cấu hình ứng dụng được phép khi thi")
	}
	var paperCount int64
	_ = db.Model(&models.ExamPaper{}).Where("exam_room_id = ?", room.ID).Count(&paperCount).Error
	if paperCount == 0 {
		issues = append(issues, "Chưa có gói đề (PDF + tài nguyên)")
	}
	var students []models.ExamRoomStudent
	_ = db.Where("exam_room_id = ?", room.ID).Find(&students).Error
	if len(students) == 0 {
		issues = append(issues, "Chưa có sinh viên trong phòng thi")
	}
	unassigned := 0
	for _, s := range students {
		if s.AssignedPaperID == nil {
			unassigned++
		}
	}
	if unassigned > 0 {
		issues = append(issues, fmt.Sprintf("Còn %d sinh viên chưa được chia gói đề", unassigned))
	}
	return issues
}

// ProcessScheduledExamSends gửi đề đã hẹn giờ — gọi định kỳ từ main.
func ProcessScheduledExamSends(db *gorm.DB) {
	internalDb.ProcessExamRoomLifecycle(db)
	now := time.Now()
	var rows []models.ExamRoomStudent
	_ = db.Table("exam_room_students AS ers").
		Select("ers.*").
		Joins("JOIN exam_rooms er ON er.id = ers.exam_room_id").
		Where("ers.paper_scheduled_at IS NOT NULL AND ers.paper_scheduled_at <= ? AND ers.paper_sent_at IS NULL", now).
		Where("er.status = ?", models.ExamStatusReady).
		Find(&rows).Error
	for _, row := range rows {
		deliverExamPaper(db, row.ID)
	}
}

func deliverExamPaper(db *gorm.DB, enrollmentID uint) {
	var en models.ExamRoomStudent
	if err := db.First(&en, enrollmentID).Error; err != nil {
		return
	}
	if en.AssignedPaperID == nil {
		return
	}
	now := time.Now()
	en.PaperSentAt = &now
	en.PaperScheduledAt = nil
	_ = db.Save(&en).Error

	var room models.ExamRoom
	var paper models.ExamPaper
	if err := db.First(&room, en.ExamRoomID).Error; err != nil {
		return
	}
	if room.Status != models.ExamStatusReady {
		return
	}
	_ = db.First(&paper, *en.AssignedPaperID).Error

	internalWs.Hub.PushExamToStudent(en.StudentRkID, map[string]any{
		"examRoomId":  room.ID,
		"examName":    room.Name,
		"paperId":     paper.ID,
		"paperTitle":  paper.Title,
		"paperSentAt": now.Format(time.RFC3339),
	})
}

// GET /api/exam-rooms
func ListExamRoomsHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		var rooms []models.ExamRoom
		if err := db.Order("start_time desc").Find(&rooms).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		type row struct {
			models.ExamRoom
			StudentCount  int    `json:"studentCount"`
			PaperCount    int    `json:"paperCount"`
			DisplayStatus string `json:"displayStatus"`
		}
		out := make([]row, 0, len(rooms))
		now := time.Now()
		for _, r := range rooms {
			if r.Status == "" {
				r.Status = models.ExamStatusDraft
			}
			var sc, pc int64
			_ = db.Model(&models.ExamRoomStudent{}).Where("exam_room_id = ?", r.ID).Count(&sc).Error
			_ = db.Model(&models.ExamPaper{}).Where("exam_room_id = ?", r.ID).Count(&pc).Error
			out = append(out, row{
				ExamRoom:      r,
				StudentCount:  int(sc),
				PaperCount:    int(pc),
				DisplayStatus: internalDb.ExamDisplayStatus(r, now),
			})
		}
		return c.JSON(fiber.Map{"data": out})
	}
}

// POST /api/exam-rooms
func CreateExamRoomHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		var req struct {
			Name        string `json:"name"`
			StartTime   string `json:"startTime"`
			EndTime     string `json:"endTime"`
			AllowedApps string `json:"allowedApps"`
			QuizURL     string `json:"quizUrl"`
		}
		if err := c.BodyParser(&req); err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
		}
		req.Name = strings.TrimSpace(req.Name)
		if req.Name == "" {
			return c.Status(400).JSON(fiber.Map{"error": "Tên phòng thi bắt buộc"})
		}
		start, err1 := time.Parse(time.RFC3339, req.StartTime)
		end, err2 := time.Parse(time.RFC3339, req.EndTime)
		if err1 != nil || err2 != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Thời gian không hợp lệ (RFC3339)"})
		}
		if !end.After(start) {
			return c.Status(400).JSON(fiber.Map{"error": "Giờ kết thúc phải sau giờ bắt đầu"})
		}
		apps := strings.TrimSpace(req.AllowedApps)
		if apps == "" {
			apps = defaultExamAllowedApps
		}
		room := models.ExamRoom{
			Name:        req.Name,
			StartTime:   start,
			EndTime:     end,
			AllowedApps: apps,
			QuizURL:     strings.TrimSpace(req.QuizURL),
			Status:      models.ExamStatusDraft,
		}
		if err := db.Create(&room).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		_ = os.MkdirAll(examRoomDir(room.ID), 0755)
		return c.Status(201).JSON(room)
	}
}

// GET /api/exam-rooms/:id
func GetExamRoomHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		id, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, id).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy phòng thi"})
		}
		var papers []models.ExamPaper
		_ = db.Where("exam_room_id = ?", id).Order("sort_order asc, id asc").Find(&papers).Error
		type paperRow struct {
			models.ExamPaper
			Resources []models.ExamPaperResource `json:"resources"`
		}
		paperRows := make([]paperRow, 0, len(papers))
		for _, p := range papers {
			var res []models.ExamPaperResource
			_ = db.Where("exam_paper_id = ?", p.ID).Find(&res).Error
			paperRows = append(paperRows, paperRow{ExamPaper: p, Resources: res})
		}
		var students []models.ExamRoomStudent
		_ = db.Where("exam_room_id = ?", id).Find(&students).Error
		type studentRow struct {
			models.ExamRoomStudent
			FullName    string `json:"fullName"`
			StudentCode string `json:"studentCode"`
			Email       string `json:"email"`
			Avatar      string `json:"avatar,omitempty"`
			PaperTitle  string `json:"paperTitle,omitempty"`
			Submitted   bool   `json:"submitted"`
		}
		stRows := make([]studentRow, 0, len(students))
		for _, s := range students {
			row := studentRow{ExamRoomStudent: s}
			var st models.Student
			if err := db.Where("rk_id = ?", s.StudentRkID).First(&st).Error; err == nil {
				row.FullName = st.FullName
				row.StudentCode = st.StudentCode
				row.Email = st.Email
				if st.Avatar != nil {
					row.Avatar = *st.Avatar
				}
			}
			if s.AssignedPaperID != nil {
				var p models.ExamPaper
				if err := db.First(&p, *s.AssignedPaperID).Error; err == nil {
					row.PaperTitle = p.Title
				}
			}
			var subCount int64
			_ = db.Model(&models.ExamSubmission{}).Where("exam_room_id = ? AND student_rk_id = ?", id, s.StudentRkID).Count(&subCount).Error
			row.Submitted = subCount > 0
			stRows = append(stRows, row)
		}
		st := internalDb.ExamDisplayStatus(room, time.Now())
		now := time.Now()
		return c.JSON(fiber.Map{
			"room":          room,
			"papers":        paperRows,
			"students":      stRows,
			"displayStatus": st,
			"editable":      internalDb.ExamRoomEditable(room),
			"prepEditable":  internalDb.ExamRoomPrepEditable(room, now),
			"canPublish":    room.Status == models.ExamStatusDraft,
			"canUnpublish":  internalDb.ExamRoomCanUnpublish(room, time.Now()),
			"canCancel":     internalDb.ExamRoomCanCancel(room, time.Now()),
		})
	}
}

// GET /api/exam-rooms/:id/online-students
func GetExamRoomOnlineStudentsHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		id, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var students []models.ExamRoomStudent
		if err := db.Where("exam_room_id = ?", id).Find(&students).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		online := make([]int64, 0, len(students))
		for _, s := range students {
			if internalWs.Hub.IsStudentOnline(s.StudentRkID) {
				online = append(online, s.StudentRkID)
			}
		}
		return c.JSON(fiber.Map{"onlineStudentIds": online})
	}
}

// PATCH /api/exam-rooms/:id
func UpdateExamRoomHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		id, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, id).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		if !internalDb.ExamRoomEditable(room) {
			return c.Status(403).JSON(fiber.Map{"error": "Chỉ sửa được phòng thi ở trạng thái tạm thời"})
		}
		var req struct {
			Name        *string `json:"name"`
			StartTime   *string `json:"startTime"`
			EndTime     *string `json:"endTime"`
			AllowedApps *string `json:"allowedApps"`
			QuizURL     *string `json:"quizUrl"`
		}
		if err := c.BodyParser(&req); err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
		}
		if req.Name != nil {
			room.Name = strings.TrimSpace(*req.Name)
		}
		if req.StartTime != nil {
			t, err := time.Parse(time.RFC3339, *req.StartTime)
			if err != nil {
				return c.Status(400).JSON(fiber.Map{"error": "startTime không hợp lệ"})
			}
			room.StartTime = t
		}
		if req.EndTime != nil {
			t, err := time.Parse(time.RFC3339, *req.EndTime)
			if err != nil {
				return c.Status(400).JSON(fiber.Map{"error": "endTime không hợp lệ"})
			}
			room.EndTime = t
		}
		if !room.EndTime.After(room.StartTime) {
			return c.Status(400).JSON(fiber.Map{"error": "Giờ kết thúc phải sau giờ bắt đầu"})
		}
		if req.AllowedApps != nil {
			room.AllowedApps = strings.TrimSpace(*req.AllowedApps)
		}
		if req.QuizURL != nil {
			room.QuizURL = strings.TrimSpace(*req.QuizURL)
		}
		if err := db.Save(&room).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		return c.JSON(room)
	}
}

// POST /api/exam-rooms/:id/publish — đẩy phòng thi (validate → ready)
func PublishExamRoomHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		id, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, id).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		if room.Status != models.ExamStatusDraft {
			return c.Status(403).JSON(fiber.Map{"error": "Chỉ đẩy được phòng thi ở trạng thái tạm thời"})
		}
		if issues := validateExamRoomForPublish(db, &room); len(issues) > 0 {
			return c.Status(400).JSON(fiber.Map{"error": "Chưa đủ điều kiện", "issues": issues})
		}
		room.Status = models.ExamStatusReady
		if err := db.Save(&room).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		return c.JSON(fiber.Map{"ok": true, "room": room, "displayStatus": internalDb.ExamDisplayStatus(room, time.Now())})
	}
}

// POST /api/exam-rooms/:id/unpublish — thu hồi lệnh đẩy (ready → draft, trước giờ thi)
func UnpublishExamRoomHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		id, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, id).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		if !internalDb.ExamRoomCanUnpublish(room, time.Now()) {
			return c.Status(403).JSON(fiber.Map{"error": "Không thể thu hồi — phòng đã vào giờ thi hoặc không ở trạng thái sẵn sàng"})
		}
		room.Status = models.ExamStatusDraft
		if err := db.Save(&room).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		return c.JSON(fiber.Map{"ok": true, "room": room, "displayStatus": models.ExamStatusDraft})
	}
}

// POST /api/exam-rooms/:id/cancel — hủy phòng thi sớm (đang diễn ra)
func CancelExamRoomHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		id, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, id).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		now := time.Now()
		if !internalDb.ExamRoomCanCancel(room, now) {
			return c.Status(403).JSON(fiber.Map{"error": "Chỉ hủy được phòng thi đang diễn ra"})
		}
		room.Status = models.ExamStatusCancelled
		room.EndTime = now
		if err := db.Save(&room).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		return c.JSON(fiber.Map{"ok": true, "room": room, "displayStatus": models.ExamStatusCancelled})
	}
}

// DELETE /api/exam-rooms/:id
func DeleteExamRoomHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		id, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, id).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		if internalDb.ExamRoomIsLive(room, time.Now()) {
			return c.Status(403).JSON(fiber.Map{"error": "Không thể xóa phòng thi đang diễn ra — hãy hủy phòng trước"})
		}
		if room.Status == models.ExamStatusReady {
			return c.Status(403).JSON(fiber.Map{"error": "Phòng đã đẩy — thu hồi lệnh đẩy trước khi xóa"})
		}
		_ = os.RemoveAll(examRoomDir(id))
		_ = db.Where("exam_room_id = ?", id).Delete(&models.ExamSubmission{}).Error
		_ = db.Where("exam_room_id = ?", id).Delete(&models.ExamRoomStudent{}).Error
		var papers []models.ExamPaper
		_ = db.Where("exam_room_id = ?", id).Find(&papers).Error
		for _, p := range papers {
			_ = db.Where("exam_paper_id = ?", p.ID).Delete(&models.ExamPaperResource{}).Error
		}
		_ = db.Where("exam_room_id = ?", id).Delete(&models.ExamPaper{}).Error
		_ = db.Delete(&room).Error
		return c.JSON(fiber.Map{"ok": true})
	}
}

// GET /api/exam-rooms/search-students?q=
func SearchExamStudentsHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		q := strings.TrimSpace(c.Query("q"))
		if q == "" {
			return c.JSON(fiber.Map{"data": []any{}})
		}
		pat := "%" + q + "%"
		var ids []int64
		_ = db.Model(&models.Student{}).
			Where("full_name LIKE ? OR student_code LIKE ? OR email LIKE ?", pat, pat, pat).
			Limit(30).
			Pluck("rk_id", &ids).Error
		var classIDs []int64
		_ = db.Model(&models.Class{}).Where("class_code LIKE ? OR name LIKE ?", pat, pat).Pluck("rk_id", &classIDs).Error
		if len(classIDs) > 0 {
			var more []int64
			_ = db.Model(&models.ClassStudent{}).Where("class_rk_id IN ?", classIDs).Distinct().Pluck("student_rk_id", &more).Error
			ids = append(ids, more...)
		}
		seen := map[int64]bool{}
		uniq := make([]int64, 0)
		for _, id := range ids {
			if id > 0 && !seen[id] {
				seen[id] = true
				uniq = append(uniq, id)
			}
		}
		if len(uniq) > 40 {
			uniq = uniq[:40]
		}
		var students []models.Student
		if len(uniq) > 0 {
			_ = db.Where("rk_id IN ?", uniq).Limit(40).Find(&students).Error
		}
		type row struct {
			StudentRkID int64  `json:"studentRkId"`
			FullName    string `json:"fullName"`
			StudentCode string `json:"studentCode"`
			Email       string `json:"email"`
			ClassCodes  string `json:"classCodes"`
		}
		out := make([]row, 0, len(students))
		for _, st := range students {
			var codes []string
			_ = db.Table("class_students cs").
				Select("c.class_code").
				Joins("JOIN classes c ON c.rk_id = cs.class_rk_id").
				Where("cs.student_rk_id = ?", st.RkID).
				Pluck("class_code", &codes).Error
			out = append(out, row{
				StudentRkID: st.RkID,
				FullName:    st.FullName,
				StudentCode: st.StudentCode,
				Email:       st.Email,
				ClassCodes:  strings.Join(codes, ", "),
			})
		}
		return c.JSON(fiber.Map{"data": out})
	}
}

// POST /api/exam-rooms/:id/students
func AddExamRoomStudentsHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		roomID, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, roomID).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		if !internalDb.ExamRoomPrepEditable(room, time.Now()) {
			return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã bắt đầu — không thể thêm sinh viên"})
		}
		var req struct {
			StudentRkIDs []int64 `json:"studentRkIds"`
		}
		if err := c.BodyParser(&req); err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
		}
		added := 0
		for _, sid := range req.StudentRkIDs {
			if sid <= 0 {
				continue
			}
			var cnt int64
			_ = db.Model(&models.ExamRoomStudent{}).Where("exam_room_id = ? AND student_rk_id = ?", roomID, sid).Count(&cnt).Error
			if cnt > 0 {
				continue
			}
			_ = db.Create(&models.ExamRoomStudent{ExamRoomID: roomID, StudentRkID: sid}).Error
			added++
		}
		return c.JSON(fiber.Map{"added": added})
	}
}

// DELETE /api/exam-rooms/:id/students/:studentRkId
func RemoveExamRoomStudentHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		roomID, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		studentRkID, err := strconv.ParseInt(c.Params("studentRkId"), 10, 64)
		if err != nil || studentRkID <= 0 {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid studentRkId"})
		}
		var room models.ExamRoom
		if err := db.First(&room, roomID).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		if !internalDb.ExamRoomPrepEditable(room, time.Now()) {
			return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã bắt đầu — không thể xóa sinh viên"})
		}
		_ = db.Where("exam_room_id = ? AND student_rk_id = ?", roomID, studentRkID).Delete(&models.ExamRoomStudent{}).Error
		return c.JSON(fiber.Map{"ok": true})
	}
}

// POST /api/exam-rooms/:id/papers — multipart: title, pdf, resources[] (gói đề)
func UploadExamPaperHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		roomID, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, roomID).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		now := time.Now()
		if !internalDb.ExamRoomPrepEditable(room, now) {
			return c.Status(403).JSON(fiber.Map{"error": "Chỉ thêm gói đề khi phòng tạm thời hoặc sẵn sàng (trước giờ thi)"})
		}
		form, err := c.MultipartForm()
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Không đọc được dữ liệu upload — kiểm tra file PDF"})
		}
		var pdfFiles []*multipart.FileHeader
		if form != nil {
			pdfFiles = form.File["pdf"]
		}
		if len(pdfFiles) == 0 {
			return c.Status(400).JSON(fiber.Map{"error": "Thiếu file PDF trong gói đề (field: pdf)"})
		}
		file := pdfFiles[0]
		title := strings.TrimSpace(c.FormValue("title"))
		if title == "" {
			title = strings.TrimSuffix(filepath.Base(file.Filename), filepath.Ext(file.Filename))
		}
		if title == "" {
			title = "Gói đề"
		}
		if !strings.HasSuffix(strings.ToLower(file.Filename), ".pdf") {
			return c.Status(400).JSON(fiber.Map{"error": "File đề chính phải là PDF"})
		}
		var cnt int64
		_ = db.Model(&models.ExamPaper{}).Where("exam_room_id = ?", roomID).Count(&cnt).Error
		paper := models.ExamPaper{ExamRoomID: roomID, Title: title, SortOrder: int(cnt)}
		if err := db.Create(&paper).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		dir := filepath.Join(examRoomDir(roomID), "papers", fmt.Sprintf("%d", paper.ID))
		if err := os.MkdirAll(dir, 0755); err != nil {
			_ = db.Delete(&paper).Error
			return c.Status(500).JSON(fiber.Map{"error": "Không tạo được thư mục lưu file"})
		}
		dest := filepath.Join(dir, "main.pdf")
		if err := c.SaveFile(file, dest); err != nil {
			_ = os.RemoveAll(dir)
			_ = db.Delete(&paper).Error
			return c.Status(500).JSON(fiber.Map{"error": "Lưu file PDF thất bại"})
		}
		paper.PdfPath = dest
		if err := db.Save(&paper).Error; err != nil {
			_ = os.RemoveAll(dir)
			_ = db.Delete(&paper).Error
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}

		var resourceHeaders []*multipart.FileHeader
		if form != nil {
			resourceHeaders = form.File["resources"]
		}
		savedResources := saveExamPaperResources(db, c, roomID, paper.ID, resourceHeaders)
		type pkgResp struct {
			models.ExamPaper
			Resources []models.ExamPaperResource `json:"resources"`
		}
		return c.Status(201).JSON(pkgResp{ExamPaper: paper, Resources: savedResources})
	}
}

func saveExamPaperResources(db *gorm.DB, c *fiber.Ctx, roomID, paperID uint, onlyFiles []*multipart.FileHeader) []models.ExamPaperResource {
	headers := onlyFiles
	if len(headers) == 0 {
		return nil
	}
	if len(headers) == 0 {
		return nil
	}
	dir := filepath.Join(examRoomDir(roomID), "papers", fmt.Sprintf("%d", paperID), "resources")
	_ = os.MkdirAll(dir, 0755)
	out := make([]models.ExamPaperResource, 0, len(headers))
	for _, file := range headers {
		safeName := filepath.Base(file.Filename)
		if safeName == "" || safeName == "." {
			continue
		}
		dest := filepath.Join(dir, safeName)
		if err := c.SaveFile(file, dest); err != nil {
			continue
		}
		res := models.ExamPaperResource{ExamPaperID: paperID, FilePath: dest, FileName: safeName}
		if err := db.Create(&res).Error; err != nil {
			continue
		}
		out = append(out, res)
	}
	return out
}

// POST /api/exam-rooms/:id/papers/:paperId/resources — thêm tài nguyên vào gói đề
func UploadExamPaperResourceHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		roomID, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		paperID, err := parseUintParam(c, "paperId")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid paperId"})
		}
		var room models.ExamRoom
		if err := db.First(&room, roomID).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy phòng"})
		}
		if !internalDb.ExamRoomPrepEditable(room, time.Now()) {
			return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã bắt đầu — không thể thêm tài nguyên"})
		}
		var paper models.ExamPaper
		if err := db.Where("id = ? AND exam_room_id = ?", paperID, roomID).First(&paper).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy gói đề"})
		}
		form, _ := c.MultipartForm()
		var files []*multipart.FileHeader
		if form != nil {
			if batch, ok := form.File["resources"]; ok && len(batch) > 0 {
				files = batch
			} else if batch, ok := form.File["file"]; ok && len(batch) > 0 {
				files = batch
			}
		}
		if len(files) == 0 {
			if file, err := c.FormFile("file"); err == nil {
				files = []*multipart.FileHeader{file}
			}
		}
		if len(files) == 0 {
			return c.Status(400).JSON(fiber.Map{"error": "Thiếu file tài nguyên"})
		}
		saved := saveExamPaperResources(db, c, roomID, paperID, files)
		if len(saved) == 0 {
			return c.Status(500).JSON(fiber.Map{"error": "Lưu tài nguyên thất bại"})
		}
		if len(saved) == 1 {
			return c.Status(201).JSON(saved[0])
		}
		return c.Status(201).JSON(fiber.Map{"data": saved})
	}
}

// DELETE /api/exam-rooms/:id/papers/:paperId
func DeleteExamPaperHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		roomID, err := parseUintParam(c, "id")
		paperID, err2 := parseUintParam(c, "paperId")
		if err != nil || err2 != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, roomID).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		if !internalDb.ExamRoomPrepEditable(room, time.Now()) {
			return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã bắt đầu — không thể xóa gói đề"})
		}
		var assigned int64
		_ = db.Model(&models.ExamRoomStudent{}).Where("exam_room_id = ? AND assigned_paper_id = ?", roomID, paperID).Count(&assigned).Error
		if assigned > 0 {
			return c.Status(403).JSON(fiber.Map{"error": "Gói đề đã được gán cho sinh viên — không thể xóa"})
		}
		_ = os.RemoveAll(filepath.Join(examRoomDir(roomID), "papers", fmt.Sprintf("%d", paperID)))
		_ = db.Where("exam_paper_id = ?", paperID).Delete(&models.ExamPaperResource{}).Error
		_ = db.Where("id = ? AND exam_room_id = ?", paperID, roomID).Delete(&models.ExamPaper{}).Error
		return c.JSON(fiber.Map{"ok": true})
	}
}

// POST /api/exam-rooms/:id/assign-random
func RandomAssignExamPapersHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		roomID, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, roomID).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		if !internalDb.ExamRoomPrepEditable(room, time.Now()) {
			return c.Status(403).JSON(fiber.Map{"error": "Chỉ chia gói đề khi phòng tạm thời hoặc sẵn sàng (trước giờ thi)"})
		}
		var papers []models.ExamPaper
		if err := db.Where("exam_room_id = ?", roomID).Order("sort_order asc, id asc").Find(&papers).Error; err != nil || len(papers) == 0 {
			return c.Status(400).JSON(fiber.Map{"error": "Chưa có gói đề để chia"})
		}
		var students []models.ExamRoomStudent
		if err := db.Where("exam_room_id = ?", roomID).Find(&students).Error; err != nil || len(students) == 0 {
			return c.Status(400).JSON(fiber.Map{"error": "Chưa có sinh viên"})
		}
		rand.Shuffle(len(students), func(i, j int) { students[i], students[j] = students[j], students[i] })
		for i := range students {
			p := papers[i%len(papers)]
			pid := p.ID
			students[i].AssignedPaperID = &pid
			_ = db.Save(&students[i]).Error
		}
		return c.JSON(fiber.Map{"assigned": len(students), "packages": len(papers)})
	}
}

// POST /api/exam-rooms/:id/send-papers
func SendExamPapersHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		roomID, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var room models.ExamRoom
		if err := db.First(&room, roomID).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy phòng"})
		}
		now := time.Now()
		ds := internalDb.ExamDisplayStatus(room, now)
		if ds != models.ExamStatusReady && ds != "active" {
			return c.Status(403).JSON(fiber.Map{"error": "Chỉ gửi đề khi phòng đã đẩy và trong/ trước khung giờ thi"})
		}
		var req struct {
			ScheduledAt   *string `json:"scheduledAt"`
			StudentRkIDs  []int64 `json:"studentRkIds"`
		}
		if err := c.BodyParser(&req); err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
		}
		var scheduled *time.Time
		if req.ScheduledAt != nil && strings.TrimSpace(*req.ScheduledAt) != "" {
			t, err := time.Parse(time.RFC3339, strings.TrimSpace(*req.ScheduledAt))
			if err != nil {
				return c.Status(400).JSON(fiber.Map{"error": "scheduledAt không hợp lệ"})
			}
			scheduled = &t
		}
		q := db.Where("exam_room_id = ?", roomID)
		if len(req.StudentRkIDs) > 0 {
			q = q.Where("student_rk_id IN ?", req.StudentRkIDs)
		}
		var rows []models.ExamRoomStudent
		if err := q.Find(&rows).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		sent, scheduledCount := 0, 0
		for _, row := range rows {
			if row.AssignedPaperID == nil {
				continue
			}
			if scheduled != nil && scheduled.After(time.Now()) {
				row.PaperScheduledAt = scheduled
				_ = db.Save(&row).Error
				scheduledCount++
				continue
			}
			deliverExamPaper(db, row.ID)
			sent++
		}
		return c.JSON(fiber.Map{"sent": sent, "scheduled": scheduledCount})
	}
}

// GET /api/exam-rooms/:id/submissions
func ListExamSubmissionsHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		roomID, err := parseUintParam(c, "id")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
		}
		var subs []models.ExamSubmission
		_ = db.Where("exam_room_id = ?", roomID).Order("created_at desc").Find(&subs).Error
		type row struct {
			models.ExamSubmission
			FullName    string `json:"fullName"`
			StudentCode string `json:"studentCode"`
		}
		out := make([]row, 0, len(subs))
		for _, s := range subs {
			r := row{ExamSubmission: s}
			var st models.Student
			if err := db.Where("rk_id = ?", s.StudentRkID).First(&st).Error; err == nil {
				r.FullName = st.FullName
				r.StudentCode = st.StudentCode
			}
			out = append(out, r)
		}
		return c.JSON(fiber.Map{"data": out})
	}
}

// GET /api/student/exam
func GetStudentExamHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		studentID, err := strconv.ParseInt(c.Query("studentRkId", "0"), 10, 64)
		if err != nil || studentID <= 0 {
			return c.Status(400).JSON(fiber.Map{"error": "studentRkId required"})
		}
		info := internalDb.FindActiveExamForStudent(db, studentID)
		if info == nil {
			return c.JSON(fiber.Map{"active": false})
		}
		paperSent := info.Enrollment.PaperSentAt != nil
		var subCount int64
		_ = db.Model(&models.ExamSubmission{}).Where("exam_room_id = ? AND student_rk_id = ?", info.Room.ID, studentID).Count(&subCount).Error
		resp := fiber.Map{
			"active":      true,
			"examRoomId":  info.Room.ID,
			"examName":    info.Room.Name,
			"startTime":   info.Room.StartTime,
			"endTime":     info.Room.EndTime,
			"quizUrl":     info.Room.QuizURL,
			"paperSent":   paperSent,
			"submitted":   subCount > 0,
		}
		if paperSent && info.Paper != nil {
			resp["paperId"] = info.Paper.ID
			resp["paperTitle"] = info.Paper.Title
		}
		return c.JSON(resp)
	}
}

// GET /api/student/exam/paper-files?studentRkId=
func GetStudentExamPaperFilesHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		studentID, _ := strconv.ParseInt(c.Query("studentRkId", "0"), 10, 64)
		if studentID <= 0 {
			return c.Status(400).JSON(fiber.Map{"error": "studentRkId required"})
		}
		info := internalDb.FindActiveExamForStudent(db, studentID)
		if info == nil || info.Enrollment.PaperSentAt == nil || info.Paper == nil {
			return c.Status(403).JSON(fiber.Map{"error": "Chưa có đề được gửi"})
		}
		var resources []models.ExamPaperResource
		_ = db.Where("exam_paper_id = ?", info.Paper.ID).Find(&resources).Error
		resList := make([]fiber.Map, 0, len(resources))
		for _, r := range resources {
			resList = append(resList, fiber.Map{
				"id":       r.ID,
				"fileName": r.FileName,
				"url":      fmt.Sprintf("/api/student/exam/download?studentRkId=%d&kind=resource&fileId=%d", studentID, r.ID),
			})
		}
		return c.JSON(fiber.Map{
			"paperTitle": info.Paper.Title,
			"pdfUrl":     fmt.Sprintf("/api/student/exam/download?studentRkId=%d&kind=pdf", studentID),
			"resources":  resList,
		})
	}
}

// GET /api/student/exam/download
func DownloadStudentExamFileHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		studentID, _ := strconv.ParseInt(c.Query("studentRkId", "0"), 10, 64)
		if studentID <= 0 {
			return c.Status(400).JSON(fiber.Map{"error": "studentRkId required"})
		}
		info := internalDb.FindActiveExamForStudent(db, studentID)
		if info == nil || info.Enrollment.PaperSentAt == nil || info.Paper == nil {
			return c.Status(403).JSON(fiber.Map{"error": "Không có quyền"})
		}
		kind := c.Query("kind", "pdf")
		if kind == "pdf" {
			return c.Download(info.Paper.PdfPath, filepath.Base(info.Paper.PdfPath))
		}
		fileID, _ := strconv.ParseUint(c.Query("fileId", "0"), 10, 64)
		var res models.ExamPaperResource
		if err := db.Where("id = ? AND exam_paper_id = ?", fileID, info.Paper.ID).First(&res).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "File không tồn tại"})
		}
		return c.Download(res.FilePath, res.FileName)
	}
}

// POST /api/student/exam/submit — multipart zip
func SubmitStudentExamHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		studentID, _ := strconv.ParseInt(c.FormValue("studentRkId"), 10, 64)
		if studentID <= 0 {
			return c.Status(400).JSON(fiber.Map{"error": "studentRkId required"})
		}
		info := internalDb.FindActiveExamForStudent(db, studentID)
		if info == nil {
			return c.Status(403).JSON(fiber.Map{"error": "Không trong giờ thi"})
		}
		file, err := c.FormFile("submission")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Thiếu file nộp bài"})
		}
		dir := filepath.Join(examRoomDir(info.Room.ID), "submissions", fmt.Sprintf("%d", studentID))
		_ = os.MkdirAll(dir, 0755)
		name := fmt.Sprintf("%s_%d.zip", time.Now().Format("20060102_150405"), studentID)
		dest := filepath.Join(dir, name)
		if err := c.SaveFile(file, dest); err != nil {
			return c.Status(500).JSON(fiber.Map{"error": "Lưu bài thất bại"})
		}
		_ = db.Where("exam_room_id = ? AND student_rk_id = ?", info.Room.ID, studentID).Delete(&models.ExamSubmission{}).Error
		sub := models.ExamSubmission{
			ExamRoomID:  info.Room.ID,
			StudentRkID: studentID,
			FilePath:    dest,
			FileName:    name,
		}
		_ = db.Create(&sub).Error
		return c.JSON(fiber.Map{"ok": true, "fileName": name})
	}
}

// GET /api/exam-rooms/:id/submissions/:subId/download — staff
func DownloadExamSubmissionHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		subID, err := parseUintParam(c, "subId")
		if err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid subId"})
		}
		var sub models.ExamSubmission
		if err := db.First(&sub, subID).Error; err != nil {
			return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
		}
		return c.Download(sub.FilePath, sub.FileName)
	}
}

func parseUintParam(c *fiber.Ctx, name string) (uint, error) {
	v, err := strconv.ParseUint(c.Params(name), 10, 64)
	return uint(v), err
}

// ZipFolder tạo file zip từ thư mục (dùng từ client hoặc test).
func ZipFolder(srcDir, destZip string) error {
	f, err := os.Create(destZip)
	if err != nil {
		return err
	}
	defer f.Close()
	w := zip.NewWriter(f)
	defer w.Close()
	return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
		if err != nil || info.IsDir() {
			return err
		}
		rel, err := filepath.Rel(srcDir, path)
		if err != nil {
			return err
		}
		rel = filepath.ToSlash(rel)
		hdr, err := zip.FileInfoHeader(info)
		if err != nil {
			return err
		}
		hdr.Name = rel
		hdr.Method = zip.Deflate
		writer, err := w.CreateHeader(hdr)
		if err != nil {
			return err
		}
		rf, err := os.Open(path)
		if err != nil {
			return err
		}
		defer rf.Close()
		_, err = io.Copy(writer, rf)
		return err
	})
}
