// Package vauth is a single-file VelvetAuth API 1.1 client (Go equivalent of vauth.cs).
//
// Standard library only. Copy this file into your module as vauth/vauth.go, then:
//
//	auth, err := vauth.New("YOUR_APP_ID", "YOUR_SECRET", "1.0")
//	if err != nil {
//	    log.Fatal(err)
//	}
//	defer auth.Close()
//	auth.APIBaseURL = "https://your-domain.com/api/1.1/"
//
//	if !auth.Initialize() {
//	    log.Fatal(auth.LastError)
//	}
//	if !auth.LoginUser("user", "pass") {
//	    log.Fatal(auth.LastError)
//	}
package vauth

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

const HeartbeatInterval = 3 * time.Second

var Instance *VAuth

type VpnLocation struct {
	ID                string `json:"id"`
	Name              string `json:"name"`
	Country           string `json:"country"`
	Label             string `json:"label"`
	Username          string `json:"username"`
	PasswordVariable  string `json:"password_variable"`
	Ovpn              string `json:"ovpn"`
}

func (v VpnLocation) String() string {
	if v.Name != "" && v.Country != "" {
		return v.Name + " (" + v.Country + ")"
	}
	if v.Name != "" {
		return v.Name
	}
	return v.ID
}

type VAuth struct {
	APIBaseURL string
	Debug      bool
	LastError  string
	HWID       string

	OnSessionEnded    func()
	OnUpdateAvailable func(filename, downloadURL string, autoDownload bool)
	ExitOnKick        bool

	SessionID                 string
	SessionKicked             bool
	Username                  string
	Email                     string
	UserLevel                 int
	ExpiryDate                *time.Time
	EmailVerificationRequired bool
	VariableTitle             string
	VariableValue             string
	VpnLocations              []VpnLocation
	VpnID                     string
	VpnName                   string
	VpnCountry                string
	VpnLabel                  string
	VpnUsername               string
	VpnPasswordVariable       string
	VpnOvpn                   string

	httpClient *http.Client
	appID      string
	secret     string
	version    string

	mu             sync.Mutex
	heartbeatBusy  int32
	heartbeatReady bool
	heartbeatStop  chan struct{}
}

type apiResponse struct {
	Status       string          `json:"status"`
	Message      string          `json:"message"`
	Error        string          `json:"error"`
	SessionID    string          `json:"session_id"`
	DownloadURL  string          `json:"download_url"`
	Filename     string          `json:"filename"`
	AutoDownload flexBool        `json:"auto_download"`
	Data         json.RawMessage `json:"data"`
}

type userPayload struct {
	Email         string  `json:"email"`
	ExpiryDate    string  `json:"expiry_date"`
	UserLevel     flexInt `json:"user_level"`
	NewExpiryDate string  `json:"new_expiry_date"`
	Title         string  `json:"title"`
	Value         string  `json:"value"`
	Username      string  `json:"username"`
}

type flexBool bool
type flexInt int

func (b *flexBool) UnmarshalJSON(data []byte) error {
	s := strings.Trim(strings.TrimSpace(string(data)), `"`)
	switch strings.ToLower(s) {
	case "true", "1":
		*b = true
	default:
		*b = false
	}
	return nil
}

func (n *flexInt) UnmarshalJSON(data []byte) error {
	s := strings.Trim(strings.TrimSpace(string(data)), `"`)
	if s == "" || s == "null" {
		*n = 0
		return nil
	}
	i, err := strconv.Atoi(s)
	if err != nil {
		return err
	}
	*n = flexInt(i)
	return nil
}

func New(appID, secret, version string) (*VAuth, error) {
	encAppID, err := EncryptString(secret, appID)
	if err != nil {
		return nil, fmt.Errorf("encrypt app id: %w", err)
	}
	encVersion, err := EncryptString(secret, version)
	if err != nil {
		return nil, fmt.Errorf("encrypt version: %w", err)
	}

	v := &VAuth{
		APIBaseURL: "http://localhost/velvetauth/vauth-source/api/1.1/",
		HWID:       defaultHWID(),
		httpClient: &http.Client{Timeout: 15 * time.Second},
		appID:      encAppID,
		secret:     secret,
		version:    encVersion,
	}
	Instance = v
	return v, nil
}

func (v *VAuth) Close() {
	v.StopHeartbeat()
	Instance = nil
}

func (v *VAuth) Initialize() bool {
	v.clearError()
	if v.SessionID != "" {
		return true
	}

	body, ok := v.post(map[string]string{
		"type":    "init",
		"app_id":  v.appID,
		"secret":  v.secret,
		"version": v.version,
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during initialization: " + err.Error())
	}

	if strings.EqualFold(resp.Status, "true") {
		v.SessionID = resp.SessionID
		v.log("Initialization successful.")
		return true
	}

	if resp.Error == "wrong_version" {
		filename := resp.Filename
		if filename == "" {
			filename = "update.exe"
		}
		if v.OnUpdateAvailable != nil {
			v.OnUpdateAvailable(filename, resp.DownloadURL, bool(resp.AutoDownload))
		}
		if resp.AutoDownload {
			if dest, err := v.DownloadUpdate(filename); err != nil {
				return v.fail("Error downloading new version: " + err.Error())
			} else {
				v.log("New version saved to: " + dest)
			}
		} else if resp.DownloadURL != "" {
			v.log("A new version is required. Download URL: " + resp.DownloadURL)
		} else {
			return v.fail("A new version is required, but no update file is uploaded yet.")
		}
		return v.fail("wrong_version")
	}

	if resp.Error != "" {
		return v.fail("Initialization failed: " + resp.Error)
	}
	if resp.Message != "" {
		return v.fail("Initialization failed: " + resp.Message)
	}
	return v.fail("Initialization failed.")
}

func (v *VAuth) DownloadUpdate(fileName string) (string, error) {
	if fileName == "" {
		fileName = "update.exe"
	}
	fileName = filepath.Base(fileName)

	payload, err := json.Marshal(map[string]string{
		"type":   "download_update",
		"app_id": v.appID,
		"secret": v.secret,
	})
	if err != nil {
		return "", err
	}

	client := &http.Client{Timeout: 10 * time.Minute}
	httpResp, err := client.Post(v.endpoint(), "application/json", bytes.NewReader(payload))
	if err != nil {
		return "", err
	}
	defer httpResp.Body.Close()

	if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
		return "", fmt.Errorf("could not download the new version from the API (%s)", httpResp.Status)
	}

	data, err := io.ReadAll(httpResp.Body)
	if err != nil {
		return "", err
	}
	if len(data) == 0 {
		return "", errors.New("the update download was empty")
	}

	mediaType := ""
	if ct := httpResp.Header.Get("Content-Type"); ct != "" {
		mediaType = strings.ToLower(strings.Split(ct, ";")[0])
	}
	if strings.Contains(mediaType, "json") {
		return "", fmt.Errorf("update download failed: %s", strings.TrimSpace(string(data)))
	}

	dest := fileName
	if err := os.WriteFile(dest, data, 0755); err != nil {
		return "", err
	}
	return dest, nil
}

func (v *VAuth) KeylessRegister(username, password, email string) bool {
	v.clearError()
	body, ok := v.post(map[string]string{
		"type":     "keyless",
		"username": v.mustEncrypt(username),
		"app_id":   v.appID,
		"password": v.mustEncrypt(password),
		"secret":   v.secret,
		"email":    v.mustEncrypt(email),
		"hwid":     v.mustEncrypt(v.HWID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during registration: " + err.Error())
	}

	switch resp.Message {
	case "Registration successful":
		v.EmailVerificationRequired = false
		v.log("Registration successful.")
		return true
	case "Verification code sent":
		v.EmailVerificationRequired = true
		v.Username = username
		v.Email = email
		v.log("Verification code sent. Check email, then call VerifyEmail.")
		return true
	case "Username is already used":
		return v.fail("Registration failed: Username is already used.")
	case "Email is already used":
		return v.fail("Registration failed: Email is already used.")
	default:
		return v.fail("Registration failed: " + firstNonEmpty(resp.Message, "Unknown error"))
	}
}

func (v *VAuth) RegisterLicense(username, password, licenseKey, email string) bool {
	v.clearError()
	body, ok := v.post(map[string]string{
		"type":        "register",
		"username":    v.mustEncrypt(username),
		"app_id":      v.appID,
		"password":    v.mustEncrypt(password),
		"secret":      v.secret,
		"license_key": v.mustEncrypt(licenseKey),
		"email":       v.mustEncrypt(email),
		"hwid":        v.mustEncrypt(v.HWID),
		"session_id":  v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during registration: " + err.Error())
	}

	switch resp.Message {
	case "Registration successful":
		data := decodeUser(resp.Data)
		v.EmailVerificationRequired = false
		v.Username = username
		v.Email = data.Email
		v.ExpiryDate = parseExpiry(data.ExpiryDate)
		v.UserLevel = int(data.UserLevel)
		v.StartHeartbeat()
		v.log("Registration successful.")
		return true
	case "Verification code sent":
		v.EmailVerificationRequired = true
		v.Username = username
		v.Email = email
		v.log("Verification code sent. Check email, then call VerifyEmail.")
		return true
	default:
		return v.fail("Registration failed: " + firstNonEmpty(resp.Message, "Unknown error"))
	}
}

func (v *VAuth) Logout() bool {
	v.clearError()
	body, ok := v.post(map[string]string{
		"type":       "logout",
		"session_id": v.mustEncrypt(v.SessionID),
		"app_id":     v.appID,
		"secret":     v.secret,
		"username":   v.mustEncrypt(v.Username),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during logout: " + err.Error())
	}

	if strings.EqualFold(strings.TrimSpace(resp.Message), "Logout successful") {
		v.StopHeartbeat()
		v.SessionKicked = false
		v.log("Logout successful.")
		return true
	}
	return v.fail("Logout failed: " + firstNonEmpty(resp.Message, "Unknown error"))
}

func (v *VAuth) LoginUser(username, password string) bool {
	v.clearError()
	body, ok := v.post(map[string]string{
		"type":       "login",
		"username":   v.mustEncrypt(username),
		"password":   v.mustEncrypt(password),
		"hwid":       v.mustEncrypt(v.HWID),
		"app_id":     v.appID,
		"secret":     v.secret,
		"session_id": v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during login: " + err.Error())
	}

	if resp.Message == "Login successful" {
		data := decodeUser(resp.Data)
		v.Username = username
		v.Email = data.Email
		v.ExpiryDate = parseExpiry(data.ExpiryDate)
		v.UserLevel = int(data.UserLevel)
		v.StartHeartbeat()
		v.log("Login successful.")
		return true
	}
	return v.fail("Login failed: " + firstNonEmpty(resp.Error, resp.Message, "Unknown error"))
}

func (v *VAuth) ChangeUsername(currentPassword, newUsername string) bool {
	v.clearError()
	if v.SessionID == "" {
		return v.fail("Change username failed: Call Initialize() first.")
	}
	if v.Username == "" {
		return v.fail("Change username failed: Login first.")
	}

	body, ok := v.post(map[string]string{
		"type":         "change_username",
		"password":     v.mustEncrypt(currentPassword),
		"new_username": v.mustEncrypt(newUsername),
		"app_id":       v.appID,
		"secret":       v.secret,
		"session_id":   v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during change username: " + err.Error())
	}
	if resp.Message == "Username updated" {
		data := decodeUser(resp.Data)
		if data.Username != "" {
			v.Username = data.Username
		} else {
			v.Username = newUsername
		}
		v.log("Username updated.")
		return true
	}
	return v.fail("Change username failed: " + firstNonEmpty(resp.Message, resp.Error, "Unknown error"))
}

func (v *VAuth) ChangeEmail(currentPassword, newEmail string) bool {
	v.clearError()
	if v.SessionID == "" {
		return v.fail("Change email failed: Call Initialize() first.")
	}
	if v.Username == "" {
		return v.fail("Change email failed: Login first.")
	}

	body, ok := v.post(map[string]string{
		"type":       "change_email",
		"password":   v.mustEncrypt(currentPassword),
		"new_email":  v.mustEncrypt(newEmail),
		"app_id":     v.appID,
		"secret":     v.secret,
		"session_id": v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during change email: " + err.Error())
	}
	switch resp.Message {
	case "Email updated", "Email updated. Verification code sent", "Email updated but failed to send verification email":
		data := decodeUser(resp.Data)
		if data.Email != "" {
			v.Email = data.Email
		} else {
			v.Email = newEmail
		}
		v.EmailVerificationRequired = resp.Message != "Email updated"
		v.log(resp.Message)
		return true
	default:
		return v.fail("Change email failed: " + firstNonEmpty(resp.Message, resp.Error, "Unknown error"))
	}
}

func (v *VAuth) ChangePassword(currentPassword, newPassword string) bool {
	v.clearError()
	if v.SessionID == "" {
		return v.fail("Change password failed: Call Initialize() first.")
	}
	if v.Username == "" {
		return v.fail("Change password failed: Login first.")
	}

	body, ok := v.post(map[string]string{
		"type":         "change_password",
		"password":     v.mustEncrypt(currentPassword),
		"new_password": v.mustEncrypt(newPassword),
		"app_id":       v.appID,
		"secret":       v.secret,
		"session_id":   v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during change password: " + err.Error())
	}
	if resp.Message == "Password updated" {
		v.log("Password updated.")
		return true
	}
	return v.fail("Change password failed: " + firstNonEmpty(resp.Message, resp.Error, "Unknown error"))
}

func (v *VAuth) ForgotPassword(email string) bool {
	v.clearError()
	if v.SessionID == "" {
		return v.fail("Forgot password failed: Call Initialize() first.")
	}

	body, ok := v.post(map[string]string{
		"type":       "forgot_password",
		"email":      v.mustEncrypt(email),
		"app_id":     v.appID,
		"secret":     v.secret,
		"session_id": v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during forgot password: " + err.Error())
	}
	if resp.Message == "Password reset code sent" {
		v.log("Password reset code sent to your email.")
		return true
	}
	return v.fail("Forgot password failed: " + firstNonEmpty(resp.Message, resp.Error, "Unknown error"))
}

func (v *VAuth) ResetPassword(email, code, newPassword string) bool {
	v.clearError()
	if v.SessionID == "" {
		return v.fail("Reset password failed: Call Initialize() first.")
	}

	body, ok := v.post(map[string]string{
		"type":         "reset_password",
		"email":        v.mustEncrypt(email),
		"code":         v.mustEncrypt(code),
		"new_password": v.mustEncrypt(newPassword),
		"app_id":       v.appID,
		"secret":       v.secret,
		"session_id":   v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during reset password: " + err.Error())
	}
	if resp.Message == "Password reset successful" {
		v.log("Password reset successful.")
		return true
	}
	return v.fail("Reset password failed: " + firstNonEmpty(resp.Message, resp.Error, "Unknown error"))
}

func (v *VAuth) VerifyEmail(email, code string) bool {
	v.clearError()
	if v.SessionID == "" {
		return v.fail("Verify email failed: Call Initialize() first.")
	}

	body, ok := v.post(map[string]string{
		"type":       "verify_email",
		"email":      v.mustEncrypt(email),
		"code":       v.mustEncrypt(code),
		"app_id":     v.appID,
		"secret":     v.secret,
		"session_id": v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during verify email: " + err.Error())
	}
	if resp.Message == "Email verified" {
		v.EmailVerificationRequired = false
		v.Email = email
		v.StartHeartbeat()
		v.log("Email verified.")
		return true
	}
	return v.fail("Verify email failed: " + firstNonEmpty(resp.Message, resp.Error, "Unknown error"))
}

func (v *VAuth) ResendVerification(email string) bool {
	v.clearError()
	if v.SessionID == "" {
		return v.fail("Resend verification failed: Call Initialize() first.")
	}

	body, ok := v.post(map[string]string{
		"type":       "resend_verification",
		"email":      v.mustEncrypt(email),
		"app_id":     v.appID,
		"secret":     v.secret,
		"session_id": v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during resend verification: " + err.Error())
	}
	if resp.Message == "Verification code sent" {
		v.log("Verification code sent to your email.")
		return true
	}
	return v.fail("Resend verification failed: " + firstNonEmpty(resp.Message, resp.Error, "Unknown error"))
}

func (v *VAuth) GetVariable(variableCode string) string {
	v.VariableTitle = ""
	v.VariableValue = ""
	if v.SessionID == "" || variableCode == "" {
		return ""
	}

	body, ok := v.postQuiet(map[string]string{
		"type":          "get_variable",
		"variable_code": v.mustEncrypt(variableCode),
		"app_id":        v.appID,
		"secret":        v.secret,
		"session_id":    v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return ""
	}

	resp, err := parseResponse(body)
	if err != nil || resp.Message != "Variable found" {
		return ""
	}
	data := decodeUser(resp.Data)
	v.VariableTitle = data.Title
	v.VariableValue = data.Value
	return v.VariableValue
}

func (v *VAuth) GetVpns() []VpnLocation {
	v.VpnLocations = nil
	if v.SessionID == "" {
		return v.VpnLocations
	}

	body, ok := v.postQuiet(map[string]string{
		"type":       "get_vpns",
		"app_id":     v.appID,
		"secret":     v.secret,
		"session_id": v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return v.VpnLocations
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.VpnLocations
	}
	if resp.Message == "VPNs found" || resp.Message == "No VPNs found" {
		if len(resp.Data) > 0 {
			_ = json.Unmarshal(resp.Data, &v.VpnLocations)
		}
	}
	if v.VpnLocations == nil {
		v.VpnLocations = []VpnLocation{}
	}
	return v.VpnLocations
}

func (v *VAuth) GetVpn(vpnID string) string {
	v.VpnID = ""
	v.VpnName = ""
	v.VpnCountry = ""
	v.VpnLabel = ""
	v.VpnUsername = ""
	v.VpnPasswordVariable = ""
	v.VpnOvpn = ""
	if v.SessionID == "" || vpnID == "" {
		return ""
	}

	body, ok := v.postQuiet(map[string]string{
		"type":       "get_vpn",
		"vpn_id":     v.mustEncrypt(vpnID),
		"app_id":     v.appID,
		"secret":     v.secret,
		"session_id": v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return ""
	}

	resp, err := parseResponse(body)
	if err != nil || resp.Message != "VPN found" {
		return ""
	}

	var location VpnLocation
	if err := json.Unmarshal(resp.Data, &location); err != nil {
		return ""
	}
	v.VpnID = location.ID
	v.VpnName = location.Name
	v.VpnCountry = location.Country
	v.VpnLabel = location.Label
	v.VpnUsername = location.Username
	v.VpnPasswordVariable = location.PasswordVariable
	v.VpnOvpn = location.Ovpn
	return v.VpnOvpn
}

func (v *VAuth) Heartbeat() bool {
	if v.SessionID == "" {
		return false
	}
	if !atomic.CompareAndSwapInt32(&v.heartbeatBusy, 0, 1) {
		return false
	}
	defer atomic.StoreInt32(&v.heartbeatBusy, 0)

	body, ok := v.postQuiet(map[string]string{
		"type":       "heartbeat",
		"app_id":     v.appID,
		"secret":     v.secret,
		"session_id": v.mustEncrypt(v.SessionID),
	})
	if !ok {
		return !v.SessionKicked
	}

	raw := string(body)
	if i := strings.Index(raw, "{"); i > 0 {
		raw = raw[i:]
	}
	resp, err := parseResponse([]byte(raw))
	if err != nil {
		return !v.SessionKicked
	}

	message := strings.TrimSpace(resp.Message)
	if strings.EqualFold(message, "Heartbeat ok") {
		v.mu.Lock()
		v.heartbeatReady = true
		v.mu.Unlock()
	}
	if strings.EqualFold(message, "Session kicked") {
		v.raiseSessionKicked()
		return false
	}
	return !v.SessionKicked
}

func (v *VAuth) WatchSession() {
	v.SessionKicked = false
	v.StartHeartbeat()
}

func (v *VAuth) StartHeartbeat() {
	v.mu.Lock()
	v.SessionKicked = false
	v.heartbeatReady = false
	v.mu.Unlock()
	v.StopHeartbeat()

	stop := make(chan struct{})
	v.mu.Lock()
	v.heartbeatStop = stop
	v.mu.Unlock()

	go func() {
		_ = v.Heartbeat()
		ticker := time.NewTicker(HeartbeatInterval)
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
				_ = v.Heartbeat()
			case <-stop:
				return
			}
		}
	}()
}

func (v *VAuth) StopHeartbeat() {
	v.mu.Lock()
	stop := v.heartbeatStop
	v.heartbeatStop = nil
	v.mu.Unlock()
	if stop != nil {
		close(stop)
	}
}

func (v *VAuth) ExtendLicenseExpiry(username, licenseKey string) bool {
	v.clearError()
	body, ok := v.post(map[string]string{
		"type":        "extend_expiry",
		"username":    v.mustEncrypt(username),
		"license_key": v.mustEncrypt(licenseKey),
		"app_id":      v.appID,
		"secret":      v.secret,
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during license extension: " + err.Error())
	}
	if resp.Message == "License expiry extended successfully" {
		data := decodeUser(resp.Data)
		v.ExpiryDate = parseExpiry(data.NewExpiryDate)
		v.log("License expiry extended successfully.")
		return true
	}
	return v.fail("License extension failed: " + firstNonEmpty(resp.Message, "Unknown error"))
}

func (v *VAuth) Log(message string) bool {
	v.clearError()
	body, ok := v.post(map[string]string{
		"type":    "log",
		"app_id":  v.appID,
		"secret":  v.secret,
		"message": v.mustEncrypt(message),
	})
	if !ok {
		return false
	}

	resp, err := parseResponse(body)
	if err != nil {
		return v.fail("JSON parsing error during log: " + err.Error())
	}
	if resp.Message == "Log sent to Discord" {
		v.log("Log sent successfully.")
		return true
	}
	return v.fail("Log failed: " + firstNonEmpty(resp.Error, "Unknown error"))
}

func (v *VAuth) raiseSessionKicked() {
	v.mu.Lock()
	if v.SessionKicked {
		v.mu.Unlock()
		return
	}
	v.SessionKicked = true
	v.mu.Unlock()
	v.StopHeartbeat()
	v.LastError = "You've been kicked from the session."
	if v.OnSessionEnded != nil {
		v.OnSessionEnded()
	}
	if v.ExitOnKick {
		os.Exit(0)
	}
}

func (v *VAuth) endpoint() string {
	base := strings.TrimRight(v.APIBaseURL, "/") + "/"
	return base + "index.php"
}

func (v *VAuth) post(data map[string]string) ([]byte, bool) {
	return v.doPost(data, true)
}

func (v *VAuth) postQuiet(data map[string]string) ([]byte, bool) {
	return v.doPost(data, false)
}

func (v *VAuth) doPost(data map[string]string, setError bool) ([]byte, bool) {
	payload, err := json.Marshal(data)
	if err != nil {
		if setError {
			v.fail("Request failed: " + err.Error())
		}
		return nil, false
	}

	v.logf("Request URL: %s", v.endpoint())
	v.logf("Request Data: %s", payload)

	httpResp, err := v.httpClient.Post(v.endpoint(), "application/json", bytes.NewReader(payload))
	if err != nil {
		if setError {
			if isTimeout(err) {
				v.fail("Request timed out. The server may be slow sending email — try again or check SMTP on the web host.")
			} else {
				v.fail("Request failed: " + err.Error())
			}
		}
		return nil, false
	}
	defer httpResp.Body.Close()

	v.logf("Response Status Code: %s", httpResp.Status)

	body, err := io.ReadAll(httpResp.Body)
	if err != nil {
		if setError {
			v.fail("Request failed: " + err.Error())
		}
		return nil, false
	}

	if setError {
		v.logf("Raw response: %s", body)
	}

	if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
		if setError {
			v.fail("HTTP " + httpResp.Status)
		}
		return body, false
	}
	return body, true
}

func (v *VAuth) mustEncrypt(plain string) string {
	out, err := EncryptString(v.secret, plain)
	if err != nil {
		v.fail("Encryption failed: " + err.Error())
		return ""
	}
	return out
}

func (v *VAuth) clearError() {
	v.LastError = ""
}

func (v *VAuth) fail(msg string) bool {
	v.LastError = msg
	v.log(msg)
	return false
}

func (v *VAuth) log(msg string) {
	if v.Debug {
		log.Println("[vauth]", msg)
	}
}

func (v *VAuth) logf(format string, args ...interface{}) {
	if v.Debug {
		log.Printf("[vauth] "+format, args...)
	}
}

func parseResponse(body []byte) (*apiResponse, error) {
	var resp apiResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, err
	}
	return &resp, nil
}

func decodeUser(raw json.RawMessage) userPayload {
	var data userPayload
	if len(raw) > 0 {
		_ = json.Unmarshal(raw, &data)
	}
	return data
}

func parseExpiry(value string) *time.Time {
	value = strings.TrimSpace(value)
	if value == "" || strings.EqualFold(value, "null") {
		return nil
	}
	layouts := []string{
		"2006-01-02",
		"2006-01-02 15:04:05",
		time.RFC3339,
	}
	for _, layout := range layouts {
		if t, err := time.ParseInLocation(layout, value, time.Local); err == nil {
			return &t
		}
	}
	return nil
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}

func isTimeout(err error) bool {
	type timeout interface{ Timeout() bool }
	if t, ok := err.(timeout); ok && t.Timeout() {
		return true
	}
	return strings.Contains(strings.ToLower(err.Error()), "timeout")
}

func EncryptString(keyHex, plainText string) (string, error) {
	key, err := hex.DecodeString(strings.TrimSpace(keyHex))
	if err != nil {
		return "", fmt.Errorf("secret must be hex: %w", err)
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	padded := pkcs7Pad([]byte(plainText), aes.BlockSize)
	iv := make([]byte, aes.BlockSize)
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return "", err
	}

	ciphertext := make([]byte, len(padded))
	cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, padded)
	return base64.StdEncoding.EncodeToString(append(iv, ciphertext...)), nil
}

func DecryptString(keyHex, cipherText string) (string, error) {
	key, err := hex.DecodeString(strings.TrimSpace(keyHex))
	if err != nil {
		return "", fmt.Errorf("secret must be hex: %w", err)
	}
	full, err := base64.StdEncoding.DecodeString(cipherText)
	if err != nil {
		return "", err
	}
	if len(full) < aes.BlockSize {
		return "", errors.New("ciphertext too short")
	}

	iv := full[:aes.BlockSize]
	raw := full[aes.BlockSize:]
	if len(raw)%aes.BlockSize != 0 {
		return "", errors.New("ciphertext is not a multiple of the block size")
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}
	plain := make([]byte, len(raw))
	cipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, raw)
	plain, err = pkcs7Unpad(plain)
	if err != nil {
		return "", err
	}
	return string(plain), nil
}

func pkcs7Pad(data []byte, blockSize int) []byte {
	pad := blockSize - (len(data) % blockSize)
	return append(data, bytes.Repeat([]byte{byte(pad)}, pad)...)
}

func pkcs7Unpad(data []byte) ([]byte, error) {
	if len(data) == 0 {
		return nil, errors.New("empty plaintext")
	}
	pad := int(data[len(data)-1])
	if pad == 0 || pad > len(data) {
		return nil, errors.New("invalid padding")
	}
	for i := 0; i < pad; i++ {
		if data[len(data)-1-i] != byte(pad) {
			return nil, errors.New("invalid padding")
		}
	}
	return data[:len(data)-pad], nil
}

func defaultHWID() string {
	if runtime.GOOS == "windows" {
		if sid := windowsSID(); sid != "" {
			return sid
		}
	}
	if u, err := user.Current(); err == nil {
		if u.Uid != "" {
			return u.Uid + ":" + u.Username
		}
		if u.Username != "" {
			return u.Username
		}
	}
	host, _ := os.Hostname()
	return host
}

func windowsSID() string {
	out, err := exec.Command("whoami", "/user", "/fo", "csv", "/nh").Output()
	if err != nil {
		return ""
	}
	line := strings.TrimSpace(string(out))
	parts := strings.Split(line, ",")
	if len(parts) == 0 {
		return ""
	}
	sid := strings.Trim(strings.TrimSpace(parts[len(parts)-1]), "\"")
	if strings.HasPrefix(sid, "S-") {
		return sid
	}
	return ""
}
