package handlers

import (
	"archive/zip"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"server/internal/middleware"
	"server/internal/models"

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

var invalidPathChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f]+`)

type submissionRow struct {
	models.ExamSubmission
	FullName    string
	StudentCode string
}

func sanitizePathPart(s string) string {
	s = strings.TrimSpace(s)
	s = invalidPathChars.ReplaceAllString(s, "_")
	s = strings.Trim(s, " .")
	if s == "" {
		return "unknown"
	}
	return s
}

func studentSubmissionFolder(code, fullName string) string {
	code = sanitizePathPart(code)
	name := sanitizePathPart(fullName)
	if code != "unknown" && name != "unknown" {
		return code + "_" + name
	}
	if code != "unknown" {
		return code
	}
	if name != "unknown" {
		return name
	}
	return "sinh_vien"
}

func shouldSkipBundledPath(rel string) bool {
	rel = filepath.ToSlash(strings.TrimSpace(rel))
	if rel == "" {
		return true
	}
	for _, part := range strings.Split(rel, "/") {
		switch part {
		case ".git", "__MACOSX", ".svn", ".hg":
			return true
		}
	}
	base := filepath.Base(rel)
	switch base {
	case ".DS_Store", "Thumbs.db", "desktop.ini":
		return true
	}
	return false
}

func unzipToDir(zipPath, destDir string) (skipped []string, err error) {
	r, err := zip.OpenReader(zipPath)
	if err != nil {
		return nil, err
	}
	defer r.Close()

	cleanDest := filepath.Clean(destDir)
	for _, f := range r.File {
		name := filepath.FromSlash(f.Name)
		if shouldSkipBundledPath(name) {
			continue
		}
		target := filepath.Join(destDir, name)
		cleanTarget := filepath.Clean(target)
		if cleanTarget != cleanDest && !strings.HasPrefix(cleanTarget, cleanDest+string(os.PathSeparator)) {
			continue
		}
		if f.FileInfo().IsDir() {
			if err := os.MkdirAll(cleanTarget, 0755); err != nil {
				skipped = append(skipped, name+": "+err.Error())
			}
			continue
		}
		if err := os.MkdirAll(filepath.Dir(cleanTarget), 0755); err != nil {
			skipped = append(skipped, name+": "+err.Error())
			continue
		}
		rc, err := f.Open()
		if err != nil {
			skipped = append(skipped, name+": "+err.Error())
			continue
		}
		out, err := os.Create(cleanTarget)
		if err != nil {
			rc.Close()
			skipped = append(skipped, name+": "+err.Error())
			continue
		}
		_, copyErr := io.Copy(out, rc)
		out.Close()
		rc.Close()
		if copyErr != nil {
			skipped = append(skipped, name+": "+copyErr.Error())
		}
	}
	return skipped, nil
}

func loadExamSubmissionRows(db *gorm.DB, roomID uint) ([]submissionRow, error) {
	var subs []models.ExamSubmission
	if err := db.Where("exam_room_id = ?", roomID).Order("created_at asc").Find(&subs).Error; err != nil {
		return nil, err
	}
	rows := make([]submissionRow, 0, len(subs))
	for _, s := range subs {
		row := submissionRow{ExamSubmission: 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
		}
		rows = append(rows, row)
	}
	return rows, nil
}

func copySubmissionZipEntries(srcZipPath, prefix string, addFile func(destPath string, open func() (io.ReadCloser, error), size int64) error) (skipped []string, err error) {
	r, err := zip.OpenReader(srcZipPath)
	if err != nil {
		return nil, err
	}
	defer r.Close()

	prefix = strings.Trim(prefix, "/\\")
	for _, f := range r.File {
		name := filepath.ToSlash(filepath.FromSlash(f.Name))
		if shouldSkipBundledPath(name) {
			continue
		}
		if f.FileInfo().IsDir() {
			continue
		}
		dest := name
		if prefix != "" {
			dest = prefix + "/" + name
		}
		dest = strings.TrimPrefix(dest, "/")
		entry := f
		addErr := addFile(dest, entry.Open, int64(entry.UncompressedSize64))
		if addErr != nil {
			skipped = append(skipped, dest+": "+addErr.Error())
		}
	}
	return skipped, nil
}

func bundleSubmissionZips(rows []submissionRow, destZipPath string) (warnings []string, fileCount int, err error) {
	f, err := os.Create(destZipPath)
	if err != nil {
		return nil, 0, err
	}
	zw := zip.NewWriter(f)
	usedFolders := map[string]int{}

	for _, row := range rows {
		if _, statErr := os.Stat(row.FilePath); statErr != nil {
			warnings = append(warnings, fmt.Sprintf("bài SV %d: không tìm thấy file", row.StudentRkID))
			continue
		}
		folder := studentSubmissionFolder(row.StudentCode, row.FullName)
		if n := usedFolders[folder]; n > 0 {
			folder = fmt.Sprintf("%s_%d", folder, n+1)
		}
		usedFolders[folder]++

		skipped, copyErr := copySubmissionZipEntries(row.FilePath, folder, func(destPath string, open func() (io.ReadCloser, error), size int64) error {
			hdr := &zip.FileHeader{
				Name:   destPath,
				Method: zip.Deflate,
			}
			w, err := zw.CreateHeader(hdr)
			if err != nil {
				return err
			}
			rc, err := open()
			if err != nil {
				return err
			}
			_, copyErr := io.Copy(w, rc)
			rc.Close()
			if copyErr == nil {
				fileCount++
			}
			return copyErr
		})
		if copyErr != nil {
			warnings = append(warnings, folder+": "+copyErr.Error())
			continue
		}
		for _, s := range skipped {
			warnings = append(warnings, folder+"/"+s)
		}
	}

	if closeErr := zw.Close(); closeErr != nil {
		f.Close()
		_ = os.Remove(destZipPath)
		return warnings, fileCount, closeErr
	}
	if closeErr := f.Close(); closeErr != nil {
		_ = os.Remove(destZipPath)
		return warnings, fileCount, closeErr
	}
	if fileCount == 0 {
		_ = os.Remove(destZipPath)
		return warnings, 0, errors.New("không có file bài nộp hợp lệ để gộp")
	}
	return warnings, fileCount, nil
}

func collectSubmissionZipFiles(rows []submissionRow) (files []gitPathFile, warnings []string) {
	return collectSubmissionZipFilesWithPrefix(rows, true)
}

func collectSubmissionZipFilesWithPrefix(rows []submissionRow, useStudentFolder bool) (files []gitPathFile, warnings []string) {
	byPath := map[string]gitPathFile{}
	usedFolders := map[string]int{}
	for _, row := range rows {
		if _, statErr := os.Stat(row.FilePath); statErr != nil {
			warnings = append(warnings, fmt.Sprintf("bài SV %d: không tìm thấy file", row.StudentRkID))
			continue
		}
		prefix := ""
		if useStudentFolder {
			folder := studentSubmissionFolder(row.StudentCode, row.FullName)
			if n := usedFolders[folder]; n > 0 {
				folder = fmt.Sprintf("%s_%d", folder, n+1)
			}
			usedFolders[folder]++
			prefix = folder
		}

		skipped, err := copySubmissionZipEntries(row.FilePath, prefix, func(destPath string, open func() (io.ReadCloser, error), size int64) error {
			if size > githubBlobMaxBytes {
				warnings = append(warnings, destPath+": file quá lớn (>100MB), bỏ qua")
				return nil
			}
			rc, err := open()
			if err != nil {
				return err
			}
			data, readErr := io.ReadAll(rc)
			rc.Close()
			if readErr != nil {
				return readErr
			}
			byPath[destPath] = gitPathFile{path: destPath, content: data}
			return nil
		})
		if err != nil {
			warnings = append(warnings, prefix+": "+err.Error())
			continue
		}
		for _, s := range skipped {
			warnings = append(warnings, prefix+"/"+s)
		}
	}
	files = make([]gitPathFile, 0, len(byPath))
	for _, f := range byPath {
		files = append(files, f)
	}
	return files, warnings
}

func buildExamSubmissionsBundle(room models.ExamRoom, rows []submissionRow) (zipPath string, cleanup func(), err error) {
	tmpBase, err := os.MkdirTemp("", "exam-bundle-*")
	if err != nil {
		return "", nil, err
	}
	cleanup = func() { _ = os.RemoveAll(tmpBase) }

	rootName := sanitizePathPart(room.Name) + "_bai_nop"
	outZip := filepath.Join(tmpBase, rootName+".zip")
	_, _, err = bundleSubmissionZips(rows, outZip)
	if err != nil {
		cleanup()
		return "", nil, err
	}
	return outZip, cleanup, nil
}

func prepareExamSubmissionsForGit(rows []submissionRow) (files []gitPathFile, warnings []string, err error) {
	files, warnings = collectSubmissionZipFiles(rows)
	if len(files) == 0 {
		return nil, warnings, errors.New("không có file bài nộp hợp lệ để đẩy")
	}
	return files, warnings, nil
}

// GET /api/exam-rooms/:id/submissions/download-all
func DownloadAllExamSubmissionsHandler(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 thi"})
		}
		rows, err := loadExamSubmissionRows(db, roomID)
		if err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		if len(rows) == 0 {
			return c.Status(400).JSON(fiber.Map{"error": "Chưa có bài nộp"})
		}

		zipPath, cleanup, err := buildExamSubmissionsBundle(room, rows)
		if err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		zipBytes, readErr := os.ReadFile(zipPath)
		cleanup()
		if readErr != nil {
			return c.Status(500).JSON(fiber.Map{"error": "Không đọc được file zip gộp"})
		}

		downloadName := sanitizePathPart(room.Name) + "_bai_nop.zip"
		c.Set("Content-Type", "application/zip")
		c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, downloadName))
		return c.Send(zipBytes)
	}
}

// PATCH /api/exam-rooms/:id/git-settings
func UpdateExamRoomGitSettingsHandler(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 thi"})
		}
		var req struct {
			GitRepoURL *string `json:"gitRepoUrl"`
			GitBranch  *string `json:"gitBranch"`
		}
		if err := c.BodyParser(&req); err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
		}
		if req.GitRepoURL != nil {
			room.GitRepoURL = strings.TrimSpace(*req.GitRepoURL)
		}
		if req.GitBranch != nil {
			b := strings.TrimSpace(*req.GitBranch)
			if b == "" {
				b = "main"
			}
			room.GitBranch = b
		}
		if room.GitBranch == "" {
			room.GitBranch = "main"
		}
		if err := db.Save(&room).Error; err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		return c.JSON(fiber.Map{
			"gitRepoUrl":    room.GitRepoURL,
			"gitBranch":     room.GitBranch,
			"gitPublishUrl": room.GitPublishURL,
		})
	}
}

func parseGitHubRepo(repoURL string) (owner, repo string, err error) {
	u := strings.TrimSpace(repoURL)
	u = strings.TrimSuffix(u, ".git")
	u = strings.TrimPrefix(u, "https://github.com/")
	u = strings.TrimPrefix(u, "http://github.com/")
	u = strings.TrimPrefix(u, "github.com/")
	parts := strings.Split(strings.Trim(u, "/"), "/")
	if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
		return "", "", errors.New("URL GitHub không hợp lệ (vd: https://github.com/org/repo)")
	}
	return parts[0], parts[1], nil
}

// POST /api/exam-rooms/:id/submissions/publish-git
func PublishExamSubmissionsGitHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		staffID := middleware.StaffIDFromCtx(c)
		if staffID == 0 {
			return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
		}
		token, ghLogin, err := loadStaffGitHubToken(db, staffID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return c.Status(401).JSON(fiber.Map{
					"error":            "Chưa kết nối GitHub. Vào Tài khoản của tôi → Kết nối GitHub.",
					"needsGitHubOAuth": true,
				})
			}
			return c.Status(500).JSON(fiber.Map{"error": "Không đọc được token GitHub"})
		}

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

		rows, err := loadExamSubmissionRows(db, roomID)
		if err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		if len(rows) == 0 {
			return c.Status(400).JSON(fiber.Map{"error": "Chưa có bài nộp"})
		}

		files, unpackWarn, err := prepareExamSubmissionsForGit(rows)
		if err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error(), "warnings": unpackWarn})
		}

		_, repo, branch, repoHTML, err := resolvePublishRepo(db, staffID, token, ghLogin, &room)
		if err != nil {
			return c.Status(502).JSON(fiber.Map{"error": err.Error()})
		}
		if repoHTML != "" {
			room.GitRepoURL = repoHTML
			room.GitBranch = branch
		}
		pushOwner, pushRepo := resolveGitPushTarget(token, ghLogin, repo, repoHTML)

		commitMsg := fmt.Sprintf("Simple Care: bài nộp phòng thi %s (#%d)", room.Name, roomID)
		viewURL, pushWarn, err := githubPushFiles(token, pushOwner, pushRepo, branch, files, commitMsg)
		allWarn := append(unpackWarn, pushWarn...)
		if err != nil {
			return c.Status(502).JSON(fiber.Map{
				"error":    "Đẩy Git thất bại: " + err.Error(),
				"warnings": allWarn,
			})
		}

		room.GitPublishURL = viewURL
		_ = db.Save(&room).Error
		roomIDCopy := roomID
		recordStaffGitHubRepo(db, staffID, pushOwner, pushRepo, repoHTML, models.GitPublishModeRoom, room.Name, &roomIDCopy, nil)

		msg := fmt.Sprintf("Đã đẩy %d bài lên GitHub — mỗi sinh viên một folder.", len(rows))
		if len(allWarn) > 0 {
			msg += fmt.Sprintf(" (%d file bỏ qua do lỗi)", len(allWarn))
		}

		return c.JSON(fiber.Map{
			"ok":            true,
			"url":           viewURL,
			"gitPublishUrl": viewURL,
			"gitRepoUrl":    room.GitRepoURL,
			"githubLogin":   ghLogin,
			"openUrl":       viewURL,
			"warnings":      allWarn,
			"message":       msg,
		})
	}
}

// POST /api/exam-rooms/:id/submissions/publish-git-students — mỗi sinh viên một repo GitHub riêng
func PublishExamSubmissionsGitPerStudentHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		staffID := middleware.StaffIDFromCtx(c)
		if staffID == 0 {
			return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
		}
		token, ghLogin, err := loadStaffGitHubToken(db, staffID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return c.Status(401).JSON(fiber.Map{
					"error":            "Chưa kết nối GitHub. Vào Tài khoản của tôi → Kết nối GitHub.",
					"needsGitHubOAuth": true,
				})
			}
			return c.Status(500).JSON(fiber.Map{"error": "Không đọc được token GitHub"})
		}

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

		rows, err := loadExamSubmissionRows(db, roomID)
		if err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}
		if len(rows) == 0 {
			return c.Status(400).JSON(fiber.Map{"error": "Chưa có bài nộp"})
		}

		published := 0
		var allWarn []string
		var repoURLs []string
		type studentRepo struct {
			StudentRkID int64  `json:"studentRkId"`
			StudentCode string `json:"studentCode"`
			FullName    string `json:"fullName"`
			URL         string `json:"url"`
		}
		studentRepos := make([]studentRepo, 0, len(rows))

		for _, row := range rows {
			files, unpackWarn := collectSubmissionZipFilesWithPrefix([]submissionRow{row}, false)
			allWarn = append(allWarn, unpackWarn...)
			if len(files) == 0 {
				allWarn = append(allWarn, fmt.Sprintf("%s: không có file để đẩy", studentSubmissionFolder(row.StudentCode, row.FullName)))
				continue
			}
			repoName, repoHTML, err := githubEnsureRepoForStudent(db, staffID, token, ghLogin, &room, &row)
			if err != nil {
				allWarn = append(allWarn, row.StudentCode+": "+err.Error())
				continue
			}
			pushOwner, pushRepo := resolveGitPushTarget(token, ghLogin, repoName, repoHTML)
			branch := "main"
			if meta, ok := githubFetchRepo(token, pushOwner, pushRepo); ok && meta.DefaultBranch != "" {
				branch = meta.DefaultBranch
			}
			label := studentSubmissionFolder(row.StudentCode, row.FullName)
			commitMsg := fmt.Sprintf("Simple Care: bài nộp %s — %s", label, room.Name)
			viewURL, pushWarn, err := githubPushFiles(token, pushOwner, pushRepo, branch, files, commitMsg)
			allWarn = append(allWarn, pushWarn...)
			if err != nil {
				allWarn = append(allWarn, label+": "+err.Error())
				continue
			}
			_ = db.Model(&models.ExamSubmission{}).Where("id = ?", row.ID).
				Updates(map[string]any{"git_repo_url": repoHTML, "git_publish_url": viewURL}).Error
			subID := row.ID
			roomIDCopy := roomID
			recordStaffGitHubRepo(db, staffID, pushOwner, pushRepo, repoHTML, models.GitPublishModeStudent, label, &roomIDCopy, &subID)
			published++
			repoURLs = append(repoURLs, viewURL)
			studentRepos = append(studentRepos, studentRepo{
				StudentRkID: row.StudentRkID,
				StudentCode: row.StudentCode,
				FullName:    row.FullName,
				URL:         viewURL,
			})
		}

		if published == 0 {
			return c.Status(502).JSON(fiber.Map{
				"error":    "Không đẩy được repo nào",
				"warnings": allWarn,
			})
		}

		msg := fmt.Sprintf("Đã tạo %d repo GitHub — mỗi sinh viên một repo.", published)
		if len(allWarn) > 0 {
			msg += fmt.Sprintf(" (%d cảnh báo)", len(allWarn))
		}
		openURL := ""
		if len(repoURLs) > 0 {
			openURL = repoURLs[0]
		}

		return c.JSON(fiber.Map{
			"ok":           true,
			"published":    published,
			"studentRepos": studentRepos,
			"openUrl":      openURL,
			"warnings":     allWarn,
			"message":      msg,
		})
	}
}
