package main

import (
	"archive/zip"
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/json"
	"encoding/base64"
	"errors"
	"fmt"
	"html"
	"io"
	"log"
	"mime/multipart"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"client/internal/blocker"
	"client/internal/camera"
	"client/internal/guard"
	"client/internal/screen"
	"client/internal/winapi"

	"github.com/gorilla/websocket"
	"github.com/wailsapp/wails/v2/pkg/runtime"
)

var blockedReportLast sync.Map // processName -> time.Time
var lastKillDialogTime time.Time
var killDialogMu sync.Mutex

var API_BASE = getEnv("API_BASE", "https://sv.rikkeiraia.org")

func getEnv(key, defaultVal string) string {
	if val := os.Getenv(key); val != "" {
		return val
	}
	return defaultVal
}

func getWsUrl(apiBase string) string {
	if strings.HasPrefix(apiBase, "https://") {
		return strings.Replace(apiBase, "https://", "wss://", 1)
	}
	if strings.HasPrefix(apiBase, "http://") {
		return strings.Replace(apiBase, "http://", "ws://", 1)
	}
	return "ws://" + apiBase
}

var wifiReportLast sync.Map    // ssidKey -> time.Time

func getDynamicEncryptionKey() []byte {
	base := "simple-care-proctor-secretkey32!"
	hostname, _ := os.Hostname()
	home, _ := os.UserHomeDir()
	combined := fmt.Sprintf("%s-%s-%s", base, hostname, home)

	hasher := sha256.New()
	hasher.Write([]byte(combined))
	return hasher.Sum(nil)
}

func encrypt(data []byte) ([]byte, error) {
	key := getDynamicEncryptionKey()
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}
	ciphertext := gcm.Seal(nonce, nonce, data, nil)
	return ciphertext, nil
}

func decrypt(data []byte) ([]byte, error) {
	key := getDynamicEncryptionKey()
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonceSize := gcm.NonceSize()
	if len(data) < nonceSize {
		return nil, errors.New("ciphertext too short")
	}
	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
	return gcm.Open(nil, nonce, ciphertext, nil)
}

type StudentData struct {
	StudentID   int64  `json:"studentId"`
	FullName    string `json:"fullName"`
	Email       string `json:"email"`
	Avatar      string `json:"avatar"`
	Dob         string `json:"dob"`
	StudentCode string `json:"studentCode"`
	Phone       string `json:"phone"`
	SystemID    int64  `json:"systemId"`
}

type App struct {
	ctx            context.Context
	student        *StudentData
	mu             sync.Mutex
	sessionPath       string
	statsPath         string
	webviewDataPath   string
	pendingViolPath   string
	runLockPath       string
	wsConn            *websocket.Conn
	wsConnected       bool
	wsConnecting      bool
	wsBackoff         time.Duration
	wsWriteMu         sync.Mutex
	wifiSSID          string
	wifiBSSID         string
	allowedApps       string
	onlineSecs        int
	offlineSecs       int
	unsyncedOn        int
	unsyncedOff       int
	lastSyncTime      time.Time
	isStreamingSc     bool
	streamScStop      chan struct{}
	isStreamingCam    bool
	streamCamStop     chan struct{}
	statusMsg               string
	expectingLogin          bool
	needsClearPortalStorage bool
	backendOnline           bool
	dashboard      StudentDashboardSnapshot
	wifiEnforce    bool
	acceptedWifi   map[string]bool
	wifiRejected      bool
	quitDialogShown   bool
	monitoringTornDown bool
	localBrowserActive bool // trình duyệt local — chỉ cho phép localhost
	chatUnread        int
	replyStaffID      uint
	fetchAppsMu       sync.Mutex
}

type LocalStats struct {
	OnlineSecs  int `json:"onlineSecs"`
	OfflineSecs int `json:"offlineSecs"`
	UnsyncedOn  int `json:"unsyncedOn"`
	UnsyncedOff int `json:"unsyncedOff"`
}

type StudentShiftSnapshot struct {
	Period           int    `json:"period"`
	CourseName       string `json:"courseName"`
	StartTime        string `json:"startTime"`
	EndTime          string `json:"endTime"`
	IsActiveNow      bool   `json:"isActiveNow"`
	OnlineSeconds    int    `json:"onlineSeconds"`
	OfflineSeconds   int    `json:"offlineSeconds"`
	AttendanceStatus int    `json:"attendanceStatus"`
	AttendanceLabel  string `json:"attendanceLabel"`
}

type StudentExamSnapshot struct {
	ExamRoomID uint   `json:"examRoomId"`
	ExamName   string `json:"examName"`
	QuizURL    string `json:"quizUrl"`
	PaperSent  bool   `json:"paperSent"`
	PaperTitle string `json:"paperTitle"`
	Submitted  bool   `json:"submitted"`
}

type StudentDashboardSnapshot struct {
	SessionDate       string                 `json:"sessionDate"`
	MonitorMode       string                 `json:"monitorMode"`
	MonitorLabel      string                 `json:"monitorLabel"`
	ClassRkID         int64                  `json:"classRkId"`
	ClassName         string                 `json:"className"`
	ClassCode         string                 `json:"classCode"`
	CurrentPeriod     int                    `json:"currentPeriod"`
	CurrentCourseName string                 `json:"currentCourseName"`
	CurrentShiftStart string                 `json:"currentShiftStart"`
	CurrentShiftEnd   string                 `json:"currentShiftEnd"`
	InScheduleNow     bool                   `json:"inScheduleNow"`
	BlockerActive     bool                   `json:"blockerActive"`
	Shifts            []StudentShiftSnapshot `json:"shifts"`
	Exam              *StudentExamSnapshot   `json:"exam,omitempty"`
}

func NewApp() *App {
	configDir, err := os.UserConfigDir()
	if err != nil {
		// Fallback to executable folder if UserConfigDir is unavailable
		exePath, _ := os.Executable()
		configDir = filepath.Dir(exePath)
	}
	appDir := filepath.Join(configDir, "SimpleCare")
	webviewDir := filepath.Join(appDir, "WebView2")
	_ = os.MkdirAll(appDir, 0755)
	_ = os.MkdirAll(webviewDir, 0755)

	return &App{
		sessionPath:       filepath.Join(appDir, "student_session.json"),
		statsPath:         filepath.Join(appDir, "student_stats.json"),
		webviewDataPath:   webviewDir,
		pendingViolPath:   filepath.Join(appDir, "pending_violations.json"),
		runLockPath:       filepath.Join(appDir, "monitoring.lock"),
		lastSyncTime:      time.Now(),
		expectingLogin:    false,
	}
}

func (a *App) startup(ctx context.Context) {
	a.ctx = ctx
	a.loadSession()
	a.loadStats()

	// Task Manager / kill process lần trước → còn file lock → báo unclean_shutdown
	a.detectUncleanShutdown()
	go a.flushPendingViolations()

	// Request Location Access (macOS)
	winapi.RequestLocationAccess()
	winapi.RequestCameraAndMicAccess()
	winapi.RequestScreenCaptureAccess()

	// Log initial permission statuses
	log.Printf("[PERMISSIONS] Camera Status: %d (0=NotDetermined, 1=Restricted, 2=Denied, 3=Authorized)", winapi.GetCameraPermission())
	log.Printf("[PERMISSIONS] Microphone Status: %d (0=NotDetermined, 1=Restricted, 2=Denied, 3=Authorized)", winapi.GetMicrophonePermission())
	log.Printf("[PERMISSIONS] Screen Capture Status: %d (0=NoAccess, 1=Authorized, -1=NotSupported)", winapi.GetScreenCapturePermission())

	// Register blocker callbacks
	blocker.Instance.OnBlocked = func(procName string, title string) {
		go a.reportBlockedApp(procName, title)
	}
	blocker.Instance.OnKill = func(procName string, title string) {
		killDialogMu.Lock()
		if time.Since(lastKillDialogTime) < 5*time.Second {
			killDialogMu.Unlock()
			return
		}
		lastKillDialogTime = time.Now()
		killDialogMu.Unlock()

		winapi.ShowWarningMessageBox(
			"Ứng dụng bị đóng",
			fmt.Sprintf("Hệ thống đã tự động đóng ứng dụng '%s' (%s) do không nằm trong danh sách các ứng dụng được phép sử dụng trong giờ học.", procName, title),
		)
	}

	// Giám sát môi trường Windows: đa màn hình, desktop ảo, đổi user
	guard.Start(a.handleGuardViolation)

	// Khởi chạy HTTP server nhận callback login thành công
	a.startLocalServer()

	// Khởi chạy vòng lặp giám sát định kỳ (mỗi 10 giây)
	go a.monitorLoop()

	// Khởi chạy vòng lặp đọc local storage của trang Rikkei Portal khi chưa đăng nhập
	go a.authStorageScanner()

	// Chặn thoát localhost khi đang dùng trình duyệt local
	go a.localBrowserGuardLoop()
}

func (a *App) handleGuardViolation(kind, reason string) {
	if kind == "" {
		kind = "guard"
	}
	a.reportViolation(kind, reason)
	a.showQuitDialog("Vi phạm giám sát", reason)
}

// HandleBeforeClose — SV bấm X / Alt+F4 khi đang giám sát = vi phạm.
// Trả về false để cho phép đóng sau khi đã báo cáo.
func (a *App) HandleBeforeClose() (prevent bool) {
	if !a.CheckLoginStatus() || !a.isMonitoringActive() {
		a.clearRunLock()
		return false
	}
	a.mu.Lock()
	mode := a.dashboard.MonitorMode
	a.mu.Unlock()
	if !shouldRecordViolation(mode) {
		a.clearRunLock()
		return false
	}

	a.reportViolation("app_closed", "Sinh viên tự đóng ứng dụng khi đang giám sát ("+mode+")")
	a.tearDownBeforeQuit()
	a.clearRunLock()
	return false
}

// tearDownBeforeQuit ngắt mọi kênh giám sát ngay — trước khi hiện dialog (tránh treo OK để duy trì kết nối).
func (a *App) tearDownBeforeQuit() {
	a.mu.Lock()
	if a.monitoringTornDown {
		a.mu.Unlock()
		return
	}
	a.monitoringTornDown = true
	a.mu.Unlock()

	blocker.Instance.Stop()
	a.stopScreenshotStream()
	a.stopWebcamStream()
	a.disconnectWS()
	guard.Stop()
	a.clearRunLock()
}

func (a *App) isMonitoringActive() bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	return !a.monitoringTornDown
}

// showQuitDialog — ngắt giám sát trước, hiện cảnh báo, sinh viên bấm OK rồi app mới thoát.
func (a *App) showQuitDialog(title, message string) {
	a.mu.Lock()
	if a.quitDialogShown {
		a.mu.Unlock()
		return
	}
	a.quitDialogShown = true
	a.mu.Unlock()

	a.tearDownBeforeQuit()

	_, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
		Type:    runtime.WarningDialog,
		Title:   title,
		Message: message + "\n\nNhấn OK để đóng ứng dụng.",
	})
	runtime.Quit(a.ctx)
}

type pendingViolation struct {
	Kind        string `json:"kind"`
	Reason      string `json:"reason"`
	MonitorMode string `json:"monitorMode"`
	ClientAt    string `json:"clientAt"`
	StudentRkID int64  `json:"studentRkId"`
}

// shouldRecordViolation — chỉ lưu vi phạm khi đang thi hoặc đang học (trong giờ).
func shouldRecordViolation(monitorMode string) bool {
	switch strings.TrimSpace(monitorMode) {
	case "exam", "learning":
		return true
	default:
		return false
	}
}

func (a *App) markRunLock() {
	a.mu.Lock()
	studentID := int64(0)
	mode := a.dashboard.MonitorMode
	if a.student != nil {
		studentID = a.student.StudentID
	}
	a.mu.Unlock()
	if studentID <= 0 || !shouldRecordViolation(mode) {
		return
	}
	payload, _ := json.Marshal(map[string]any{
		"studentRkId": studentID,
		"monitorMode": mode,
		"startedAt":   time.Now().Format(time.RFC3339),
	})
	_ = os.WriteFile(a.runLockPath, payload, 0644)
}

func (a *App) clearRunLock() {
	_ = os.Remove(a.runLockPath)
}

func (a *App) detectUncleanShutdown() {
	data, err := os.ReadFile(a.runLockPath)
	if err != nil {
		return
	}
	_ = os.Remove(a.runLockPath)

	var meta struct {
		StudentRkID int64  `json:"studentRkId"`
		MonitorMode string `json:"monitorMode"`
		StartedAt   string `json:"startedAt"`
	}
	_ = json.Unmarshal(data, &meta)
	if meta.StudentRkID <= 0 {
		a.mu.Lock()
		if a.student != nil {
			meta.StudentRkID = a.student.StudentID
		}
		a.mu.Unlock()
	}
	if meta.StudentRkID <= 0 || !shouldRecordViolation(meta.MonitorMode) {
		return
	}
	reason := "Ứng dụng bị tắt đột ngột (Task Manager / kill process) khi đang giám sát"
	if meta.MonitorMode != "" {
		reason += " (" + meta.MonitorMode + ")"
	}
	a.enqueuePendingViolation(pendingViolation{
		Kind:        "unclean_shutdown",
		Reason:      reason,
		MonitorMode: meta.MonitorMode,
		ClientAt:    time.Now().Format(time.RFC3339),
		StudentRkID: meta.StudentRkID,
	})
}

func (a *App) enqueuePendingViolation(v pendingViolation) {
	list := a.loadPendingViolations()
	list = append(list, v)
	raw, _ := json.Marshal(list)
	_ = os.WriteFile(a.pendingViolPath, raw, 0644)
}

func (a *App) loadPendingViolations() []pendingViolation {
	data, err := os.ReadFile(a.pendingViolPath)
	if err != nil {
		return nil
	}
	var list []pendingViolation
	if json.Unmarshal(data, &list) != nil {
		return nil
	}
	return list
}

func (a *App) flushPendingViolations() {
	list := a.loadPendingViolations()
	if len(list) == 0 {
		return
	}
	remaining := make([]pendingViolation, 0)
	for _, v := range list {
		if !a.postViolation(v) {
			remaining = append(remaining, v)
		}
	}
	if len(remaining) == 0 {
		_ = os.Remove(a.pendingViolPath)
		return
	}
	raw, _ := json.Marshal(remaining)
	_ = os.WriteFile(a.pendingViolPath, raw, 0644)
}

func (a *App) postViolation(v pendingViolation) bool {
	if v.StudentRkID <= 0 || !shouldRecordViolation(v.MonitorMode) {
		return true // bỏ qua — không thi / không học thì không lưu
	}
	payload := map[string]any{
		"studentRkId": v.StudentRkID,
		"kind":        v.Kind,
		"reason":      v.Reason,
		"monitorMode": v.MonitorMode,
		"clientAt":    v.ClientAt,
	}
	bodyBytes, _ := json.Marshal(payload)
	client := http.Client{Timeout: 5 * time.Second}
	resp, err := client.Post(API_BASE+"/api/student/report-violation", "application/json", bytes.NewBuffer(bodyBytes))
	if err != nil {
		log.Printf("[VIOLATION] report failed: %v", err)
		return false
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		log.Printf("[VIOLATION] report status %d: %s", resp.StatusCode, string(body))
		return false
	}
	log.Printf("[VIOLATION] reported kind=%s student=%d", v.Kind, v.StudentRkID)
	return true
}

func (a *App) reportViolation(kind, reason string) {
	a.mu.Lock()
	studentID := int64(0)
	mode := a.dashboard.MonitorMode
	if a.student != nil {
		studentID = a.student.StudentID
	}
	a.mu.Unlock()
	if studentID <= 0 || !shouldRecordViolation(mode) {
		return
	}
	v := pendingViolation{
		Kind:        kind,
		Reason:      reason,
		MonitorMode: mode,
		ClientAt:    time.Now().Format(time.RFC3339),
		StudentRkID: studentID,
	}
	if !a.postViolation(v) {
		a.enqueuePendingViolation(v)
	}
}

// startLocalServer khởi chạy server lắng nghe callback nhận thông tin sinh viên từ webview
func (a *App) startLocalServer() {
	mux := http.NewServeMux()
	mux.HandleFunc("/login-success", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
		if r.Method == "OPTIONS" {
			return
		}

		data := r.URL.Query().Get("data")
		if data == "" {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		a.mu.Lock()
		expecting := a.expectingLogin
		a.mu.Unlock()
		if !expecting {
			w.WriteHeader(http.StatusOK)
			w.Write([]byte("ignored"))
			return
		}

		var st StudentData
		if err := json.Unmarshal([]byte(data), &st); err == nil && st.StudentID > 0 {
			a.saveSession(&st)
			log.Printf("[LOCALSERVER] Student %s logged in successfully.", st.FullName)

			a.mu.Lock()
			a.expectingLogin = false
			a.mu.Unlock()

			// Kiểm tra điều kiện app được phép và giờ học trước khi tải giao diện
			go a.refreshStudentData()

			// Chuyển hướng WebView về trang dashboard của app bằng cách reload app assets
			guard.SuppressFor(5 * time.Second)
			runtime.WindowReloadApp(a.ctx)
			w.Write([]byte("ok"))
		} else {
			w.WriteHeader(http.StatusBadRequest)
		}
	})

	server := &http.Server{
		Addr:    "127.0.0.1:34115",
		Handler: mux,
	}

	mux.HandleFunc("/exam-view", func(w http.ResponseWriter, r *http.Request) {
		target := strings.TrimSpace(r.URL.Query().Get("url"))
		title := strings.TrimSpace(r.URL.Query().Get("title"))
		if target == "" {
			http.Error(w, "missing url", http.StatusBadRequest)
			return
		}
		if title == "" {
			title = "Xem trong Simple Care"
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_, _ = fmt.Fprintf(w, examViewHTML, html.EscapeString(title), html.EscapeString(title), html.EscapeString(target))
	})
	mux.HandleFunc("/exam-close", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		if r.Method == "OPTIONS" {
			w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
			return
		}
		a.setLocalBrowserActive(false)
		go func() {
			guard.SuppressFor(5 * time.Second)
			runtime.WindowReloadApp(a.ctx)
		}()
		w.Write([]byte("ok"))
	})
	mux.HandleFunc("/exam-clear-cache", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		if r.Method == "OPTIONS" {
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
			return
		}
		// Chỉ xóa HTTP disk cache — không đụng cookie / localStorage (giữ phiên đăng nhập trang thi).
		go a.purgeWebViewDiskCache()
		w.Write([]byte("ok"))
	})
	mux.HandleFunc("/local-browser", func(w http.ResponseWriter, r *http.Request) {
		blocked := strings.TrimSpace(r.URL.Query().Get("blocked"))
		from := strings.TrimSpace(r.URL.Query().Get("from"))
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		notice := ""
		if blocked == "1" {
			notice = `<div class="notice">Đã chặn liên kết ngoài mạng. Trình duyệt này chỉ mở <strong>localhost</strong> / <strong>127.0.0.1</strong>.</div>`
			if from != "" {
				notice += `<div class="notice-sub">Đã chặn: ` + html.EscapeString(from) + `</div>`
			}
		}
		_, _ = fmt.Fprintf(w, localBrowserHTML, notice)
	})
	mux.HandleFunc("/local-browser-go", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		if r.Method == "OPTIONS" {
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
			return
		}
		raw := strings.TrimSpace(r.URL.Query().Get("url"))
		if raw == "" {
			http.Error(w, "missing url", http.StatusBadRequest)
			return
		}
		if !strings.Contains(raw, "://") {
			raw = "http://" + raw
		}
		if !isLoopbackURL(raw) {
			http.Error(w, "only localhost allowed", http.StatusForbidden)
			return
		}
		a.mu.Lock()
		a.localBrowserActive = true
		a.mu.Unlock()
		go func() {
			guard.SuppressFor(3 * time.Second)
			runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", raw))
		}()
		w.Write([]byte("ok"))
	})

	go func() {
		_ = server.ListenAndServe()
	}()
	log.Println("[LOCALSERVER] Callback server started on http://127.0.0.1:34115")
}

const examViewHTML = `<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>%s</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Segoe UI,system-ui,sans-serif;background:#f5f7fa;color:#1a2332;height:100vh;display:flex;flex-direction:column}
.bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fff;border-bottom:1px solid #e4e9f0;flex-shrink:0}
.bar-title{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:auto;min-width:0;color:#1a2332}
.bar button{border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;font-weight:600;white-space:nowrap}
.btn-back{background:#fff;color:#bb2126;border:1px solid #bb2126}
.btn-back:hover{background:#fde8e9}
.btn-refresh{background:#bb2126;color:#fff;border:1px solid #bb2126}
.btn-refresh:hover{background:#9e1c22;border-color:#9e1c22}
.btn-cache{background:#fff;color:#bb2126;border:1px solid #bb2126}
.btn-cache:hover{background:#fde8e9}
.btn-cache:disabled,.btn-refresh:disabled{opacity:.65;cursor:wait}
.hint{font-size:11px;color:#8b99a8;flex-shrink:0}
.frame-wrap{flex:1;min-height:0;background:#fff}
iframe{width:100%%;height:100%%;border:0;display:block}
</style>
</head>
<body>
<div class="bar">
<button type="button" class="btn-back" onclick="goBack()">← Quay lại</button>
<span class="bar-title">%s</span>
<span class="hint">Trang trắng/lỗi: F5 hoặc Xóa cache (giữ đăng nhập)</span>
<button type="button" class="btn-refresh" id="btn-refresh" onclick="refreshExam()">⟳ F5 Làm mới</button>
<button type="button" class="btn-cache" id="btn-cache" onclick="clearExamCache()">🗑 Xóa cache</button>
</div>
<div class="frame-wrap">
<iframe id="exam-frame" src="%s" title="exam-content"></iframe>
</div>
<script>
function goBack(){
  fetch('http://127.0.0.1:34115/exam-close').catch(function(){});
}
function reloadFrameSoft(){
  var f = document.getElementById('exam-frame');
  if (!f) return;
  try {
    f.contentWindow.location.reload();
  } catch (e) {
    var src = f.getAttribute('src') || f.src;
    f.src = 'about:blank';
    setTimeout(function(){ f.src = src; }, 50);
  }
}
function refreshExam(){
  var btn = document.getElementById('btn-refresh');
  if (btn) btn.disabled = true;
  reloadFrameSoft();
  setTimeout(function(){ if (btn) btn.disabled = false; }, 800);
}
async function clearExamCache(){
  var btn = document.getElementById('btn-cache');
  if (btn) btn.disabled = true;
  // Không xóa localStorage/sessionStorage/cookie — tránh văng đăng nhập trang thi.
  try {
    await fetch('http://127.0.0.1:34115/exam-clear-cache');
  } catch (e) {}
  // Chờ một nhịp để disk cache kịp xóa rồi reload cùng URL (không thêm query bust).
  setTimeout(function(){
    reloadFrameSoft();
    if (btn) btn.disabled = false;
  }, 400);
}
document.addEventListener('keydown', function(e){
  if (e.key === 'F5') {
    e.preventDefault();
    refreshExam();
  }
});
</script>
</body>
</html>`

const localBrowserHTML = `<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Trình duyệt Local — Simple Care</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Segoe UI,system-ui,sans-serif;background:#f5f7fa;color:#1a2332;min-height:100vh;display:flex;flex-direction:column}
.bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fff;border-bottom:1px solid #e4e9f0;flex-wrap:wrap}
.bar button{border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;font-weight:600;white-space:nowrap}
.btn-back{background:#fff;color:#bb2126;border:1px solid #bb2126}
.btn-back:hover{background:#fde8e9}
.btn-go{background:#bb2126;color:#fff;border:1px solid #bb2126}
.btn-go:hover{background:#9e1c22}
.addr{flex:1;min-width:220px;padding:8px 12px;border:1px solid #e4e9f0;border-radius:8px;font-size:13px}
.addr:focus{outline:2px solid rgba(187,33,38,.25);border-color:#bb2126}
.main{flex:1;padding:28px 20px;max-width:720px;margin:0 auto;width:100%%}
h1{font-size:1.35rem;margin-bottom:8px;color:#bb2126}
.lead{color:#5a6a7e;font-size:0.95rem;margin-bottom:18px;line-height:1.5}
.notice{background:#fde8e9;color:#9e1c22;border:1px solid #f5c2c4;border-radius:10px;padding:12px 14px;margin-bottom:10px;font-size:0.9rem}
.notice-sub{font-size:0.8rem;color:#8b99a8;margin-bottom:14px;word-break:break-all}
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
.chip{background:#fff;border:1px solid #e4e9f0;border-radius:999px;padding:6px 12px;font-size:12px;cursor:pointer;color:#bb2126;font-weight:600}
.chip:hover{background:#fde8e9;border-color:#bb2126}
.hint{margin-top:22px;font-size:12px;color:#8b99a8;line-height:1.5}
</style>
</head>
<body>
<div class="bar">
<button type="button" class="btn-back" onclick="goHome()">← Về trang chính</button>
<input class="addr" id="addr" type="url" placeholder="http://localhost:3000" spellcheck="false" />
<button type="button" class="btn-go" onclick="goLocal()">Mở</button>
</div>
<div class="main">
%s
<h1>Trình duyệt Local</h1>
<p class="lead">Chỉ dùng để xem bài làm chạy trên máy bạn (<strong>localhost</strong> / <strong>127.0.0.1</strong>). Mọi link online (GitHub, Google, …) đều bị chặn tại đây.</p>
<div class="chips">
<button type="button" class="chip" onclick="fill('http://localhost:3000')">localhost:3000</button>
<button type="button" class="chip" onclick="fill('http://localhost:5173')">localhost:5173</button>
<button type="button" class="chip" onclick="fill('http://127.0.0.1:8080')">127.0.0.1:8080</button>
<button type="button" class="chip" onclick="fill('http://localhost:5500')">localhost:5500</button>
</div>
<p class="hint">Mẹo: chạy dự án (npm run dev / Live Server), dán URL vào ô trên rồi bấm Mở. Về trang chính: nút ← hoặc menu Simple Care → Ctrl+H.</p>
</div>
<script>
function fill(u){ document.getElementById('addr').value = u; }
function goHome(){
  fetch('http://127.0.0.1:34115/exam-close').catch(function(){});
}
function isLoopback(raw){
  try {
    if (!raw) return false;
    if (raw.indexOf('://') < 0) raw = 'http://' + raw;
    var u = new URL(raw);
    if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
    var h = (u.hostname || '').toLowerCase();
    if (h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]') return true;
    if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
    return false;
  } catch (e) { return false; }
}
async function goLocal(){
  var raw = (document.getElementById('addr').value || '').trim();
  if (!raw) { alert('Nhập URL localhost'); return; }
  if (!isLoopback(raw)) {
    alert('Chỉ được mở localhost / 127.0.0.1');
    return;
  }
  if (raw.indexOf('://') < 0) raw = 'http://' + raw;
  try {
    var res = await fetch('http://127.0.0.1:34115/local-browser-go?url=' + encodeURIComponent(raw));
    if (!res.ok) {
      alert('Không mở được — chỉ localhost');
      return;
    }
  } catch (e) {
    location.href = raw;
  }
}
document.getElementById('addr').addEventListener('keydown', function(e){
  if (e.key === 'Enter') { e.preventDefault(); goLocal(); }
});
</script>
</body>
</html>`

// loadSession nạp session từ file cục bộ
func (a *App) loadSession() {
	a.mu.Lock()
	defer a.mu.Unlock()

	data, err := os.ReadFile(a.sessionPath)
	if err == nil {
		decrypted, errDec := decrypt(data)
		if errDec == nil {
			data = decrypted
		}
		var s StudentData
		if err := json.Unmarshal(data, &s); err == nil {
			a.student = &s
			log.Printf("[APP] Loaded local session: %s (%s)", s.FullName, s.StudentCode)

			go a.refreshStudentData()
			go a.fetchWifiPolicy()
			go a.syncProfileToServer(&s)
		}
	}
}

// saveSession lưu session ra file
func (a *App) saveSession(s *StudentData) {
	a.mu.Lock()
	defer a.mu.Unlock()

	a.student = s
	data, _ := json.MarshalIndent(s, "", "  ")
	encrypted, err := encrypt(data)
	if err == nil {
		_ = os.WriteFile(a.sessionPath, encrypted, 0644)
	} else {
		_ = os.WriteFile(a.sessionPath, data, 0644)
	}
	go a.syncProfileToServer(s)
}

func (a *App) syncProfileToServer(s *StudentData) {
	if s == nil || s.Avatar == "" {
		return
	}
	payload := map[string]any{
		"studentRkId": s.StudentID,
		"classRkId":   s.SystemID,
		"sessionDate": time.Now().Format("2006-01-02"),
		"avatar":      s.Avatar,
	}
	bodyBytes, _ := json.Marshal(payload)
	client := http.Client{Timeout: 5 * time.Second}
	resp, err := client.Post(API_BASE+"/api/student/sync-log", "application/json", bytes.NewBuffer(bodyBytes))
	if err != nil {
		return
	}
	_ = resp.Body.Close()
}

// loadStats nạp stats
func (a *App) loadStats() {
	a.mu.Lock()
	defer a.mu.Unlock()

	data, err := os.ReadFile(a.statsPath)
	if err == nil {
		var ls LocalStats
		if err := json.Unmarshal(data, &ls); err == nil {
			a.onlineSecs = ls.OnlineSecs
			a.offlineSecs = ls.OfflineSecs
			a.unsyncedOn = ls.UnsyncedOn
			a.unsyncedOff = ls.UnsyncedOff
		}
	}
}

// saveStats lưu stats ra file
func (a *App) saveStats() {
	a.mu.Lock()
	defer a.mu.Unlock()

	ls := LocalStats{
		OnlineSecs:  a.onlineSecs,
		OfflineSecs: a.offlineSecs,
		UnsyncedOn:  a.unsyncedOn,
		UnsyncedOff: a.unsyncedOff,
	}
	data, _ := json.Marshal(ls)
	_ = os.WriteFile(a.statsPath, data, 0644)
}

// CheckLoginStatus kiểm tra xem sinh viên đã đăng nhập chưa
func (a *App) CheckLoginStatus() bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.student != nil
}

// GetStudentInfo trả về thông tin sinh viên
func (a *App) GetStudentInfo() map[string]any {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.student == nil {
		return nil
	}
	return map[string]any{
		"studentId":   a.student.StudentID,
		"fullName":    a.student.FullName,
		"email":       a.student.Email,
		"avatar":      a.student.Avatar,
		"dob":         a.student.Dob,
		"studentCode": a.student.StudentCode,
		"phone":       a.student.Phone,
		"systemId":    a.student.SystemID,
	}
}

// NavigateToLogin chuyển WebView đến trang đăng nhập Rikkei Portal
func (a *App) NavigateToLogin() {
	a.setLocalBrowserActive(false)
	a.mu.Lock()
	a.expectingLogin = true
	a.mu.Unlock()
	guard.SuppressFor(5 * time.Second)
	runtime.WindowExecJS(a.ctx, "window.location.href = 'https://portal.rikkei.edu.vn/dangnhap'")
}

// Logout đăng xuất sinh viên
func (a *App) Logout() {
	a.mu.Lock()
	a.student = nil
	a.expectingLogin = true
	a.needsClearPortalStorage = true
	a.mu.Unlock()

	_ = os.Remove(a.sessionPath)
	_ = os.Remove(a.statsPath)

	a.mu.Lock()
	a.onlineSecs = 0
	a.offlineSecs = 0
	a.unsyncedOn = 0
	a.unsyncedOff = 0
	a.mu.Unlock()

	blocker.Instance.Stop()
	a.disconnectWS()
	a.stopScreenshotStream()
	a.stopWebcamStream()

	guard.SuppressFor(5 * time.Second)
	runtime.WindowExecJS(a.ctx, "window.location.href = 'https://portal.rikkei.edu.vn/dangnhap'")
}

// GetStats trả về Wifi, thời gian online/offline và trạng thái giám sát
func (a *App) GetStats() map[string]any {
	a.mu.Lock()
	defer a.mu.Unlock()

	shifts := make([]StudentShiftSnapshot, len(a.dashboard.Shifts))
	copy(shifts, a.dashboard.Shifts)
	for i := range shifts {
		if shifts[i].IsActiveNow {
			shifts[i].OnlineSeconds += a.unsyncedOn
			shifts[i].OfflineSeconds += a.unsyncedOff
		}
	}

	return map[string]any{
		"wifiSSID":          a.wifiSSID,
		"onlineSecs":        a.onlineSecs,
		"offlineSecs":       a.offlineSecs,
		"wsConnected":       a.wsConnected,
		"serverReachable":   a.backendOnline,
		"allowedApps":       a.allowedApps,
		"statusMsg":         a.statusMsg,
		"sessionDate":       a.dashboard.SessionDate,
		"monitorMode":       a.dashboard.MonitorMode,
		"monitorLabel":      a.dashboard.MonitorLabel,
		"className":         a.dashboard.ClassName,
		"classCode":         a.dashboard.ClassCode,
		"currentPeriod":     a.dashboard.CurrentPeriod,
		"currentCourseName": a.dashboard.CurrentCourseName,
		"currentShiftStart": a.dashboard.CurrentShiftStart,
		"currentShiftEnd":   a.dashboard.CurrentShiftEnd,
		"inScheduleNow":     a.dashboard.InScheduleNow,
		"blockerActive":     a.dashboard.BlockerActive,
		"shifts":            shifts,
		"exam":              a.dashboard.Exam,
	}
}

// safeWriteWS writes a message to the WebSocket connection thread-safely
func (a *App) safeWriteWS(msg any) error {
	a.mu.Lock()
	conn := a.wsConn
	a.mu.Unlock()

	if conn == nil {
		return errors.New("websocket connection is nil")
	}

	a.wsWriteMu.Lock()
	defer a.wsWriteMu.Unlock()
	return conn.WriteJSON(msg)
}

// SendWebcamFrame truyền webcam frame từ JS frontend lên máy chủ qua WS
func (a *App) SendWebcamFrame(frameBase64 string) {
	if !a.isMonitoringActive() {
		return
	}
	_ = a.safeWriteWS(map[string]any{
		"event": "webcam_stream_frame",
		"data": map[string]any{
			"imageBuffer": frameBase64,
		},
	})
}

// authStorageScanner chỉ quét localStorage khi người dùng chủ động mở trang đăng nhập
func (a *App) authStorageScanner() {
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	for {
		<-ticker.C

		a.mu.Lock()
		expecting := a.expectingLogin
		needsClear := a.needsClearPortalStorage
		loggedIn := a.student != nil
		a.mu.Unlock()

		if needsClear && expecting {
			runtime.WindowExecJS(a.ctx, `
				try {
					localStorage.removeItem("student");
					localStorage.removeItem("token");
				} catch(e) {}
			`)
			a.mu.Lock()
			a.needsClearPortalStorage = false
			a.mu.Unlock()
			continue
		}

		if loggedIn || !expecting {
			continue
		}

		runtime.WindowExecJS(a.ctx, `
			try {
				const st = localStorage.getItem("student");
				if (st && !window.__redirecting) {
					window.__redirecting = true;
					window.location.href = "http://127.0.0.1:34115/login-success?data=" + encodeURIComponent(st);
				}
			} catch(e) {}
		`)
	}
}

// monitorLoop định kỳ 10s: check wifi, ping backend, cập nhật online/offline seconds và đồng bộ lên server
func (a *App) monitorLoop() {
	for {
		a.runMonitorTick()

		a.mu.Lock()
		wifiEmpty := a.wifiSSID == "" || strings.Contains(a.wifiSSID, "Chưa cấp quyền") || strings.Contains(a.wifiSSID, "redacted")
		expecting := a.expectingLogin
		a.mu.Unlock()

		sleepInterval := 10 * time.Second
		if wifiEmpty && !expecting {
			sleepInterval = 2 * time.Second
		}

		time.Sleep(sleepInterval)
	}
}

func (a *App) runMonitorTick() {
	if !a.isMonitoringActive() || !a.CheckLoginStatus() {
		return
	}

	wifi := winapi.GetWifiConnection()
	a.mu.Lock()
	if strings.Contains(strings.ToLower(wifi.SSID), "redacted") {
		a.wifiSSID = "Chưa cấp quyền Vị Trí (macOS)"
	} else {
		a.wifiSSID = wifi.SSID
	}
	if strings.Contains(strings.ToLower(wifi.BSSID), "redacted") {
		a.wifiBSSID = "Chưa cấp quyền Vị Trí (macOS)"
	} else {
		a.wifiBSSID = wifi.BSSID
	}
	a.mu.Unlock()

	backendOnline := a.pingBackend()

	if backendOnline && wifi.SSID != "" && wifi.BSSID != "" {
		go a.reportWifi(wifi.SSID, wifi.BSSID)
		a.fetchWifiPolicy()
		if !a.isWifiAllowed(wifi.SSID, wifi.BSSID) {
			a.rejectUnauthorizedWifi(wifi.SSID, wifi.BSSID)
			return
		}
	}

	a.mu.Lock()
	a.backendOnline = backendOnline
	if backendOnline {
		a.onlineSecs += 10
		a.unsyncedOn += 10
	} else {
		a.offlineSecs += 10
		a.unsyncedOff += 10
	}
	a.mu.Unlock()

	a.saveStats()

	if backendOnline {
		a.connectWS()
		a.syncLogsToServer()
	} else {
		a.disconnectWS()
	}
}

// refreshNetworkStatus — đọc WiFi + ping server ngay (không cộng thời gian online/offline).
func (a *App) resolveClassRkID() int64 {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.dashboard.ClassRkID > 0 {
		return a.dashboard.ClassRkID
	}
	return 0
}

func (a *App) refreshStudentData() {
	a.mu.Lock()
	student := a.student
	a.mu.Unlock()
	if student == nil {
		return
	}
	a.fetchStudentStatus(student.StudentID)
	classID := a.resolveClassRkID()
	if classID > 0 {
		a.fetchAllowedApps(classID)
	} else {
		a.fetchAllowedApps(0)
	}
	a.refreshNetworkStatus()
}

// refreshNetworkStatus — đọc WiFi + ping server ngay (không cộng thời gian online/offline).
func (a *App) refreshNetworkStatus() {
	if !a.isMonitoringActive() || !a.CheckLoginStatus() {
		return
	}
	wifi := winapi.GetWifiConnection()
	backendOnline := a.pingBackend()
	a.mu.Lock()
	if strings.Contains(strings.ToLower(wifi.SSID), "redacted") {
		a.wifiSSID = "Chưa cấp quyền Vị Trí (macOS)"
	} else {
		a.wifiSSID = wifi.SSID
	}
	if strings.Contains(strings.ToLower(wifi.BSSID), "redacted") {
		a.wifiBSSID = "Chưa cấp quyền Vị Trí (macOS)"
	} else {
		a.wifiBSSID = wifi.BSSID
	}
	a.backendOnline = backendOnline
	a.mu.Unlock()
	if backendOnline {
		a.connectWS()
	}
}

func (a *App) pingBackend() bool {
	client := http.Client{Timeout: 3 * time.Second}
	resp, err := client.Get(API_BASE + "/api/health")
	if err != nil {
		return false
	}
	_ = resp.Body.Close()
	return resp.StatusCode == http.StatusOK
}

func (a *App) syncLogsToServer() {
	if !a.isMonitoringActive() {
		return
	}
	a.mu.Lock()
	student := a.student
	unsyncedOn := a.unsyncedOn
	unsyncedOff := a.unsyncedOff
	ssid := a.wifiSSID
	a.mu.Unlock()

	if student == nil {
		return
	}

	go func() {
		a.fetchStudentStatus(student.StudentID)
		classID := a.resolveClassRkID()
		if classID <= 0 {
			classID = student.SystemID
		}
		a.fetchAllowedApps(classID)
	}()

	payload := map[string]any{
		"studentRkId":       student.StudentID,
		"classRkId":         a.resolveClassRkID(),
		"sessionDate":       time.Now().Format("2006-01-02"),
		"addOnlineSeconds":  unsyncedOn,
		"addOfflineSeconds": unsyncedOff,
		"wifiSsid":          ssid,
		"avatar":            student.Avatar,
	}

	bodyBytes, _ := json.Marshal(payload)
	client := http.Client{Timeout: 5 * time.Second}
	resp, err := client.Post(API_BASE+"/api/student/sync-log", "application/json", bytes.NewBuffer(bodyBytes))
	if err != nil {
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		a.mu.Lock()
		a.unsyncedOn -= unsyncedOn
		a.unsyncedOff -= unsyncedOff
		a.mu.Unlock()
		a.saveStats()
		log.Printf("[SYNC] Synced %ds online, %ds offline to server.", unsyncedOn, unsyncedOff)
	}
}

func (a *App) reportBlockedApp(procName string, title string) {
	if !a.isMonitoringActive() {
		return
	}
	a.mu.Lock()
	student := a.student
	a.mu.Unlock()
	if student == nil || student.StudentID <= 0 {
		return
	}

	procKey := strings.ToLower(strings.TrimSpace(procName))
	if procKey == "" {
		return
	}
	if v, ok := blockedReportLast.Load(procKey); ok {
		if time.Since(v.(time.Time)) < 20*time.Second {
			return
		}
	}
	blockedReportLast.Store(procKey, time.Now())

	payload := map[string]any{
		"studentRkId": student.StudentID,
		"processName": procName,
		"windowTitle": title,
	}
	bodyBytes, _ := json.Marshal(payload)
	client := http.Client{Timeout: 4 * time.Second}
	resp, err := client.Post(API_BASE+"/api/student/report-blocked-app", "application/json", bytes.NewBuffer(bodyBytes))
	if err != nil {
		log.Printf("[CLIENT] report-blocked-app failed: %v", err)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		log.Printf("[CLIENT] report-blocked-app status %d: %s", resp.StatusCode, string(body))
		return
	}
	log.Printf("[CLIENT] report-blocked-app ok: %s", procName)
}

func (a *App) reportWifi(ssid, bssid string) {
	if !a.isMonitoringActive() {
		return
	}
	a.mu.Lock()
	student := a.student
	a.mu.Unlock()
	if student == nil || strings.TrimSpace(ssid) == "" || strings.TrimSpace(bssid) == "" {
		return
	}
	key := winapi.BSSIDKey(bssid)
	if len(key) != 12 {
		return
	}
	if v, ok := wifiReportLast.Load(key); ok {
		if time.Since(v.(time.Time)) < 30*time.Second {
			return
		}
	}
	wifiReportLast.Store(key, time.Now())

	payload := map[string]any{
		"studentRkId": student.StudentID,
		"ssid":        ssid,
		"bssid":       bssid,
	}
	bodyBytes, _ := json.Marshal(payload)
	client := http.Client{Timeout: 4 * time.Second}
	resp, err := client.Post(API_BASE+"/api/student/report-wifi", "application/json", bytes.NewBuffer(bodyBytes))
	if err != nil {
		return
	}
	_ = resp.Body.Close()
}

func (a *App) fetchWifiPolicy() {
	client := http.Client{Timeout: 4 * time.Second}
	resp, err := client.Get(API_BASE + "/api/student/wifi-policy")
	if err != nil {
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return
	}
	var res struct {
		Enforce  bool `json:"enforce"`
		Accepted []struct {
			SSID  string `json:"ssid"`
			BSSID string `json:"bssid"`
		} `json:"accepted"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
		return
	}
	allowed := make(map[string]bool, len(res.Accepted))
	for _, item := range res.Accepted {
		k := winapi.BSSIDKey(item.BSSID)
		if len(k) == 12 {
			allowed[k] = true
		}
	}
	a.mu.Lock()
	a.wifiEnforce = res.Enforce
	a.acceptedWifi = allowed
	a.mu.Unlock()
}

func (a *App) isWifiAllowed(ssid, bssid string) bool {
	a.mu.Lock()
	enforce := a.wifiEnforce
	allowed := a.acceptedWifi
	a.mu.Unlock()

	if !enforce || len(allowed) == 0 {
		return true
	}
	// Bypass Wi-Fi check if it's redacted by macOS privacy controls
	if strings.Contains(strings.ToLower(ssid), "redacted") || strings.Contains(strings.ToLower(bssid), "redacted") {
		return true
	}
	bKey := winapi.BSSIDKey(bssid)
	if bKey == "" || len(bKey) != 12 {
		return false
	}
	return allowed[bKey]
}

func (a *App) rejectUnauthorizedWifi(ssid, bssid string) {
	a.mu.Lock()
	if a.wifiRejected {
		a.mu.Unlock()
		return
	}
	a.wifiRejected = true
	a.mu.Unlock()

	a.reportViolation("wifi", fmt.Sprintf("WiFi không được phép: %s (%s)", ssid, bssid))
	a.showQuitDialog(
		"WiFi không được phép",
		fmt.Sprintf("Điểm phát WiFi \"%s\" (%s) không được phép.\n\nCó thể là mạng giả mạo (hotspot trùng tên). Hãy kết nối đúng WiFi của trường rồi mở lại ứng dụng.", ssid, bssid),
	)
}

func (a *App) fetchAllowedApps(classId int64) {
	a.fetchAppsMu.Lock()
	defer a.fetchAppsMu.Unlock()

	a.mu.Lock()
	studentID := int64(0)
	if a.student != nil {
		studentID = a.student.StudentID
	}
	a.mu.Unlock()

	url := fmt.Sprintf("%s/api/classes/%d/allowed-apps?studentId=%d", API_BASE, classId, studentID)
	client := http.Client{Timeout: 4 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		var res struct {
			Keywords string `json:"keywords"`
			Exit     bool   `json:"exit"`
			Reason   string `json:"reason"`
		}
		if err := json.NewDecoder(resp.Body).Decode(&res); err == nil {
			if res.Exit {
				a.mu.Lock()
				stillExam := a.dashboard.MonitorMode == "exam"
				a.mu.Unlock()
				if stillExam {
					return
				}
				reason := res.Reason
				if reason == "" {
					reason = "Không đáp ứng điều kiện học tập (chưa cấu hình ứng dụng được phép hoặc ngoài giờ học)."
				}
				log.Printf("[CLIENT] Conditions not met: %s. Blocker suspended.", reason)
				
				a.mu.Lock()
				a.allowedApps = ""
				a.statusMsg = reason
				a.mu.Unlock()
				
				blocker.Instance.Stop()
				return
			}
			a.mu.Lock()
			a.allowedApps = res.Keywords
			if a.dashboard.MonitorLabel != "" {
				a.statusMsg = a.dashboard.MonitorLabel
			} else {
				a.statusMsg = "Đang trong giờ học"
			}
			a.mu.Unlock()
			
			log.Printf("[CLIENT] Allowed apps fetched from server: %s", res.Keywords)
			blocker.Instance.SetKeywords(res.Keywords)
			blocker.Instance.Start()
		}
	}
}

func (a *App) fetchStudentStatus(studentID int64) {
	if studentID <= 0 {
		return
	}
	client := http.Client{Timeout: 4 * time.Second}
	resp, err := client.Get(fmt.Sprintf("%s/api/student/status?studentRkId=%d", API_BASE, studentID))
	if err != nil {
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return
	}

	var res StudentDashboardSnapshot
	if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
		return
	}

	a.mu.Lock()
	prevMode := a.dashboard.MonitorMode
	a.dashboard = res
	if res.MonitorLabel != "" {
		a.statusMsg = res.MonitorLabel
	}
	if res.ClassName != "" && res.MonitorMode == "learning" {
		a.statusMsg = fmt.Sprintf("%s · %s", res.ClassName, res.MonitorLabel)
	} else if res.ClassName != "" && res.MonitorMode == "outside_schedule" {
		a.statusMsg = fmt.Sprintf("%s · Ngoài giờ học", res.ClassName)
	} else if res.ClassName != "" && res.MonitorMode == "not_configured" {
		a.statusMsg = fmt.Sprintf("%s · %s", res.ClassName, res.MonitorLabel)
	} else if res.MonitorMode == "exam" {
		a.statusMsg = res.MonitorLabel
	}
	a.mu.Unlock()

	if prevMode != res.MonitorMode {
		guard.SuppressFor(4 * time.Second)
	}
}

func (a *App) connectWS() {
	if !a.CheckLoginStatus() {
		return
	}
	a.mu.Lock()
	if a.wsConnected || a.wsConnecting || a.student == nil {
		a.mu.Unlock()
		return
	}
	a.wsConnecting = true
	student := a.student
	classID := a.dashboard.ClassRkID
	if classID <= 0 {
		classID = student.SystemID
	}
	backoff := a.wsBackoff
	if backoff <= 0 {
		backoff = 500 * time.Millisecond
	}
	a.mu.Unlock()

	wsUrl := fmt.Sprintf("%s/ws?role=student&studentId=%d&classId=%d", getWsUrl(API_BASE), student.StudentID, classID)
	dialer := websocket.Dialer{HandshakeTimeout: 4 * time.Second}
	conn, _, err := dialer.Dial(wsUrl, nil)
	if err != nil {
		a.mu.Lock()
		a.wsConnecting = false
		delay := backoff
		next := delay * 2
		if next > 10*time.Second {
			next = 10 * time.Second
		}
		a.wsBackoff = next
		a.mu.Unlock()
		log.Printf("[WS] Dial failed: %v — retry in %v", err, delay)
		time.AfterFunc(delay, func() {
			if a.isMonitoringActive() && a.CheckLoginStatus() {
				a.connectWS()
			}
		})
		return
	}

	_ = conn.SetReadDeadline(time.Now().Add(45 * time.Second))
	conn.SetPongHandler(func(string) error {
		return conn.SetReadDeadline(time.Now().Add(45 * time.Second))
	})

	a.mu.Lock()
	a.wsConn = conn
	a.wsConnected = true
	a.wsConnecting = false
	a.wsBackoff = 500 * time.Millisecond
	a.mu.Unlock()

	log.Println("[WS] Connected to proctor websocket hub.")
	a.markRunLock()

	pingDone := make(chan struct{})
	go func() {
		ticker := time.NewTicker(15 * time.Second)
		defer ticker.Stop()
		for {
			select {
			case <-pingDone:
				return
			case <-ticker.C:
				a.wsWriteMu.Lock()
				err := conn.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(5*time.Second))
				a.wsWriteMu.Unlock()
				if err != nil {
					return
				}
				// App-level ping (một số proxy không forward WS control frames đúng).
				_ = a.safeWriteWS(map[string]any{"event": "client:ping", "data": map[string]any{}})
			}
		}
	}()

	go func() {
		defer close(pingDone)
		defer func() {
			a.disconnectWS(conn)
			a.mu.Lock()
			delay := a.wsBackoff
			if delay <= 0 {
				delay = 500 * time.Millisecond
			}
			next := delay * 2
			if next > 10*time.Second {
				next = 10 * time.Second
			}
			a.wsBackoff = next
			a.mu.Unlock()
			log.Printf("[WS] Disconnected — reconnect in %v", delay)
			time.AfterFunc(delay, func() {
				if a.isMonitoringActive() && a.CheckLoginStatus() {
					a.connectWS()
				}
			})
		}()
		for {
			var msg struct {
				Event string         `json:"event"`
				Data  map[string]any `json:"data"`
			}
			err := conn.ReadJSON(&msg)
			if err != nil {
				log.Printf("[WS] ReadJSON error: %v", err)
				break
			}
			_ = conn.SetReadDeadline(time.Now().Add(45 * time.Second))
			if msg.Event == "client:pong" {
				continue
			}
			log.Printf("[WS] Received event: %s", msg.Event)

			switch msg.Event {
			case "start_screenshot_stream":
				a.startScreenshotStream()
			case "stop_screenshot_stream":
				a.stopScreenshotStream()
			case "start_webcam_stream":
				a.startWebcamStream()
			case "stop_webcam_stream":
				a.stopWebcamStream()
			case "chat:message":
				senderRole, _ := msg.Data["senderRole"].(string)
				if senderRole == "staff" {
					from := "Giảng viên"
					if n, ok := msg.Data["staffName"].(string); ok && n != "" {
						from = n
					}
					preview := ""
					if b, ok := msg.Data["body"].(string); ok {
						preview = b
					}
					if staffVal, ok := msg.Data["staffId"].(float64); ok {
						a.mu.Lock()
						a.replyStaffID = uint(staffVal)
						a.chatUnread++
						a.mu.Unlock()
					}
					a.alertChatIncoming(from, preview)
				}
				runtime.EventsEmit(a.ctx, "chat:message", msg.Data)
			case "exam:paper-sent":
				runtime.EventsEmit(a.ctx, "exam:paper-sent", msg.Data)
				a.mu.Lock()
				if a.dashboard.Exam != nil {
					a.dashboard.Exam.PaperSent = true
					if t, ok := msg.Data["paperTitle"].(string); ok {
						a.dashboard.Exam.PaperTitle = t
					}
				}
				a.mu.Unlock()
			}
		}
	}()
}

func (a *App) disconnectWS(expectedConn ...*websocket.Conn) {
	var shouldStopStreams bool

	a.mu.Lock()
	if len(expectedConn) > 0 && expectedConn[0] != nil {
		if a.wsConn != expectedConn[0] {
			a.mu.Unlock()
			return
		}
	}
	if a.wsConn != nil {
		_ = a.wsConn.Close()
		a.wsConn = nil
	}
	a.wsConnected = false
	a.wsConnecting = false
	shouldStopStreams = a.isStreamingSc || a.isStreamingCam
	a.mu.Unlock()

	if shouldStopStreams {
		a.stopScreenshotStream()
		a.stopWebcamStream()
	}
}

func (a *App) startScreenshotStream() {
	a.mu.Lock()
	if a.isStreamingSc {
		a.mu.Unlock()
		return
	}
	a.isStreamingSc = true
	a.streamScStop = make(chan struct{})
	a.mu.Unlock()

	go func() {
		ticker := time.NewTicker(250 * time.Millisecond)
		defer ticker.Stop()

		for {
			select {
			case <-ticker.C:
				frame, err := screen.CaptureScreen()
				if err != nil {
					continue
				}

				_ = a.safeWriteWS(map[string]any{
					"event": "screenshot_stream_frame",
					"data": map[string]any{
						"imageBuffer": frame,
					},
				})
			case <-a.streamScStop:
				return
			}
		}
	}()
	log.Println("[WS] Screenshot screen-streaming started.")
}

func (a *App) stopScreenshotStream() {
	a.mu.Lock()
	defer a.mu.Unlock()

	if !a.isStreamingSc {
		return
	}
	a.isStreamingSc = false
	close(a.streamScStop)
	log.Println("[WS] Screenshot screen-streaming stopped.")
}

func (a *App) startWebcamStream() {
	a.mu.Lock()
	if a.isStreamingCam {
		a.mu.Unlock()
		return
	}
	a.isStreamingCam = true
	a.streamCamStop = make(chan struct{})
	a.mu.Unlock()

	if !camera.IsNative {
		log.Println("[WS] Platform is non-darwin, delegating webcam stream to frontend events")
		runtime.EventsEmit(a.ctx, "start_webcam_stream")
		return
	}

	if err := camera.StartCapture(); err != nil {
		log.Printf("[WS] Failed to start native camera: %v", err)
		a.mu.Lock()
		a.isStreamingCam = false
		a.mu.Unlock()
		return
	}

	go func() {
		ticker := time.NewTicker(250 * time.Millisecond)
		defer ticker.Stop()
		emptyCount := 0
		sentCount := 0

		for {
			select {
			case <-ticker.C:
				frame := camera.GetFrame()
				if frame == "" {
					emptyCount++
					if emptyCount <= 20 || emptyCount%100 == 0 {
						log.Printf("[WS] Webcam: no frame available (empty count: %d)", emptyCount)
					}
					continue
				}
				emptyCount = 0
				sentCount++

				err := a.safeWriteWS(map[string]any{
					"event": "webcam_stream_frame",
					"data": map[string]any{
						"imageBuffer": frame,
					},
				})
				if sentCount <= 5 {
					log.Printf("[WS] Webcam frame #%d sent (len=%d, err=%v)", sentCount, len(frame), err)
				}
			case <-a.streamCamStop:
				return
			}
		}
	}()
	log.Println("[WS] Webcam native streaming started.")
}

func (a *App) stopWebcamStream() {
	a.mu.Lock()
	defer a.mu.Unlock()

	if !a.isStreamingCam {
		return
	}
	a.isStreamingCam = false
	if a.streamCamStop != nil {
		close(a.streamCamStop)
	}

	if !camera.IsNative {
		log.Println("[WS] Platform is non-darwin, stopping frontend webcam stream via event")
		runtime.EventsEmit(a.ctx, "stop_webcam_stream")
		return
	}

	camera.StopCapture()
	log.Println("[WS] Webcam native streaming stopped.")
}

func (a *App) alertChatIncoming(from, preview string) {
	if preview == "" {
		preview = "Bạn có tin nhắn mới"
	}
	if len(preview) > 120 {
		preview = preview[:120] + "…"
	}
	winapi.PlayNotifySound()
	winapi.ActivateAppWindow("Simple Care")
	runtime.WindowUnminimise(a.ctx)
	runtime.WindowShow(a.ctx)
	a.mu.Lock()
	unread := a.chatUnread
	staffID := a.replyStaffID
	a.mu.Unlock()
	runtime.EventsEmit(a.ctx, "chat:notify", map[string]any{
		"unread":  unread,
		"preview": preview,
		"from":    from,
		"staffId": staffID,
	})
}

// UnlockChatAudio — gọi từ UI sau lần click đầu để mở khóa Web Audio (dự phòng)
func (a *App) UnlockChatAudio() {}

func (a *App) GetChatUnread() int {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.chatUnread
}

func (a *App) ClearChatUnread() {
	a.mu.Lock()
	a.chatUnread = 0
	a.mu.Unlock()
}

func (a *App) GetChatConversations() ([]map[string]any, error) {
	a.mu.Lock()
	student := a.student
	a.mu.Unlock()
	if student == nil {
		return nil, errors.New("chưa đăng nhập")
	}
	url := fmt.Sprintf("%s/api/student/chat/conversations?studentRkId=%d", API_BASE, student.StudentID)
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var payload struct {
		Data []map[string]any `json:"data"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return nil, err
	}
	return payload.Data, nil
}

func (a *App) GetChatMessages(staffID uint) ([]map[string]any, error) {
	a.mu.Lock()
	student := a.student
	a.mu.Unlock()
	if student == nil {
		return nil, errors.New("chưa đăng nhập")
	}
	url := fmt.Sprintf("%s/api/student/chat/messages?studentRkId=%d&staffId=%d", API_BASE, student.StudentID, staffID)
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var payload struct {
		Data         []map[string]any `json:"data"`
		ReplyStaffID uint             `json:"replyStaffId"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return nil, err
	}
	if staffID > 0 {
		a.mu.Lock()
		a.replyStaffID = staffID
		a.mu.Unlock()
	} else if payload.ReplyStaffID > 0 {
		a.mu.Lock()
		a.replyStaffID = payload.ReplyStaffID
		a.mu.Unlock()
	}
	a.ClearChatUnread()
	return payload.Data, nil
}

func (a *App) SendChatMessage(body string) error {
	body = strings.TrimSpace(body)
	if body == "" {
		return errors.New("tin nhắn trống")
	}
	a.mu.Lock()
	student := a.student
	staffID := a.replyStaffID
	a.mu.Unlock()
	if student == nil {
		return errors.New("chưa đăng nhập")
	}
	payload := map[string]any{
		"studentRkId": student.StudentID,
		"staffId":     staffID,
		"body":        body,
	}
	b, _ := json.Marshal(payload)
	resp, err := http.Post(API_BASE+"/api/student/chat/messages", "application/json", bytes.NewReader(b))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		var errBody map[string]any
		_ = json.NewDecoder(resp.Body).Decode(&errBody)
		if msg, ok := errBody["error"].(string); ok {
			return errors.New(msg)
		}
		return fmt.Errorf("gửi tin thất bại (%d)", resp.StatusCode)
	}
	return nil
}

func (a *App) examStudentContext() (int64, *StudentExamSnapshot, error) {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.student == nil {
		return 0, nil, errors.New("chưa đăng nhập")
	}
	if a.dashboard.Exam == nil {
		return 0, nil, errors.New("không trong giờ thi")
	}
	return a.student.StudentID, a.dashboard.Exam, nil
}

// ReturnToDashboard quay lại giao diện chính của app (giữ cookie WebView2).
func (a *App) ReturnToDashboard() {
	a.setLocalBrowserActive(false)
	guard.SuppressFor(5 * time.Second)
	runtime.WindowReloadApp(a.ctx)
}

func (a *App) setLocalBrowserActive(on bool) {
	a.mu.Lock()
	a.localBrowserActive = on
	a.mu.Unlock()
}

func isLoopbackURL(raw string) bool {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return false
	}
	if !strings.Contains(raw, "://") {
		raw = "http://" + raw
	}
	u, err := url.Parse(raw)
	if err != nil || u.Host == "" {
		return false
	}
	scheme := strings.ToLower(u.Scheme)
	if scheme != "http" && scheme != "https" {
		return false
	}
	host := strings.ToLower(u.Hostname())
	if host == "localhost" {
		return true
	}
	ip := net.ParseIP(host)
	return ip != nil && ip.IsLoopback()
}

func (a *App) localBrowserGuardLoop() {
	ticker := time.NewTicker(500 * time.Millisecond)
	defer ticker.Stop()
	for range ticker.C {
		a.mu.Lock()
		active := a.localBrowserActive && a.ctx != nil && !a.monitoringTornDown
		ctx := a.ctx
		a.mu.Unlock()
		if !active {
			continue
		}
		// Nếu hostname không phải loopback → đá về trang local-browser
		runtime.WindowExecJS(ctx, `
			(function(){
				try {
					var h = (location.hostname || '').toLowerCase();
					var ok = h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]' || /^127\.\d+\.\d+\.\d+$/.test(h);
					if (!ok) {
						location.href = 'http://127.0.0.1:34115/local-browser?blocked=1&from=' + encodeURIComponent(location.href);
					}
				} catch (e) {}
			})();
		`)
	}
}

// OpenGitHub mở github.com trong WebView (về trang chính: Ctrl+H).
func (a *App) OpenGitHub() {
	a.setLocalBrowserActive(false)
	guard.SuppressFor(5 * time.Second)
	runtime.WindowExecJS(a.ctx, `window.location.href = 'https://github.com'`)
}

// OpenGoogleTranslate mở Google Dịch trong WebView.
func (a *App) OpenGoogleTranslate() {
	a.setLocalBrowserActive(false)
	guard.SuppressFor(5 * time.Second)
	runtime.WindowExecJS(a.ctx, `window.location.href = 'https://translate.google.com/?sl=auto&tl=vi'`)
}

// OpenLocalBrowser mở trình duyệt chỉ cho phép localhost (test bài làm local).
func (a *App) OpenLocalBrowser() {
	a.setLocalBrowserActive(true)
	guard.SuppressFor(5 * time.Second)
	runtime.WindowExecJS(a.ctx, `window.location.href = 'http://127.0.0.1:34115/local-browser'`)
}

// ReloadExamPage — F5: làm mới trang / iframe đang mở (trắc nghiệm).
func (a *App) ReloadExamPage() {
	guard.SuppressFor(3 * time.Second)
	runtime.WindowExecJS(a.ctx, `
		(function(){
			try {
				if (typeof window.refreshExam === 'function') { window.refreshExam(); return; }
				var f = document.getElementById('exam-frame');
				if (f) {
					try { f.contentWindow.location.reload(); }
					catch (e) { var s = f.src; f.src = 'about:blank'; setTimeout(function(){ f.src = s; }, 50); }
					return;
				}
			} catch (e) {}
			location.reload();
		})();
	`)
}

// ClearExamBrowserCache — chỉ xóa HTTP disk cache, giữ cookie/localStorage (không văng đăng nhập trang thi).
func (a *App) ClearExamBrowserCache() {
	guard.SuppressFor(8 * time.Second)
	a.purgeWebViewDiskCache()
	runtime.WindowExecJS(a.ctx, `
		(function(){
			try {
				if (typeof window.reloadFrameSoft === 'function') {
					window.reloadFrameSoft();
					return;
				}
				var f = document.getElementById('exam-frame');
				if (f) {
					try { f.contentWindow.location.reload(); }
					catch (e) {
						var s = f.getAttribute('src') || f.src;
						f.src = 'about:blank';
						setTimeout(function(){ f.src = s; }, 50);
					}
					return;
				}
			} catch (e) {}
			location.reload();
		})();
	`)
}

func (a *App) purgeWebViewDiskCache() {
	if a.webviewDataPath == "" {
		return
	}
	// Chỉ Cache tải tài nguyên — KHÔNG xóa Cookies / Local Storage / Service Worker (giữ phiên đăng nhập).
	subs := []string{
		filepath.Join("EBWebView", "Default", "Cache"),
		filepath.Join("EBWebView", "Default", "Code Cache"),
		filepath.Join("EBWebView", "Default", "GPUCache"),
	}
	for _, sub := range subs {
		path := filepath.Join(a.webviewDataPath, sub)
		if err := os.RemoveAll(path); err != nil {
			log.Printf("[CACHE] purge %s: %v", path, err)
		} else {
			log.Printf("[CACHE] purged %s", path)
		}
	}
}

func isLocalExamURL(raw string) bool {
	u, err := url.Parse(strings.TrimSpace(raw))
	if err != nil || u.Host == "" {
		return false
	}
	host := strings.ToLower(u.Hostname())
	return host == "127.0.0.1" || host == "localhost"
}

func (a *App) openExamWebView(targetURL, title string) error {
	targetURL = strings.TrimSpace(targetURL)
	if targetURL == "" {
		return errors.New("không có nội dung để mở")
	}
	a.setLocalBrowserActive(false)
	// URL local (PDF/resource): khung exam-view. URL ngoài (quiz): mở top-level giữ first-party cookies/session.
	if isLocalExamURL(targetURL) {
		wrapper := fmt.Sprintf(
			"http://127.0.0.1:34115/exam-view?url=%s&title=%s",
			url.QueryEscape(targetURL),
			url.QueryEscape(title),
		)
		guard.SuppressFor(5 * time.Second)
		runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", wrapper))
		return nil
	}
	guard.SuppressFor(5 * time.Second)
	runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", targetURL))
	return nil
}

func (a *App) GetExamPaperViewURL() (string, error) {
	studentID, exam, err := a.examStudentContext()
	if err != nil {
		return "", err
	}
	if !exam.PaperSent {
		return "", errors.New("Giảng viên chưa gửi đề")
	}
	return fmt.Sprintf("%s/api/student/exam/download?studentRkId=%d&kind=pdf&view=1", API_BASE, studentID), nil
}

func (a *App) GetExamQuizViewURL() (string, error) {
	_, exam, err := a.examStudentContext()
	if err != nil {
		return "", err
	}
	quizURL := strings.TrimSpace(exam.QuizURL)
	if quizURL == "" {
		return "", errors.New("Phòng thi chưa cấu hình link trắc nghiệm")
	}
	return quizURL, nil
}

func (a *App) GetExamPaperFiles() (map[string]any, error) {
	studentID, exam, err := a.examStudentContext()
	if err != nil {
		return nil, err
	}
	if !exam.PaperSent {
		return nil, errors.New("Giảng viên chưa gửi đề")
	}
	apiURL := fmt.Sprintf("%s/api/student/exam/paper-files?studentRkId=%d", API_BASE, studentID)
	resp, err := http.Get(apiURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New("không tải được danh sách tài nguyên")
	}
	var payload map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return nil, err
	}
	base := API_BASE
	if pdf, ok := payload["pdfUrl"].(string); ok && pdf != "" && !strings.HasPrefix(pdf, "http") {
		payload["pdfUrl"] = base + pdf
	}
	if raw, ok := payload["resources"].([]any); ok {
		for i, item := range raw {
			m, ok := item.(map[string]any)
			if !ok {
				continue
			}
			if u, ok := m["url"].(string); ok && u != "" && !strings.HasPrefix(u, "http") {
				m["url"] = base + u
			}
			if u, ok := m["downloadUrl"].(string); ok && u != "" && !strings.HasPrefix(u, "http") {
				m["downloadUrl"] = base + u
			}
			raw[i] = m
		}
		payload["resources"] = raw
	}
	return payload, nil
}

func (a *App) OpenExamPaper() error {
	viewURL, err := a.GetExamPaperViewURL()
	if err != nil {
		return err
	}
	title := "Đề thi"
	a.mu.Lock()
	if a.dashboard.Exam != nil && strings.TrimSpace(a.dashboard.Exam.PaperTitle) != "" {
		title = strings.TrimSpace(a.dashboard.Exam.PaperTitle)
	}
	a.mu.Unlock()
	return a.openExamWebView(viewURL, title)
}

func (a *App) OpenExamQuiz() error {
	viewURL, err := a.GetExamQuizViewURL()
	if err != nil {
		return err
	}
	// Mở trực tiếp (không iframe) để cookie/localStorage đăng nhập web quiz là first-party.
	// F5 / Xóa cache / Về trang chính: menu Simple Care.
	return a.openExamWebView(viewURL, "Làm trắc nghiệm")
}

func (a *App) OpenExamResource(fileID uint) error {
	studentID, _, err := a.examStudentContext()
	if err != nil {
		return err
	}
	if fileID == 0 {
		return errors.New("tài nguyên không hợp lệ")
	}
	viewURL := fmt.Sprintf(
		"%s/api/student/exam/download?studentRkId=%d&kind=resource&fileId=%d&view=1",
		API_BASE, studentID, fileID,
	)
	title := "Tài nguyên đề thi"
	files, err := a.GetExamPaperFiles()
	if err == nil {
		if raw, ok := files["resources"].([]any); ok {
			for _, item := range raw {
				m, ok := item.(map[string]any)
				if !ok {
					continue
				}
				id, _ := m["id"].(float64)
				if uint(id) == fileID {
					if name, ok := m["fileName"].(string); ok && name != "" {
						title = name
					}
					break
				}
			}
		}
	}
	return a.openExamWebView(viewURL, title)
}

func examResourceDownloadDir() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
	candidates := []string{
		filepath.Join(home, "Downloads"),
		filepath.Join(home, "Desktop"),
		home,
	}
	for _, dir := range candidates {
		if st, err := os.Stat(dir); err == nil && st.IsDir() {
			return dir, nil
		}
	}
	return "", errors.New("không tìm thấy thư mục Downloads hoặc Desktop")
}

func uniqueFilePath(path string) string {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return path
	}
	ext := filepath.Ext(path)
	base := strings.TrimSuffix(filepath.Base(path), ext)
	dir := filepath.Dir(path)
	for i := 1; i < 100; i++ {
		candidate := filepath.Join(dir, fmt.Sprintf("%s (%d)%s", base, i, ext))
		if _, err := os.Stat(candidate); os.IsNotExist(err) {
			return candidate
		}
	}
	return filepath.Join(dir, fmt.Sprintf("%s_%d%s", base, time.Now().Unix(), ext))
}

func openFolderInExplorer(filePath string) {
	guard.SuppressFor(5 * time.Second)
	dir := filepath.Dir(filePath)
	_ = exec.Command("explorer", dir).Start()
}

func (a *App) DownloadExamResource(fileID uint) (string, error) {
	studentID, _, err := a.examStudentContext()
	if err != nil {
		return "", err
	}
	if fileID == 0 {
		return "", errors.New("tài nguyên không hợp lệ")
	}
	fileName := fmt.Sprintf("resource_%d", fileID)
	files, err := a.GetExamPaperFiles()
	if err == nil {
		if raw, ok := files["resources"].([]any); ok {
			for _, item := range raw {
				m, ok := item.(map[string]any)
				if !ok {
					continue
				}
				id, _ := m["id"].(float64)
				if uint(id) == fileID {
					if name, ok := m["fileName"].(string); ok && name != "" {
						fileName = filepath.Base(name)
					}
					break
				}
			}
		}
	}
	dlURL := fmt.Sprintf(
		"%s/api/student/exam/download?studentRkId=%d&kind=resource&fileId=%d",
		API_BASE, studentID, fileID,
	)
	resp, err := http.Get(dlURL)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return "", errors.New("không tải được tài nguyên")
	}
	dir, err := examResourceDownloadDir()
	if err != nil {
		return "", err
	}
	dest := uniqueFilePath(filepath.Join(dir, fileName))
	f, err := os.Create(dest)
	if err != nil {
		return "", err
	}
	if _, err := io.Copy(f, resp.Body); err != nil {
		f.Close()
		return "", err
	}
	f.Close()
	openFolderInExplorer(dest)
	return dest, nil
}

// LoadExamViewFile tải file đề/tài nguyên qua Go (không mở URL trực tiếp trong WebView).
func (a *App) LoadExamViewFile(fileURL string) (map[string]any, error) {
	fileURL = strings.TrimSpace(fileURL)
	if !strings.HasPrefix(fileURL, API_BASE + "/api/student/exam/download") {
		return nil, errors.New("URL không hợp lệ")
	}
	if _, _, err := a.examStudentContext(); err != nil {
		return nil, err
	}
	resp, err := http.Get(fileURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New("không tải được file")
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	mime := strings.TrimSpace(resp.Header.Get("Content-Type"))
	if mime == "" {
		mime = "application/octet-stream"
	}
	// Bỏ charset nếu có
	if i := strings.Index(mime, ";"); i >= 0 {
		mime = strings.TrimSpace(mime[:i])
	}
	return map[string]any{
		"data": base64.StdEncoding.EncodeToString(body),
		"mime": mime,
	}, nil
}

func (a *App) SubmitExamWork() (string, error) {
	a.mu.Lock()
	student := a.student
	exam := a.dashboard.Exam
	a.mu.Unlock()
	if student == nil {
		return "", errors.New("chưa đăng nhập")
	}
	if exam == nil {
		return "", errors.New("không trong giờ thi")
	}
	dir, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
		Title: "Chọn folder bài làm để nộp",
	})
	if err != nil {
		return "", err
	}
	if dir == "" {
		return "", errors.New("đã hủy")
	}
	zipPath := filepath.Join(os.TempDir(), fmt.Sprintf("exam_submit_%d_%d.zip", exam.ExamRoomID, time.Now().Unix()))
	if err := zipFolder(dir, zipPath); err != nil {
		return "", err
	}
	defer os.Remove(zipPath)

	f, err := os.Open(zipPath)
	if err != nil {
		return "", err
	}
	defer f.Close()

	var body bytes.Buffer
	w := multipart.NewWriter(&body)
	_ = w.WriteField("studentRkId", fmt.Sprintf("%d", student.StudentID))
	part, err := w.CreateFormFile("submission", filepath.Base(zipPath))
	if err != nil {
		return "", err
	}
	if _, err := io.Copy(part, f); err != nil {
		return "", err
	}
	_ = w.Close()

	req, err := http.NewRequest("POST", API_BASE + "/api/student/exam/submit", &body)
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", w.FormDataContentType())
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	var res struct {
		OK       bool   `json:"ok"`
		FileName string `json:"fileName"`
		Error    string `json:"error"`
	}
	_ = json.NewDecoder(resp.Body).Decode(&res)
	if resp.StatusCode >= 300 || !res.OK {
		if res.Error != "" {
			return "", errors.New(res.Error)
		}
		return "", errors.New("nộp bài thất bại")
	}
	a.mu.Lock()
	if a.dashboard.Exam != nil {
		a.dashboard.Exam.Submitted = true
	}
	a.mu.Unlock()
	return res.FileName, nil
}

func zipFolder(srcDir, destZip string) error {
	out, err := os.Create(destZip)
	if err != nil {
		return err
	}
	defer out.Close()
	zw := zip.NewWriter(out)
	defer zw.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
		w, err := zw.CreateHeader(hdr)
		if err != nil {
			return err
		}
		rf, err := os.Open(path)
		if err != nil {
			return err
		}
		_, copyErr := io.Copy(w, rf)
		rf.Close()
		return copyErr
	})
}
