package main

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

	"client/internal/blocker"
	"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 wifiReportLast sync.Map    // ssidKey -> time.Time

var encryptionKey = []byte("simple-care-proctor-secretkey32!") // 32-byte key for AES-256

func encrypt(data []byte) ([]byte, error) {
	block, err := aes.NewCipher(encryptionKey)
	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) {
	block, err := aes.NewCipher(encryptionKey)
	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
	wsConn         *websocket.Conn
	wsConnected    bool
	wifiSSID       string
	wifiBSSID      string
	allowedApps    string
	onlineSecs     int
	offlineSecs    int
	unsyncedOn     int
	unsyncedOff    int
	lastSyncTime   time.Time
	isStreamingSc  bool
	streamScStop   chan struct{}
	statusMsg      string
	expectingLogin bool
	backendOnline  bool
	dashboard      StudentDashboardSnapshot
	wifiEnforce    bool
	acceptedWifi   map[string]bool
	wifiRejected      bool
	quitDialogShown   bool
	monitoringTornDown bool
	chatUnread        int
	replyStaffID      uint
}

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")
	_ = os.MkdirAll(appDir, 0755)

	return &App{
		sessionPath:    filepath.Join(appDir, "student_session.json"),
		statsPath:      filepath.Join(appDir, "student_stats.json"),
		lastSyncTime:   time.Now(),
		expectingLogin: false,
	}
}

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

	// Register blocker callbacks
	blocker.Instance.OnBlocked = func(procName string, title string) {
		go a.reportBlockedApp(procName, title)
	}
	blocker.Instance.OnKill = func(procName string, title string) {
		_, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
			Type:    runtime.WarningDialog,
			Title:   "Ứng dụng bị đóng",
			Message: 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()
}

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

// 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()
	runtime.EventsEmit(a.ctx, "stop_webcam_stream")
	a.disconnectWS()
	guard.Stop()
}

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

// 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()

			blocker.Instance.Start()

			// 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
			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
		}
		go runtime.WindowReloadApp(a.ctx)
		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:#1e293b;color:#f8fafc;height:100vh;display:flex;flex-direction:column}
.bar{display:flex;align-items:center;gap:12px;padding:10px 14px;background:#0f172a;border-bottom:1px solid #334155;flex-shrink:0}
.bar button{background:#7c3aed;color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:14px;cursor:pointer;font-weight:600}
.bar button:hover{background:#6d28d9}
.bar span{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.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" onclick="goBack()">← Quay lại</button>
<span>%s</span>
</div>
<div class="frame-wrap">
<iframe src="%s" title="exam-content"></iframe>
</div>
<script>
function goBack(){
  fetch('http://127.0.0.1:34115/exam-close').catch(function(){});
}
</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)
			blocker.Instance.Start()

			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("http://127.0.0.1:8080/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.mu.Lock()
	a.expectingLogin = true
	a.mu.Unlock()
	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 = false
	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()

	runtime.WindowReloadApp(a.ctx)
}

// 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,
	}
}

// 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.mu.Lock()
	conn := a.wsConn
	a.mu.Unlock()

	if conn != nil {
		_ = conn.WriteJSON(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
		loggedIn := a.student != nil
		a.mu.Unlock()

		if loggedIn || !expecting {
			continue
		}

		runtime.WindowExecJS(a.ctx, `
			try {
				const st = localStorage.getItem("student");
				if (st) {
					fetch("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() {
	ticker := time.NewTicker(10 * time.Second)
	defer ticker.Stop()

	a.runMonitorTick()

	for {
		<-ticker.C
		a.runMonitorTick()
	}
}

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

	wifi := winapi.GetWifiConnection()
	a.mu.Lock()
	a.wifiSSID = wifi.SSID
	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()
	a.wifiSSID = wifi.SSID
	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("http://127.0.0.1:8080/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("http://127.0.0.1:8080/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("http://127.0.0.1:8080/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("http://127.0.0.1:8080/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("http://127.0.0.1:8080/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
	}
	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.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.mu.Lock()
	studentID := int64(0)
	if a.student != nil {
		studentID = a.student.StudentID
	}
	a.mu.Unlock()

	url := fmt.Sprintf("http://127.0.0.1:8080/api/classes/%d/allowed-apps?studentId=%d", 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 {
				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()
			
			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("http://127.0.0.1:8080/api/student/status?studentRkId=%d", 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()
	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()
}

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

	wsUrl := fmt.Sprintf("ws://127.0.0.1:8080/ws?role=student&studentId=%d&classId=%d", student.StudentID, classID)
	dialer := websocket.Dialer{HandshakeTimeout: 4 * time.Second}
	conn, _, err := dialer.Dial(wsUrl, nil)
	if err != nil {
		return
	}

	a.mu.Lock()
	a.wsConn = conn
	a.wsConnected = true
	a.mu.Unlock()

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

	go func() {
		defer a.disconnectWS()
		for {
			var msg struct {
				Event string         `json:"event"`
				Data  map[string]any `json:"data"`
			}
			err := conn.ReadJSON(&msg)
			if err != nil {
				break
			}

			switch msg.Event {
			case "start_screenshot_stream":
				a.startScreenshotStream()
			case "stop_screenshot_stream":
				a.stopScreenshotStream()
			case "start_webcam_stream":
				runtime.EventsEmit(a.ctx, "start_webcam_stream")
			case "stop_webcam_stream":
				runtime.EventsEmit(a.ctx, "stop_webcam_stream")
			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() {
	a.mu.Lock()
	defer a.mu.Unlock()

	if a.wsConn != nil {
		_ = a.wsConn.Close()
		a.wsConn = nil
	}
	a.wsConnected = false
}

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.mu.Lock()
				conn := a.wsConn
				a.mu.Unlock()

				if conn != nil {
					_ = conn.WriteJSON(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) 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("http://127.0.0.1:8080/api/student/chat/conversations?studentRkId=%d", 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("http://127.0.0.1:8080/api/student/chat/messages?studentRkId=%d&staffId=%d", 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("http://127.0.0.1:8080/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
}

func (a *App) openExamWebView(targetURL, title string) error {
	targetURL = strings.TrimSpace(targetURL)
	if targetURL == "" {
		return errors.New("không có nội dung để mở")
	}
	wrapper := fmt.Sprintf(
		"http://127.0.0.1:34115/exam-view?url=%s&title=%s",
		url.QueryEscape(targetURL),
		url.QueryEscape(title),
	)
	runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", wrapper))
	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("http://127.0.0.1:8080/api/student/exam/download?studentRkId=%d&kind=pdf", 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("http://127.0.0.1:8080/api/student/exam/paper-files?studentRkId=%d", 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 := "http://127.0.0.1:8080"
	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
			}
			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
	}
	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(
		"http://127.0.0.1:8080/api/student/exam/download?studentRkId=%d&kind=resource&fileId=%d",
		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 (a *App) DownloadExamResource(fileID uint) (string, error) {
	studentID, exam, 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(
		"http://127.0.0.1:8080/api/student/exam/download?studentRkId=%d&kind=resource&fileId=%d",
		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")
	}
	configDir, err := os.UserConfigDir()
	if err != nil {
		return "", err
	}
	dir := filepath.Join(configDir, "SimpleCare", "ExamResources", fmt.Sprintf("room_%d", exam.ExamRoomID))
	if err := os.MkdirAll(dir, 0755); err != nil {
		return "", err
	}
	dest := 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()
	return dest, 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", "http://127.0.0.1:8080/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
		}
		defer rf.Close()
		_, err = io.Copy(w, rf)
		return err
	})
}
