package handlers

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

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

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

func githubOAuthConfig() (clientID, clientSecret, redirectURI string, err error) {
	clientID = strings.TrimSpace(os.Getenv("GITHUB_OAUTH_CLIENT_ID"))
	clientSecret = strings.TrimSpace(os.Getenv("GITHUB_OAUTH_CLIENT_SECRET"))
	redirectURI = strings.TrimSpace(os.Getenv("GITHUB_OAUTH_REDIRECT_URI"))
	if redirectURI == "" {
		redirectURI = "http://127.0.0.1:8080/api/auth/github/callback"
	}
	if clientID == "" || clientSecret == "" {
		return "", "", "", errors.New("Server chưa cấu hình GITHUB_OAUTH_CLIENT_ID / GITHUB_OAUTH_CLIENT_SECRET")
	}
	return clientID, clientSecret, redirectURI, nil
}

func loadStaffGitHubToken(db *gorm.DB, staffID uint) (token string, login string, err error) {
	var row models.StaffGitHubAuth
	if err := db.First(&row, "staff_id = ?", staffID).Error; err != nil {
		return "", "", err
	}
	token, err = auth.DecryptSecret(row.AccessTokenEnc)
	if err != nil {
		return "", "", err
	}
	return token, row.GitHubLogin, nil
}

// GET /api/auth/github/authorize — trả URL để mở popup OAuth (cần JWT staff)
func GitHubAuthorizeURLHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		if _, _, _, err := githubOAuthConfig(); err != nil {
			return c.Status(503).JSON(fiber.Map{"error": err.Error()})
		}
		staffID := middleware.StaffIDFromCtx(c)
		if staffID == 0 {
			return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
		}
		state, err := auth.IssueGitHubOAuthState(staffID)
		if err != nil {
			return c.Status(500).JSON(fiber.Map{"error": "Không tạo được state OAuth"})
		}
		clientID, _, redirectURI, _ := githubOAuthConfig()
		authorizeURL := fmt.Sprintf(
			"https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=%s&state=%s",
			url.QueryEscape(clientID),
			url.QueryEscape(redirectURI),
			url.QueryEscape("repo delete_repo"),
			url.QueryEscape(state),
		)
		return c.JSON(fiber.Map{"authorizeUrl": authorizeURL})
	}
}

// GET /api/auth/github/callback — GitHub redirect (public)
func GitHubOAuthCallbackHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		if errMsg := strings.TrimSpace(c.Query("error")); errMsg != "" {
			return oauthCallbackHTML(c, false, "GitHub từ chối: "+errMsg)
		}
		code := strings.TrimSpace(c.Query("code"))
		state := strings.TrimSpace(c.Query("state"))
		if code == "" || state == "" {
			return oauthCallbackHTML(c, false, "Thiếu mã xác thực từ GitHub")
		}
		staffID, err := auth.ParseGitHubOAuthState(state)
		if err != nil {
			return oauthCallbackHTML(c, false, "Phiên OAuth hết hạn — thử kết nối lại")
		}
		clientID, clientSecret, redirectURI, err := githubOAuthConfig()
		if err != nil {
			return oauthCallbackHTML(c, false, err.Error())
		}

		form := url.Values{}
		form.Set("client_id", clientID)
		form.Set("client_secret", clientSecret)
		form.Set("code", code)
		form.Set("redirect_uri", redirectURI)

		req, err := http.NewRequest(http.MethodPost, "https://github.com/login/oauth/access_token", strings.NewReader(form.Encode()))
		if err != nil {
			return oauthCallbackHTML(c, false, err.Error())
		}
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("Accept", "application/json")

		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return oauthCallbackHTML(c, false, "Không đổi được token GitHub")
		}
		defer resp.Body.Close()
		var tokenResp struct {
			AccessToken string `json:"access_token"`
			Scope       string `json:"scope"`
			Error       string `json:"error"`
			Description string `json:"error_description"`
		}
		_ = json.NewDecoder(resp.Body).Decode(&tokenResp)
		if tokenResp.AccessToken == "" {
			msg := tokenResp.Description
			if msg == "" {
				msg = tokenResp.Error
			}
			if msg == "" {
				msg = "Không nhận được access token"
			}
			return oauthCallbackHTML(c, false, msg)
		}

		userReq, _ := http.NewRequest(http.MethodGet, "https://api.github.com/user", nil)
		userReq.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken)
		userReq.Header.Set("Accept", "application/vnd.github+json")
		userResp, err := http.DefaultClient.Do(userReq)
		if err != nil {
			return oauthCallbackHTML(c, false, "Không lấy được thông tin GitHub")
		}
		defer userResp.Body.Close()
		var ghUser struct {
			Login string `json:"login"`
		}
		_ = json.NewDecoder(userResp.Body).Decode(&ghUser)
		if ghUser.Login == "" {
			return oauthCallbackHTML(c, false, "Tài khoản GitHub không hợp lệ")
		}

		enc, err := auth.EncryptSecret(tokenResp.AccessToken)
		if err != nil {
			return oauthCallbackHTML(c, false, "Không lưu được token")
		}
		row := models.StaffGitHubAuth{
			StaffID:        staffID,
			GitHubLogin:    ghUser.Login,
			AccessTokenEnc: enc,
			Scope:          tokenResp.Scope,
			ConnectedAt:    time.Now(),
		}
		if err := db.Save(&row).Error; err != nil {
			return oauthCallbackHTML(c, false, "Lưu DB thất bại")
		}
		return oauthCallbackHTML(c, true, ghUser.Login)
	}
}

func oauthCallbackHTML(c *fiber.Ctx, ok bool, detail string) error {
	c.Set("Content-Type", "text/html; charset=utf-8")
	payload, _ := json.Marshal(map[string]any{"type": "simple-care-github-connected", "ok": ok, "detail": detail})
	html := fmt.Sprintf(`<!DOCTYPE html>
<html lang="vi"><head><meta charset="utf-8"><title>GitHub</title></head>
<body style="font-family:Segoe UI,sans-serif;padding:2rem;text-align:center">
<p>%s</p>
<script>
try {
  if (window.opener) window.opener.postMessage(%s, "*");
} catch (e) {}
setTimeout(function(){ window.close(); }, 800);
</script>
</body></html>`, detail, string(payload))
	if !ok {
		html = strings.Replace(html, detail, "Lỗi: "+detail, 1)
	} else {
		html = strings.Replace(html, detail, "Đã kết nối GitHub: @"+detail, 1)
	}
	return c.SendString(html)
}

// GET /api/auth/github/status
func GitHubStatusHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		staffID := middleware.StaffIDFromCtx(c)
		var row models.StaffGitHubAuth
		if err := db.First(&row, "staff_id = ?", staffID).Error; err != nil {
			return c.JSON(fiber.Map{"connected": false})
		}
		return c.JSON(fiber.Map{
			"connected":      true,
			"githubLogin":    row.GitHubLogin,
			"connectedAt":    row.ConnectedAt,
			"scope":          row.Scope,
			"canDeleteRepos": githubScopeAllowsDelete(row.Scope),
		})
	}
}

// DELETE /api/auth/github
func GitHubDisconnectHandler(db *gorm.DB) fiber.Handler {
	return func(c *fiber.Ctx) error {
		staffID := middleware.StaffIDFromCtx(c)
		_ = db.Delete(&models.StaffGitHubAuth{}, "staff_id = ?", staffID).Error
		return c.JSON(fiber.Map{"ok": true})
	}
}

func githubScopeAllowsDelete(scope string) bool {
	scope = strings.ToLower(strings.TrimSpace(scope))
	if scope == "" {
		return false
	}
	for _, part := range strings.FieldsFunc(scope, func(r rune) bool {
		return r == ' ' || r == ','
	}) {
		if part == "delete_repo" {
			return true
		}
	}
	return false
}

func githubGetUser(token string) (login string, err error) {
	req, _ := http.NewRequest(http.MethodGet, "https://api.github.com/user", nil)
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Accept", "application/vnd.github+json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	var u struct {
		Login string `json:"login"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
		return "", err
	}
	if u.Login == "" {
		return "", errors.New("không lấy được tài khoản GitHub")
	}
	return u.Login, nil
}

func githubGetRepo(token, owner, repoName string) (htmlURL string, found bool) {
	meta, ok := githubFetchRepo(token, owner, repoName)
	if !ok {
		return "", false
	}
	return meta.HTMLURL, true
}

func githubFetchRepo(token, owner, repoName string) (meta struct {
	HTMLURL       string
	DefaultBranch string
}, ok bool) {
	apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s", url.PathEscape(owner), url.PathEscape(repoName))
	req, _ := http.NewRequest(http.MethodGet, apiURL, nil)
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return meta, false
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return meta, false
	}
	var repo struct {
		HTMLURL       string `json:"html_url"`
		DefaultBranch string `json:"default_branch"`
	}
	_ = json.NewDecoder(resp.Body).Decode(&repo)
	if repo.HTMLURL == "" {
		return meta, false
	}
	meta.HTMLURL = repo.HTMLURL
	meta.DefaultBranch = strings.TrimSpace(repo.DefaultBranch)
	if meta.DefaultBranch == "" {
		meta.DefaultBranch = "main"
	}
	return meta, true
}

func clearStaleExamRoomGitURLs(db *gorm.DB, room *models.ExamRoom) {
	room.GitRepoURL = ""
	room.GitPublishURL = ""
	_ = db.Model(room).Updates(map[string]any{"git_repo_url": "", "git_publish_url": ""}).Error
}

func clearStaleSubmissionGitURLs(db *gorm.DB, row *submissionRow) {
	row.GitRepoURL = ""
	row.GitPublishURL = ""
	_ = db.Model(&models.ExamSubmission{}).Where("id = ?", row.ID).
		Updates(map[string]any{"git_repo_url": "", "git_publish_url": ""}).Error
}

func githubRepoTrackedForRoom(db *gorm.DB, staffID uint, owner, repoName string, roomID uint) bool {
	var count int64
	_ = db.Model(&models.StaffGitHubRepo{}).
		Where("staff_id = ? AND owner_login = ? AND repo_name = ? AND exam_room_id = ? AND publish_mode = ?",
			staffID, owner, repoName, roomID, models.GitPublishModeRoom).
		Count(&count).Error
	return count > 0
}

func githubRepoTrackedForSubmission(db *gorm.DB, staffID uint, owner, repoName string, submissionID uint) bool {
	var count int64
	_ = db.Model(&models.StaffGitHubRepo{}).
		Where("staff_id = ? AND owner_login = ? AND repo_name = ? AND submission_id = ? AND publish_mode = ?",
			staffID, owner, repoName, submissionID, models.GitPublishModeStudent).
		Count(&count).Error
	return count > 0
}

func repoNameMatchesSavedURL(savedURL, owner, repoName string) bool {
	if strings.TrimSpace(savedURL) == "" {
		return false
	}
	o, r, err := parseGitHubRepo(savedURL)
	return err == nil && strings.EqualFold(o, owner) && strings.EqualFold(r, repoName)
}

func githubCreateRepo(token, owner, repoName string, private bool) (actualName, htmlURL string, err error) {
	repoName = trimGitHubRepoName(repoName)
	body := map[string]any{
		"name":        repoName,
		"private":     private,
		"auto_init":   true,
		"description": "Bài nộp phòng thi — Simple Care",
	}
	payload, _ := json.Marshal(body)
	req, err := http.NewRequest(http.MethodPost, "https://api.github.com/user/repos", bytes.NewReader(payload))
	if err != nil {
		return "", "", err
	}
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", "", err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 300 {
		return "", "", fmt.Errorf("tạo repo thất bại: %s", strings.TrimSpace(string(raw)))
	}
	var created struct {
		HTMLURL string `json:"html_url"`
		Name    string `json:"name"`
		FullName string `json:"full_name"`
	}
	if err := json.Unmarshal(raw, &created); err != nil {
		return "", "", err
	}
	if created.HTMLURL == "" {
		return "", "", errors.New("GitHub không trả URL repo")
	}
	actualName = strings.TrimSpace(created.Name)
	if actualName == "" {
		if _, r, perr := parseGitHubRepo(created.HTMLURL); perr == nil {
			actualName = r
		} else {
			actualName = repoName
		}
	}
	return actualName, created.HTMLURL, nil
}

func githubOwnerRepoFromURL(htmlURL string) (owner, repo string, ok bool) {
	o, r, err := parseGitHubRepo(htmlURL)
	return o, r, err == nil
}

func isGitHubRepoNameTaken(err error) bool {
	if err == nil {
		return false
	}
	msg := strings.ToLower(err.Error())
	return strings.Contains(msg, "name already exists") || strings.Contains(msg, `"code":"custom"`) && strings.Contains(msg, "name")
}

func trimGitHubRepoName(name string) string {
	name = strings.ToLower(strings.TrimSpace(name))
	name = strings.NewReplacer(" ", "-", "_", "-").Replace(name)
	var b strings.Builder
	for _, r := range name {
		switch {
		case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '-', r == '.':
			b.WriteRune(r)
		case r == '_':
			b.WriteRune('-')
		}
	}
	name = b.String()
	for strings.Contains(name, "--") {
		name = strings.ReplaceAll(name, "--", "-")
	}
	name = strings.Trim(name, "-.")
	if len(name) > 90 {
		name = strings.Trim(name[:90], "-.")
	}
	if name == "" {
		name = "exam-repo"
	}
	return name
}

func examGitRepoBaseName(room *models.ExamRoom) string {
	base := strings.ToLower(sanitizePathPart(room.Name))
	base = strings.NewReplacer(" ", "-", "_", "-").Replace(base)
	base = invalidPathChars.ReplaceAllString(base, "-")
	base = strings.Trim(base, "-.")
	for strings.Contains(base, "--") {
		base = strings.ReplaceAll(base, "--", "-")
	}
	if base == "" {
		base = fmt.Sprintf("exam-%d", room.ID)
	}
	return trimGitHubRepoName(base)
}

func examGitRepoNameCandidates(room *models.ExamRoom) []string {
	base := examGitRepoBaseName(room)
	candidates := []string{
		base,
		fmt.Sprintf("%s-exam-%d", base, room.ID),
		fmt.Sprintf("%s-%d", base, room.ID),
		fmt.Sprintf("exam-%d", room.ID),
	}
	for i := 2; i <= 20; i++ {
		candidates = append(candidates, fmt.Sprintf("%s-exam-%d-%d", base, room.ID, i))
	}
	seen := map[string]bool{}
	out := make([]string, 0, len(candidates))
	for _, c := range candidates {
		c = trimGitHubRepoName(c)
		if seen[c] {
			continue
		}
		seen[c] = true
		out = append(out, c)
	}
	return out
}

// githubEnsureRepoForRoom — dùng repo đã gắn phòng thi, hoặc tạo mới; trùng tên thì tự đổi tên.
func githubEnsureRepoForRoom(db *gorm.DB, staffID uint, token, owner string, room *models.ExamRoom) (repoName, htmlURL string, err error) {
	if u := strings.TrimSpace(room.GitRepoURL); u != "" {
		o, r, parseErr := parseGitHubRepo(u)
		if parseErr == nil && strings.EqualFold(o, owner) {
			if url, ok := githubGetRepo(token, owner, trimGitHubRepoName(r)); ok {
				if _, name, perr := parseGitHubRepo(url); perr == nil && name != "" {
					return name, url, nil
				}
				return trimGitHubRepoName(r), url, nil
			}
			clearStaleExamRoomGitURLs(db, room)
		}
	}

	var tracked models.StaffGitHubRepo
	if err := db.Where("staff_id = ? AND exam_room_id = ? AND publish_mode = ?", staffID, room.ID, models.GitPublishModeRoom).
		Order("created_at desc").First(&tracked).Error; err == nil {
		if strings.EqualFold(tracked.OwnerLogin, owner) {
			trackedName := trimGitHubRepoName(tracked.RepoName)
			if url, ok := githubGetRepo(token, owner, trackedName); ok {
				if _, name, perr := parseGitHubRepo(url); perr == nil && name != "" {
					return name, url, nil
				}
				return trackedName, url, nil
			}
		}
	}

	for _, candidate := range examGitRepoNameCandidates(room) {
		if url, ok := githubGetRepo(token, owner, candidate); ok {
			if repoNameMatchesSavedURL(room.GitRepoURL, owner, candidate) ||
				githubRepoTrackedForRoom(db, staffID, owner, candidate, room.ID) {
				return candidate, url, nil
			}
			continue
		}
		actualName, url, createErr := githubCreateRepo(token, owner, candidate, true)
		if createErr == nil {
			return actualName, url, nil
		}
		if isGitHubRepoNameTaken(createErr) {
			continue
		}
		return "", "", createErr
	}

	fallback := trimGitHubRepoName(fmt.Sprintf("%s-sc-%d-%d", examGitRepoBaseName(room), room.ID, time.Now().Unix()))
	actualName, url, createErr := githubCreateRepo(token, owner, fallback, true)
	if createErr != nil {
		return "", "", fmt.Errorf("không tạo được repo GitHub — đã thử nhiều tên khác nhau: %w", createErr)
	}
	return actualName, url, nil
}

func examGitRepoName(room *models.ExamRoom) string {
	if u := strings.TrimSpace(room.GitRepoURL); u != "" {
		if _, name, err := parseGitHubRepo(u); err == nil && name != "" {
			return name
		}
	}
	return examGitRepoBaseName(room)
}

func resolvePublishRepo(db *gorm.DB, staffID uint, token, ghLogin string, room *models.ExamRoom) (owner, repo, branch, repoHTML string, err error) {
	owner = ghLogin
	repo, htmlURL, err := githubEnsureRepoForRoom(db, staffID, token, owner, room)
	if err != nil {
		return "", "", "", "", err
	}
	branch = "main"
	if meta, ok := githubFetchRepo(token, owner, repo); ok && meta.DefaultBranch != "" {
		branch = meta.DefaultBranch
	}
	return owner, repo, branch, htmlURL, nil
}

func studentGitRepoNameCandidates(row submissionRow, room *models.ExamRoom) []string {
	base := trimGitHubRepoName(strings.ToLower(studentSubmissionFolder(row.StudentCode, row.FullName)))
	candidates := []string{
		base,
		fmt.Sprintf("%s-exam-%d", base, room.ID),
		fmt.Sprintf("%s-%d", base, row.StudentRkID),
		fmt.Sprintf("exam-%d-%d", room.ID, row.StudentRkID),
	}
	seen := map[string]bool{}
	out := make([]string, 0, len(candidates))
	for _, c := range candidates {
		c = trimGitHubRepoName(c)
		if seen[c] {
			continue
		}
		seen[c] = true
		out = append(out, c)
	}
	for i := 2; i <= 10; i++ {
		c := trimGitHubRepoName(fmt.Sprintf("%s-%d", base, i))
		if !seen[c] {
			seen[c] = true
			out = append(out, c)
		}
	}
	return out
}

func githubEnsureRepoForStudent(db *gorm.DB, staffID uint, token, owner string, room *models.ExamRoom, row *submissionRow) (repoName, htmlURL string, err error) {
	if u := strings.TrimSpace(row.GitRepoURL); u != "" {
		o, r, parseErr := parseGitHubRepo(u)
		if parseErr == nil && strings.EqualFold(o, owner) {
			if url, ok := githubGetRepo(token, owner, trimGitHubRepoName(r)); ok {
				if _, name, perr := parseGitHubRepo(url); perr == nil && name != "" {
					return name, url, nil
				}
				return trimGitHubRepoName(r), url, nil
			}
			clearStaleSubmissionGitURLs(db, row)
		}
	}

	var tracked models.StaffGitHubRepo
	if err := db.Where("staff_id = ? AND submission_id = ? AND publish_mode = ?", staffID, row.ID, models.GitPublishModeStudent).
		Order("created_at desc").First(&tracked).Error; err == nil {
		if strings.EqualFold(tracked.OwnerLogin, owner) {
			trackedName := trimGitHubRepoName(tracked.RepoName)
			if url, ok := githubGetRepo(token, owner, trackedName); ok {
				if _, name, perr := parseGitHubRepo(url); perr == nil && name != "" {
					return name, url, nil
				}
				return trackedName, url, nil
			}
		}
	}

	for _, candidate := range studentGitRepoNameCandidates(*row, room) {
		if url, ok := githubGetRepo(token, owner, candidate); ok {
			if repoNameMatchesSavedURL(row.GitRepoURL, owner, candidate) ||
				githubRepoTrackedForSubmission(db, staffID, owner, candidate, row.ID) {
				return candidate, url, nil
			}
			continue
		}
		actualName, url, createErr := githubCreateRepo(token, owner, candidate, true)
		if createErr == nil {
			return actualName, url, nil
		}
		if isGitHubRepoNameTaken(createErr) {
			continue
		}
		return "", "", createErr
	}

	fallback := trimGitHubRepoName(fmt.Sprintf("%s-sc-%d-%d", studentSubmissionFolder(row.StudentCode, row.FullName), row.StudentRkID, time.Now().Unix()))
	actualName, url, createErr := githubCreateRepo(token, owner, fallback, true)
	if createErr != nil {
		return "", "", fmt.Errorf("không tạo được repo GitHub cho sinh viên: %w", createErr)
	}
	return actualName, url, nil
}
