package main

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"sort"
	"strings"
	"sync"
	"time"

	"SimpleArmaAdmin/internal/auth"
	"SimpleArmaAdmin/internal/crypto"
	internal_nats "SimpleArmaAdmin/internal/nats"
	"SimpleArmaAdmin/internal/parser"
	"SimpleArmaAdmin/internal/webauthn"
	"github.com/google/uuid"
	"github.com/gorilla/websocket"
	"github.com/nats-io/nats.go"
)

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool { return true },
}

// ============================================================================
// DEV MODE: In-memory stores
// ============================================================================

type DevCommunity struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	CreatedAt time.Time `json:"createdAt"`
}

type DevUser struct {
	ID           string    `json:"id"`
	Username     string    `json:"username"`
	Email        string    `json:"email"`
	PasswordHash string    `json:"-"`
	CreatedAt    time.Time `json:"createdAt"`
}

type DevMembership struct {
	UserID      string `json:"userId"`
	CommunityID string `json:"communityId"`
	Role        string `json:"role"` // "owner" | "admin" | "member"
}

type DevServer struct {
	ID                    string    `json:"id"`
	CommunityID           string    `json:"communityId"`
	Name                  string    `json:"name"`
	Description           string    `json:"description"`
	WorkerID              string    `json:"workerId"`
	StorageID             string    `json:"storageId"`
	LogPath               string    `json:"logPath"`
	EncryptedRcon         string    `json:"encryptedRcon"`
	EncryptedAutoMessages string    `json:"encryptedAutoMessages"` // E2EE blob for AutoMsgs & MOTD
	MockActive            bool      `json:"mockActive"`
	CreatedAt             time.Time `json:"createdAt"`
}

type DevPlayerNote struct {
	ID             string    `json:"id"`
	CommunityID    string    `json:"communityId"`
	PlayerNameHash string    `json:"playerNameHash"`
	Category       string    `json:"category"` // "warning" | "info" | "custom"
	Content        string    `json:"content"`  // Plaintext for simplicity in dev
	CreatedBy      string    `json:"createdBy"`
	CreatedAt      time.Time `json:"createdAt"`
}

type DevPermission struct {
	ID          string    `json:"id"`
	CommunityID string    `json:"communityId"`
	UserID      string    `json:"userId"`
	Username    string    `json:"username"`
	ServerID    string    `json:"serverId"`
	ServerName  string    `json:"serverName"`
	Scopes      []string  `json:"scopes"`
	GrantedBy   string    `json:"grantedBy"`
	GrantedAt   time.Time `json:"grantedAt"`
}

type DevPlayer struct {
	ID          string    `json:"id"`
	CommunityID string    `json:"communityId"`
	NameHash    string    `json:"nameHash"` // Blind Index
	Data        string    `json:"data"`     // Encrypted JSON (Name, IP, SteamID, etc.)
	JoinedAt    time.Time `json:"joinedAt"`
}

type DevBan struct {
	ID             string     `json:"id"`
	CommunityID    string     `json:"communityId"`
	PlayerNameHash string     `json:"playerNameHash"` // Blind Index for searching
	Data           string     `json:"data"`           // Encrypted JSON (PlayerName, Reason, BannedBy)
	BannedAt       time.Time  `json:"bannedAt"`
	ExpiresAt      *time.Time `json:"expiresAt"` // nil = permanent
}

type DevNode struct {
	ID          string     `json:"id"`
	CommunityID string     `json:"-"`
	Name        string     `json:"name"`
	Type        string     `json:"type"` // "storage" | "worker"
	Token       string     `json:"-"`    // only returned at creation
	Endpoint    string     `json:"endpoint"`
	Status      string     `json:"status"` // "pending" | "online" | "offline"
	Version     string     `json:"version"`
	LastSeen    *time.Time `json:"lastSeen"`
	CreatedAt   time.Time  `json:"createdAt"`
}

type DevPasskey struct {
	ID           string    `json:"id"`
	UserID       string    `json:"userId"`
	CommunityID  string    `json:"communityId"`
	CredentialID string    `json:"credentialId"`
	Name         string    `json:"name"`
	CreatedAt    time.Time `json:"createdAt"`
}

type DevStore struct {
	communities map[string]*DevCommunity
	users       map[string]*DevUser
	usersByName map[string]*DevUser
	memberships []DevMembership
	servers     map[string]*DevServer
	permissions map[string]*DevPermission
	players     map[string]*DevPlayer // key: communityID+":"+playerName
	roster      map[string]*DevPlayer // key: communityID+":"+playerNameHash
	playerNotes []DevPlayerNote
	bans        map[string]*DevBan // key: ban ID
	passkeys    map[string]*DevPasskey
	nodes       map[string]*DevNode
	logs        []LogEntry // In-memory log history for dev mode
	mockCancels map[string]context.CancelFunc
	mu          sync.RWMutex
}

type LogEntry struct {
	CommunityID string `json:"communityId"`
	Data        []byte `json:"data"` // Encrypted payload
}

func newDevStore() *DevStore {
	return &DevStore{
		communities: make(map[string]*DevCommunity),
		users:       make(map[string]*DevUser),
		usersByName: make(map[string]*DevUser),
		servers:     make(map[string]*DevServer),
		permissions: make(map[string]*DevPermission),
		players:     make(map[string]*DevPlayer),
		roster:      make(map[string]*DevPlayer),
		bans:        make(map[string]*DevBan),
		passkeys:    make(map[string]*DevPasskey),
		nodes:       make(map[string]*DevNode),
		mockCancels: make(map[string]context.CancelFunc),
	}
}

func generateNodeToken() string {
	return "nk_" + strings.ReplaceAll(uuid.NewString(), "-", "") + strings.ReplaceAll(uuid.NewString(), "-", "")
}

var devMockLogs = []string{
	"12:30:01.122   SCRIPT       : [RJSSupport][Chat] [Global] Zauberklöte: hi, leute kurze frage. zock seit monaten wieder mal arma, was ist aus dem gtg#4 und #5 geworden, da ist ja nix los",
	"09:37:50.865  DEFAULT      : BattlEye Server: 'Player #0 Mike1Delta (92.209.175.19:6679) connected'",
	"13:29:19.727   SCRIPT       : [RJSSupport][Chat] [Global] 纱雾.: WHAT",
	"09:38:53.842  DEFAULT      : BattlEye Server: 'Player #0 Mike1Delta disconnected'",
	"14:56:34.622   SCRIPT       : [RJSSupport][Chat] [Global] Toope: help?",
	"15:04:22.868   SCRIPT       : [RJSSupport][Chat] [Global] vatrano: Transpo 5-10min abwesend",
	"10:11:22.001  DEFAULT      : BattlEye Server: 'Player #2 SgtPepper47 (195.60.71.82:2201) connected'",
	"10:15:00.123   SCRIPT       : [RJSSupport][Chat] [Global] SgtPepper47: anyone in comms?",
	"10:22:44.500  DEFAULT      : BattlEye Server: 'Player #3 Delta_Force_R (91.77.88.99:4411) connected'",
	"10:28:10.777   SCRIPT       : [RJSSupport][Chat] [Global] Delta_Force_R: pushing north",
	"10:35:00.900  DEFAULT      : BattlEye Server: 'Player #2 SgtPepper47 disconnected'",
	"10:40:18.200   SCRIPT       : [RJSSupport][Chat] [Global] Zauberklöte: gg wp",
}

// mockPlayerIPs maps known mock player names to their IPs for JOIN events
var mockPlayerIPs = map[string]string{
	"Mike1Delta":    "92.209.175.19",
	"Zauberklöte":   "89.12.45.67",
	"纱雾":            "103.45.78.90",
	"Toope":         "178.22.33.44",
	"vatrano":       "88.99.100.11",
	"SgtPepper47":   "195.60.71.82",
	"Delta_Force_R": "91.77.88.99",
}

func extractPlayerName(content string) string {
	if strings.Contains(content, "connected to server") {
		return strings.TrimSpace(strings.Split(content, " connected")[0])
	}
	if strings.Contains(content, "left the server") {
		return strings.TrimSpace(strings.Split(content, " left")[0])
	}
	if strings.Contains(content, ":") {
		return strings.TrimSpace(strings.SplitN(content, ":", 2)[0])
	}
	return ""
}

// ============================================================================
// GATEWAY
// ============================================================================

type Gateway struct {
	natsClient *internal_nats.Client
	dashboards map[*websocket.Conn]bool
	mu         sync.Mutex
	devMode    bool
	store      *DevStore
}

func (g *Gateway) startMock(communityID, serverID string) {
	g.store.mu.Lock()
	if _, exists := g.store.mockCancels[communityID]; exists {
		g.store.mu.Unlock()
		return
	}
	ctx, cancel := context.WithCancel(context.Background())
	g.store.mockCancels[communityID] = cancel

	masterKey := []byte("this-is-a-32-byte-master-key-xyz")

	// Seed initial online players
	for name, ip := range mockPlayerIPs {
		nameHash := crypto.GenerateBlindIndex(name, masterKey)
		playerData, _ := json.Marshal(map[string]string{
			"name": name,
			"ip":   ip,
		})
		encryptedData, _ := crypto.Encrypt(playerData, masterKey)

		key := communityID + ":" + nameHash
		p := &DevPlayer{
			ID:          uuid.NewString(),
			CommunityID: communityID,
			NameHash:    nameHash,
			Data:        base64.StdEncoding.EncodeToString(encryptedData),
			JoinedAt:    time.Now().Add(-time.Duration(len(name)) * time.Minute),
		}
		g.store.players[key] = p
		g.store.roster[key] = p
	}

	// Seed mock infrastructure nodes
	seedTime := time.Now()
	storageNode := &DevNode{
		ID:          uuid.NewString(),
		CommunityID: communityID,
		Name:        "Primary Storage",
		Type:        "storage",
		Token:       generateNodeToken(),
		Endpoint:    "http://storage.example.com:9000",
		Status:      "online",
		Version:     "1.2.0",
		LastSeen:    &seedTime,
		CreatedAt:   time.Now().Add(-24 * time.Hour),
	}
	workerNode := &DevNode{
		ID:          uuid.NewString(),
		CommunityID: communityID,
		Name:        "Log Worker #1",
		Type:        "worker",
		Token:       generateNodeToken(),
		Endpoint:    "http://worker.example.com:8001",
		Status:      "online",
		Version:     "0.9.5",
		LastSeen:    &seedTime,
		CreatedAt:   time.Now().Add(-12 * time.Hour),
	}
	g.store.nodes[storageNode.ID] = storageNode
	g.store.nodes[workerNode.ID] = workerNode
	g.store.mu.Unlock()

	go func() {
		ticker := time.NewTicker(10 * time.Second)
		defer ticker.Stop()
		i := 0
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				line := devMockLogs[i%len(devMockLogs)]
				event := parser.ParseLine(line)
				if event != nil {
					event.ServerID = serverID
					event.ServerName = "AMS-NODE-01" // Mock server name

					if event.PlayerName != "" {
						event.PlayerNameHash = crypto.GenerateBlindIndex(event.PlayerName, masterKey)
					} else {
						// Assign to SYSTEM if no player is present
						event.PlayerName = "SYSTEM"
						event.PlayerNameHash = "system-blind-index"
					}

					// Keep player list in sync with mock events
					g.store.mu.Lock()
					switch event.Type {
					case "JOIN":
						if event.PlayerName != "SYSTEM" {
							playerName := event.PlayerName
							ip := mockPlayerIPs[playerName]
							if ip == "" {
								ip = "10.0.0.1"
							}
							nameHash := crypto.GenerateBlindIndex(playerName, masterKey)
							playerData, _ := json.Marshal(map[string]string{
								"name": playerName,
								"ip":   ip,
							})
							encryptedData, _ := crypto.Encrypt(playerData, masterKey)

							key := communityID + ":" + nameHash
							p := &DevPlayer{
								ID:          uuid.NewString(),
								CommunityID: communityID,
								NameHash:    nameHash,
								Data:        base64.StdEncoding.EncodeToString(encryptedData),
								JoinedAt:    time.Now(),
							}
							g.store.players[key] = p
							g.store.roster[key] = p
						}
					case "LEAVE":
						if event.PlayerName != "SYSTEM" {
							nameHash := crypto.GenerateBlindIndex(event.PlayerName, masterKey)
							delete(g.store.players, communityID+":"+nameHash)
						}
					}
					// Simulate heartbeats for mock nodes
					hbNow := time.Now()
					for _, node := range g.store.nodes {
						if node.CommunityID == communityID {
							node.LastSeen = &hbNow
							node.Status = "online"
						}
					}

					onlineCount := 0
					for _, p := range g.store.players {
						if p.CommunityID == communityID {
							onlineCount++
						}
					}
					g.store.mu.Unlock()

					payload, err := json.Marshal(event)
					if err == nil {
						if encrypted, err := crypto.Encrypt(payload, masterKey); err == nil {
							g.broadcast(websocket.BinaryMessage, encrypted)
						}
					}

					telemetry, _ := json.Marshal(map[string]interface{}{
						"type":    "TELEMETRY",
						"fps":     45.5 + float64(time.Now().Unix()%5),
						"players": onlineCount,
					})
					g.broadcast(websocket.TextMessage, telemetry)
				}
				i++
			}
		}
	}()
	log.Printf("[DEV] Mock data stream started for community %s", communityID)
}

func (g *Gateway) stopMock(communityID string) {
	g.store.mu.Lock()
	defer g.store.mu.Unlock()
	if cancel, exists := g.store.mockCancels[communityID]; exists {
		cancel()
		delete(g.store.mockCancels, communityID)
		// Clear online players for this community
		for key, p := range g.store.players {
			if p.CommunityID == communityID {
				delete(g.store.players, key)
			}
		}
		log.Printf("[DEV] Mock data stream stopped for community %s", communityID)
	}
}

// broadcastEvent encrypts an event and broadcasts it to all dashboards
func (g *Gateway) broadcastEvent(event *parser.LogEvent, serverID string) {
	masterKey := []byte("this-is-a-32-byte-master-key-xyz")

	event.ServerID = serverID
	event.ServerName = "AMS-NODE-01" // Mock

	if event.PlayerName == "" {
		event.PlayerName = "SYSTEM"
		event.PlayerNameHash = "system-blind-index"
	} else if event.PlayerNameHash == "" {
		event.PlayerNameHash = crypto.GenerateBlindIndex(event.PlayerName, masterKey)
	}

	payload, err := json.Marshal(event)
	if err != nil {
		return
	}
	if encrypted, err := crypto.Encrypt(payload, masterKey); err == nil {
		g.broadcast(websocket.BinaryMessage, encrypted)
	}
}

// ============================================================================
// HELPERS
// ============================================================================

func writeJSON(w http.ResponseWriter, status int, v interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, message string) {
	writeJSON(w, status, map[string]string{"error": message})
}

func (g *Gateway) requireAuth(r *http.Request) (*auth.Claims, error) {
	authHeader := r.Header.Get("Authorization")
	if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
		return nil, fmt.Errorf("missing token")
	}
	return auth.VerifyJWT(strings.TrimPrefix(authHeader, "Bearer "))
}

// ============================================================================
// WEBSOCKET
// ============================================================================

func (g *Gateway) handleWebSocket(w http.ResponseWriter, r *http.Request) {
	role := r.URL.Query().Get("role")
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Printf("Upgrade error: %v", err)
		return
	}

	if role == "dashboard" {
		g.mu.Lock()
		g.dashboards[conn] = true
		g.mu.Unlock()
	}

	defer func() {
		if role == "dashboard" {
			g.mu.Lock()
			delete(g.dashboards, conn)
			g.mu.Unlock()
		}
		conn.Close()
	}()

	communityID := "comm-123-abc"

	for {
		messageType, p, err := conn.ReadMessage()
		if err != nil {
			break
		}
		if role != "dashboard" {
			if messageType == websocket.BinaryMessage {
				g.store.mu.Lock()
				g.store.logs = append(g.store.logs, LogEntry{
					CommunityID: communityID,
					Data:        p,
				})
				if len(g.store.logs) > 1000 {
					g.store.logs = g.store.logs[1:]
				}
				g.store.mu.Unlock()

				g.natsClient.PublishLog(context.Background(), communityID, "live", p)
				g.broadcast(websocket.BinaryMessage, p)
			} else {
				g.natsClient.Conn.Publish("telemetry."+communityID, p)
			}
		}
	}
}

func (g *Gateway) broadcast(messageType int, data []byte) {
	if messageType == websocket.BinaryMessage {
		g.store.mu.Lock()
		g.store.logs = append(g.store.logs, LogEntry{
			CommunityID: "dev-community", // Hardcoded for dev mode simplicity
			Data:        data,
		})
		// Keep last 1000 logs
		if len(g.store.logs) > 1000 {
			g.store.logs = g.store.logs[1:]
		}
		g.store.mu.Unlock()
	}

	g.mu.Lock()
	defer g.mu.Unlock()
	for client := range g.dashboards {
		if err := client.WriteMessage(messageType, data); err != nil {
			client.Close()
			delete(g.dashboards, client)
		}
	}
}

// ============================================================================
// REGISTRATION
// ============================================================================

func (g *Gateway) handleRegister(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req struct {
		Username      string `json:"username"`
		Email         string `json:"email"`
		Password      string `json:"password"`
		CommunityName string `json:"communityName"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}

	req.Username = strings.TrimSpace(req.Username)
	if req.Username == "" {
		writeError(w, http.StatusBadRequest, "Username is required")
		return
	}

	if err := auth.ValidatePasswordStrength(req.Password); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}

	hash, err := auth.HashPassword(req.Password)
	if err != nil {
		http.Error(w, "Failed to hash password", http.StatusInternalServerError)
		return
	}

	if g.devMode {
		g.store.mu.Lock()
		defer g.store.mu.Unlock()

		if _, exists := g.store.usersByName[strings.ToLower(req.Username)]; exists {
			writeError(w, http.StatusConflict, "Username already taken")
			return
		}

		communityID := uuid.NewString()
		communityName := req.CommunityName
		if communityName == "" {
			communityName = req.Username + "'s Team"
		}

		community := &DevCommunity{
			ID:        communityID,
			Name:      communityName,
			CreatedAt: time.Now(),
		}
		g.store.communities[communityID] = community

		userID := uuid.NewString()
		user := &DevUser{
			ID:           userID,
			Username:     req.Username,
			Email:        req.Email,
			PasswordHash: hash,
			CreatedAt:    time.Now(),
		}
		g.store.users[userID] = user
		g.store.usersByName[strings.ToLower(req.Username)] = user

		membership := DevMembership{
			UserID:      userID,
			CommunityID: communityID,
			Role:        "owner",
		}
		g.store.memberships = append(g.store.memberships, membership)

		log.Printf("[DEV] Registered user: %s, Created community: %s", req.Username, communityName)

		token, err := auth.GenerateJWT(userID, communityID, req.Username, 7*24*time.Hour)
		if err != nil {
			http.Error(w, "Failed to generate token", http.StatusInternalServerError)
			return
		}

		writeJSON(w, http.StatusOK, map[string]string{
			"status":      "success",
			"token":       token,
			"userId":      userID,
			"communityId": communityID,
			"username":    req.Username,
			"role":        "owner",
			"masterKey":   "this-is-a-32-byte-master-key-xyz",
		})
		return
	}

	// Non-dev mode fallback
	userID := "user-" + req.Username
	communityID := "comm-" + req.Username
	token, err := auth.GenerateJWT(userID, communityID, req.Username, 7*24*time.Hour)
	if err != nil {
		http.Error(w, "Failed to generate token", http.StatusInternalServerError)
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{
		"status":      "success",
		"token":       token,
		"userId":      userID,
		"communityId": communityID,
		"username":    req.Username,
		"role":        "owner",
		"masterKey":   "this-is-a-32-byte-master-key-xyz",
	})
}

func (g *Gateway) handleRegisterPasskeyBegin(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Username    string `json:"username"`
		DisplayName string `json:"displayName"`
		Email       string `json:"email"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}

	options, err := webauthn.CreateRegistrationOptions(
		"user-"+req.Username,
		req.Username,
		req.DisplayName,
		"ArmaAdmin Zero-Knowledge Cloud",
		"localhost",
	)
	if err != nil {
		http.Error(w, "Failed to create options", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(options)
}

func (g *Gateway) handleRegisterPasskeyFinish(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, map[string]string{
		"status":  "success",
		"message": "Passkey registered successfully",
	})
}

// ============================================================================
// PASSKEY MANAGEMENT (authenticated: list, add, delete)
// ============================================================================

func detectDeviceName(userAgent string) string {
	ua := strings.ToLower(userAgent)
	var os string
	switch {
	case strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad"):
		os = "iPhone/iPad"
	case strings.Contains(ua, "android"):
		os = "Android"
	case strings.Contains(ua, "mac os x"):
		os = "macOS"
	case strings.Contains(ua, "windows"):
		os = "Windows"
	case strings.Contains(ua, "linux"):
		os = "Linux"
	default:
		os = "Unknown Device"
	}
	var browser string
	switch {
	case strings.Contains(ua, "firefox"):
		browser = "Firefox"
	case strings.Contains(ua, "edg/"):
		browser = "Edge"
	case strings.Contains(ua, "chrome"):
		browser = "Chrome"
	case strings.Contains(ua, "safari"):
		browser = "Safari"
	default:
		browser = "Browser"
	}
	return browser + " on " + os
}

func (g *Gateway) handlePasskeys(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		claims, err := g.requireAuth(r)
		if err != nil {
			writeError(w, http.StatusUnauthorized, "Unauthorized")
			return
		}
		g.store.mu.RLock()
		var list []DevPasskey
		for _, pk := range g.store.passkeys {
			if pk.UserID == claims.UserID {
				list = append(list, *pk)
			}
		}
		g.store.mu.RUnlock()
		if list == nil {
			list = []DevPasskey{}
		}
		sort.Slice(list, func(i, j int) bool { return list[i].CreatedAt.Before(list[j].CreatedAt) })
		writeJSON(w, http.StatusOK, map[string]interface{}{"passkeys": list})

	case http.MethodDelete:
		claims, err := g.requireAuth(r)
		if err != nil {
			writeError(w, http.StatusUnauthorized, "Unauthorized")
			return
		}
		id := r.URL.Query().Get("id")
		if id == "" {
			writeError(w, http.StatusBadRequest, "Missing id")
			return
		}
		g.store.mu.Lock()
		pk, exists := g.store.passkeys[id]
		if !exists || pk.UserID != claims.UserID {
			g.store.mu.Unlock()
			writeError(w, http.StatusNotFound, "Passkey not found")
			return
		}
		delete(g.store.passkeys, id)
		g.store.mu.Unlock()
		writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func (g *Gateway) handleAddPasskeyBegin(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	claims, err := g.requireAuth(r)
	if err != nil {
		writeError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}
	g.store.mu.RLock()
	user, exists := g.store.users[claims.UserID]
	g.store.mu.RUnlock()
	if !exists {
		writeError(w, http.StatusNotFound, "User not found")
		return
	}
	options, err := webauthn.CreateRegistrationOptions(
		user.ID, user.Username, user.Username,
		"ArmaAdmin Zero-Knowledge Cloud", "localhost",
	)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "Failed to create options")
		return
	}
	writeJSON(w, http.StatusOK, options)
}

func (g *Gateway) handleAddPasskeyFinish(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	claims, err := g.requireAuth(r)
	if err != nil {
		writeError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}
	var req struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}
	json.NewDecoder(r.Body).Decode(&req)

	name := req.Name
	if name == "" {
		name = detectDeviceName(r.Header.Get("User-Agent"))
	}

	pk := &DevPasskey{
		ID:           uuid.New().String(),
		UserID:       claims.UserID,
		CommunityID:  claims.CommunityID,
		CredentialID: req.ID,
		Name:         name,
		CreatedAt:    time.Now(),
	}
	g.store.mu.Lock()
	g.store.passkeys[pk.ID] = pk
	g.store.mu.Unlock()

	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "passkey": pk})
}

// ============================================================================
// NODES
// ============================================================================

func (g *Gateway) handleNodes(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		writeError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	switch r.Method {
	case http.MethodGet:
		g.store.mu.RLock()
		var result []map[string]interface{}
		for _, n := range g.store.nodes {
			if n.CommunityID == claims.CommunityID {
				result = append(result, map[string]interface{}{
					"id":        n.ID,
					"name":      n.Name,
					"type":      n.Type,
					"endpoint":  n.Endpoint,
					"status":    n.Status,
					"version":   n.Version,
					"lastSeen":  n.LastSeen,
					"createdAt": n.CreatedAt,
				})
			}
		}
		g.store.mu.RUnlock()
		if result == nil {
			result = []map[string]interface{}{}
		}
		writeJSON(w, http.StatusOK, map[string]interface{}{"nodes": result})

	case http.MethodPost:
		var req struct {
			Name     string `json:"name"`
			Type     string `json:"type"`
			Endpoint string `json:"endpoint"`
		}
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, "Invalid request", http.StatusBadRequest)
			return
		}
		req.Name = strings.TrimSpace(req.Name)
		if req.Name == "" {
			writeError(w, http.StatusBadRequest, "name is required")
			return
		}
		if req.Type != "storage" && req.Type != "worker" {
			writeError(w, http.StatusBadRequest, "type must be 'storage' or 'worker'")
			return
		}
		token := generateNodeToken()
		node := &DevNode{
			ID:          uuid.NewString(),
			CommunityID: claims.CommunityID,
			Name:        req.Name,
			Type:        req.Type,
			Token:       token,
			Endpoint:    req.Endpoint,
			Status:      "pending",
			CreatedAt:   time.Now(),
		}
		g.store.mu.Lock()
		g.store.nodes[node.ID] = node
		g.store.mu.Unlock()
		writeJSON(w, http.StatusCreated, map[string]interface{}{
			"id":        node.ID,
			"name":      node.Name,
			"type":      node.Type,
			"endpoint":  node.Endpoint,
			"status":    node.Status,
			"version":   node.Version,
			"lastSeen":  node.LastSeen,
			"createdAt": node.CreatedAt,
			"token":     token,
		})

	case http.MethodDelete:
		nodeID := r.URL.Query().Get("id")
		if nodeID == "" {
			writeError(w, http.StatusBadRequest, "Missing id")
			return
		}
		g.store.mu.Lock()
		node, exists := g.store.nodes[nodeID]
		if !exists || node.CommunityID != claims.CommunityID {
			g.store.mu.Unlock()
			writeError(w, http.StatusNotFound, "Node not found")
			return
		}
		delete(g.store.nodes, nodeID)
		g.store.mu.Unlock()
		writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func (g *Gateway) handleNodeWebSocket(w http.ResponseWriter, r *http.Request) {
	token := r.URL.Query().Get("token")
	if token == "" {
		http.Error(w, "Missing token", http.StatusUnauthorized)
		return
	}

	g.store.mu.RLock()
	var node *DevNode
	for _, n := range g.store.nodes {
		if n.Token == token {
			node = n
			break
		}
	}
	g.store.mu.RUnlock()

	if node == nil {
		http.Error(w, "Invalid token", http.StatusUnauthorized)
		return
	}

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Printf("[NODE] WebSocket upgrade failed for node %s: %v", node.Name, err)
		return
	}
	defer conn.Close()

	now := time.Now()
	g.store.mu.Lock()
	node.Status = "online"
	node.LastSeen = &now
	g.store.mu.Unlock()
	log.Printf("[NODE] %s (%s) connected via WebSocket", node.Name, node.ID)

	conn.WriteJSON(map[string]string{"type": "connected", "nodeId": node.ID, "name": node.Name})

	for {
		var msg struct {
			Type    string `json:"type"`
			Version string `json:"version"`
		}
		if err := conn.ReadJSON(&msg); err != nil {
			break
		}
		if msg.Type == "heartbeat" {
			hbNow := time.Now()
			g.store.mu.Lock()
			node.LastSeen = &hbNow
			node.Status = "online"
			if msg.Version != "" {
				node.Version = msg.Version
			}
			g.store.mu.Unlock()
		}
	}

	g.store.mu.Lock()
	node.Status = "offline"
	g.store.mu.Unlock()
	log.Printf("[NODE] %s (%s) disconnected", node.Name, node.ID)
}

func (g *Gateway) handleDevNodePing(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	if !g.devMode {
		http.Error(w, "Not found", http.StatusNotFound)
		return
	}
	nodeID := r.URL.Query().Get("id")
	if nodeID == "" {
		writeError(w, http.StatusBadRequest, "Missing id")
		return
	}
	g.store.mu.Lock()
	node, exists := g.store.nodes[nodeID]
	if !exists {
		g.store.mu.Unlock()
		writeError(w, http.StatusNotFound, "Node not found")
		return
	}
	now := time.Now()
	node.LastSeen = &now
	node.Status = "online"
	token := node.Token
	name := node.Name
	g.store.mu.Unlock()

	writeJSON(w, http.StatusOK, map[string]string{
		"status": "pinged",
		"nodeId": nodeID,
		"name":   name,
		"token":  token,
	})
}

func (g *Gateway) monitorNodes() {
	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()
	for range ticker.C {
		now := time.Now()
		g.store.mu.Lock()
		for _, node := range g.store.nodes {
			if node.Status == "online" && node.LastSeen != nil && now.Sub(*node.LastSeen) > 90*time.Second {
				node.Status = "offline"
				log.Printf("[NODE] Node %s (%s) went offline", node.Name, node.ID)
			}
		}
		g.store.mu.Unlock()
	}
}

// ============================================================================
// LOGIN
// ============================================================================

func (g *Gateway) handleLoginPassword(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}

	if g.devMode {
		g.store.mu.RLock()
		user, exists := g.store.usersByName[strings.ToLower(req.Username)]

		var userMemberships []DevMembership
		if exists {
			for _, m := range g.store.memberships {
				if m.UserID == user.ID {
					userMemberships = append(userMemberships, m)
				}
			}
		}
		g.store.mu.RUnlock()

		if !exists || len(userMemberships) == 0 {
			writeError(w, http.StatusUnauthorized, "Invalid credentials or no community access")
			return
		}

		ok, err := auth.VerifyPassword(req.Password, user.PasswordHash)
		if err != nil || !ok {
			writeError(w, http.StatusUnauthorized, "Invalid credentials")
			return
		}

		// Use the first community as default for the initial token
		defaultMembership := userMemberships[0]

		// Map community names
		type communityInfo struct {
			ID   string `json:"id"`
			Name string `json:"name"`
			Role string `json:"role"`
		}
		var communities []communityInfo
		g.store.mu.RLock()
		for _, m := range userMemberships {
			if comm, ok := g.store.communities[m.CommunityID]; ok {
				communities = append(communities, communityInfo{
					ID:   comm.ID,
					Name: comm.Name,
					Role: m.Role,
				})
			}
		}
		g.store.mu.RUnlock()

		token, err := auth.GenerateJWT(user.ID, defaultMembership.CommunityID, user.Username, 7*24*time.Hour)
		if err != nil {
			http.Error(w, "Failed to generate token", http.StatusInternalServerError)
			return
		}

		writeJSON(w, http.StatusOK, map[string]interface{}{
			"status":      "success",
			"token":       token,
			"userId":      user.ID,
			"communityId": defaultMembership.CommunityID,
			"username":    user.Username,
			"role":        defaultMembership.Role,
			"communities": communities,
			"masterKey":   "this-is-a-32-byte-master-key-xyz",
		})
		return
	}

	// Non-dev mode: demo fallback
	log.Printf("[DEMO] Login attempt for user: %s (auto-accepting)", req.Username)
	userID := "user-" + req.Username
	communityID := "comm-" + req.Username
	token, err := auth.GenerateJWT(userID, communityID, req.Username, 7*24*time.Hour)
	if err != nil {
		http.Error(w, "Failed to generate token", http.StatusInternalServerError)
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{
		"status":      "success",
		"token":       token,
		"userId":      userID,
		"communityId": communityID,
		"username":    req.Username,
		"role":        "owner",
		"masterKey":   "this-is-a-32-byte-master-key-xyz",
	})
}

func (g *Gateway) handleLoginPasskeyBegin(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Username string `json:"username"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}

	options, err := webauthn.CreateAuthenticationOptions("localhost", []string{})
	if err != nil {
		http.Error(w, "Failed to create options", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(options)
}

func (g *Gateway) handleLoginPasskeyFinish(w http.ResponseWriter, r *http.Request) {
	token, err := auth.GenerateJWT("user-demo", "comm-demo", "demo", 7*24*time.Hour)
	if err != nil {
		http.Error(w, "Failed to generate token", http.StatusInternalServerError)
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{
		"status":      "success",
		"token":       token,
		"userId":      "user-demo",
		"communityId": "comm-demo",
		"username":    "demo",
		"role":        "owner",
		"masterKey":   "this-is-a-32-byte-master-key-xyz",
	})
}

// ============================================================================
// SESSION & PROFILE
// ============================================================================

func (g *Gateway) handleLogout(w http.ResponseWriter, r *http.Request) {
	// TODO: Invalidate session in database
	writeJSON(w, http.StatusOK, map[string]string{"status": "logged out"})
}

func (g *Gateway) handleMe(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	resp := map[string]string{
		"userId":      claims.UserID,
		"username":    claims.Username,
		"communityId": claims.CommunityID,
		"role":        "owner",
	}

	if g.devMode {
		g.store.mu.RLock()
		if user, exists := g.store.users[claims.UserID]; exists {
			resp["role"] = user.Role
			resp["email"] = user.Email
		}
		g.store.mu.RUnlock()
	}

	writeJSON(w, http.StatusOK, resp)
}

func (g *Gateway) handleProfile(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	switch r.Method {
	case http.MethodGet:
		resp := map[string]string{
			"id":          claims.UserID,
			"username":    claims.Username,
			"communityId": claims.CommunityID,
			"email":       "",
			"role":        "owner",
		}
		if g.devMode {
			g.store.mu.RLock()
			if user, exists := g.store.users[claims.UserID]; exists {
				resp["email"] = user.Email
				resp["role"] = user.Role
			}
			g.store.mu.RUnlock()
		}
		writeJSON(w, http.StatusOK, resp)

	case http.MethodPut:
		var req struct {
			Username    string `json:"username"`
			Email       string `json:"email"`
			OldPassword string `json:"oldPassword"`
			NewPassword string `json:"newPassword"`
		}
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, "Invalid request", http.StatusBadRequest)
			return
		}

		if !g.devMode {
			writeError(w, http.StatusNotImplemented, "Profile update requires database")
			return
		}

		g.store.mu.Lock()
		defer g.store.mu.Unlock()

		user, exists := g.store.users[claims.UserID]
		if !exists {
			http.Error(w, "User not found", http.StatusNotFound)
			return
		}

		if req.Username != "" && req.Username != user.Username {
			newKey := strings.ToLower(req.Username)
			if _, taken := g.store.usersByName[newKey]; taken {
				writeError(w, http.StatusConflict, "Username already taken")
				return
			}
			delete(g.store.usersByName, strings.ToLower(user.Username))
			user.Username = req.Username
			g.store.usersByName[newKey] = user
		}

		if req.Email != "" {
			user.Email = req.Email
		}

		if req.NewPassword != "" {
			ok, err := auth.VerifyPassword(req.OldPassword, user.PasswordHash)
			if err != nil || !ok {
				writeError(w, http.StatusUnauthorized, "Current password is incorrect")
				return
			}
			if err := auth.ValidatePasswordStrength(req.NewPassword); err != nil {
				writeError(w, http.StatusBadRequest, err.Error())
				return
			}
			hash, err := auth.HashPassword(req.NewPassword)
			if err != nil {
				http.Error(w, "Failed to hash password", http.StatusInternalServerError)
				return
			}
			user.PasswordHash = hash
		}

		token, err := auth.GenerateJWT(user.ID, user.CommunityID, user.Username, 7*24*time.Hour)
		if err != nil {
			http.Error(w, "Failed to generate token", http.StatusInternalServerError)
			return
		}

		writeJSON(w, http.StatusOK, map[string]string{
			"status":      "updated",
			"token":       token,
			"username":    user.Username,
			"email":       user.Email,
			"role":        user.Role,
			"communityId": user.CommunityID,
		})

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

// ============================================================================
// SERVER MANAGEMENT
// ============================================================================

func (g *Gateway) handleServers(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	switch r.Method {
	case http.MethodGet:
		g.store.mu.RLock()
		var result []*DevServer
		for _, s := range g.store.servers {
			if s.CommunityID == claims.CommunityID {
				result = append(result, s)
			}
		}
		g.store.mu.RUnlock()
		if result == nil {
			result = []*DevServer{}
		}
		writeJSON(w, http.StatusOK, map[string]interface{}{"servers": result})

	case http.MethodPost:
		var req struct {
			Name                  string `json:"name"`
			Description           string `json:"description"`
			WorkerID              string `json:"workerId"`
			StorageID             string `json:"storageId"`
			LogPath               string `json:"logPath"`
			EncryptedRcon         string `json:"encryptedRcon"`
			EncryptedAutoMessages string `json:"encryptedAutoMessages"`
		}
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, "Invalid request", http.StatusBadRequest)
			return
		}
		req.Name = strings.TrimSpace(req.Name)
		if req.Name == "" {
			writeError(w, http.StatusBadRequest, "Server name is required")
			return
		}

		g.store.mu.Lock()
		isFirst := true
		for _, s := range g.store.servers {
			if s.CommunityID == claims.CommunityID {
				isFirst = false
				break
			}
		}
		serverID := uuid.NewString()
		server := &DevServer{
			ID:                    serverID,
			CommunityID:           claims.CommunityID,
			Name:                  req.Name,
			Description:           req.Description,
			WorkerID:              req.WorkerID,
			StorageID:             req.StorageID,
			LogPath:               req.LogPath,
			EncryptedRcon:         req.EncryptedRcon,
			EncryptedAutoMessages: req.EncryptedAutoMessages,
			MockActive:            g.devMode && isFirst,
			CreatedAt:             time.Now(),
		}
		g.store.servers[serverID] = server
		g.store.mu.Unlock()

		if g.devMode && isFirst {
			g.startMock(claims.CommunityID, serverID)
		}

		writeJSON(w, http.StatusCreated, server)

	case http.MethodPut:
		var req struct {
			ID                    string `json:"id"`
			Name                  string `json:"name"`
			Description           string `json:"description"`
			WorkerID              string `json:"workerId"`
			StorageID             string `json:"storageId"`
			LogPath               string `json:"logPath"`
			EncryptedRcon         string `json:"encryptedRcon"`
			EncryptedAutoMessages string `json:"encryptedAutoMessages"`
		}
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, "Invalid request", http.StatusBadRequest)
			return
		}

		g.store.mu.Lock()
		defer g.store.mu.Unlock()

		server, exists := g.store.servers[req.ID]
		if !exists || server.CommunityID != claims.CommunityID {
			http.Error(w, "Server not found", http.StatusNotFound)
			return
		}

		if req.Name != "" {
			server.Name = req.Name
		}
		server.Description = req.Description
		server.WorkerID = req.WorkerID
		server.StorageID = req.StorageID
		server.LogPath = req.LogPath
		if req.EncryptedRcon != "" {
			server.EncryptedRcon = req.EncryptedRcon
		}
		if req.EncryptedAutoMessages != "" {
			server.EncryptedAutoMessages = req.EncryptedAutoMessages
		}

		// Broadcast update to Workers and Dashboards
		updateMsg, _ := json.Marshal(map[string]interface{}{
			"type":   "CONFIG_UPDATE",
			"server": server,
		})
		g.broadcast(websocket.TextMessage, updateMsg)

		writeJSON(w, http.StatusOK, server)

	case http.MethodDelete:
		serverID := r.URL.Query().Get("id")
		if serverID == "" {
			http.Error(w, "Missing id", http.StatusBadRequest)
			return
		}

		g.store.mu.Lock()
		server, exists := g.store.servers[serverID]
		if !exists || server.CommunityID != claims.CommunityID {
			g.store.mu.Unlock()
			http.Error(w, "Not found", http.StatusNotFound)
			return
		}
		delete(g.store.servers, serverID)
		hasMore := false
		for _, s := range g.store.servers {
			if s.CommunityID == claims.CommunityID {
				hasMore = true
				break
			}
		}
		g.store.mu.Unlock()

		if !hasMore {
			g.stopMock(claims.CommunityID)
		}

		writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

// ============================================================================
// USER SEARCH
// ============================================================================

func (g *Gateway) handleUserSearch(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	query := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))

	g.store.mu.RLock()
	defer g.store.mu.RUnlock()

	type userResult struct {
		ID       string `json:"id"`
		Username string `json:"username"`
		Email    string `json:"email"`
		Role     string `json:"role"`
	}

	var results []userResult
	for _, u := range g.store.users {
		if u.CommunityID == claims.CommunityID && u.ID != claims.UserID {
			if query == "" || strings.Contains(strings.ToLower(u.Username), query) {
				results = append(results, userResult{
					ID:       u.ID,
					Username: u.Username,
					Email:    u.Email,
					Role:     u.Role,
				})
			}
		}
	}
	if results == nil {
		results = []userResult{}
	}

	writeJSON(w, http.StatusOK, map[string]interface{}{"users": results})
}

// ============================================================================
// PERMISSIONS
// ============================================================================

func (g *Gateway) handlePermissions(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	switch r.Method {
	case http.MethodGet:
		userID := r.URL.Query().Get("userId")
		g.store.mu.RLock()
		var result []*DevPermission
		for _, p := range g.store.permissions {
			if p.CommunityID == claims.CommunityID {
				if userID == "" || p.UserID == userID {
					result = append(result, p)
				}
			}
		}
		g.store.mu.RUnlock()
		if result == nil {
			result = []*DevPermission{}
		}
		writeJSON(w, http.StatusOK, map[string]interface{}{"permissions": result})

	case http.MethodPost:
		var req struct {
			UserID   string   `json:"userId"`
			ServerID string   `json:"serverId"`
			Scopes   []string `json:"scopes"`
		}
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, "Invalid request", http.StatusBadRequest)
			return
		}
		if req.UserID == "" {
			writeError(w, http.StatusBadRequest, "userId is required")
			return
		}

		g.store.mu.Lock()
		defer g.store.mu.Unlock()

		targetUser, exists := g.store.users[req.UserID]
		if !exists {
			writeError(w, http.StatusNotFound, "User not found")
			return
		}

		serverName := "All Servers"
		if req.ServerID != "" {
			if s, ok := g.store.servers[req.ServerID]; ok {
				serverName = s.Name
			}
		}

		// Upsert: update if same user+server combo exists
		for _, p := range g.store.permissions {
			if p.UserID == req.UserID && p.ServerID == req.ServerID && p.CommunityID == claims.CommunityID {
				p.Scopes = req.Scopes
				p.GrantedBy = claims.Username
				p.GrantedAt = time.Now()
				writeJSON(w, http.StatusOK, p)
				return
			}
		}

		perm := &DevPermission{
			ID:          uuid.NewString(),
			CommunityID: claims.CommunityID,
			UserID:      req.UserID,
			Username:    targetUser.Username,
			ServerID:    req.ServerID,
			ServerName:  serverName,
			Scopes:      req.Scopes,
			GrantedBy:   claims.Username,
			GrantedAt:   time.Now(),
		}
		g.store.permissions[perm.ID] = perm
		writeJSON(w, http.StatusCreated, perm)

	case http.MethodDelete:
		permID := r.URL.Query().Get("id")
		if permID == "" {
			http.Error(w, "Missing id", http.StatusBadRequest)
			return
		}
		g.store.mu.Lock()
		defer g.store.mu.Unlock()
		perm, exists := g.store.permissions[permID]
		if !exists || perm.CommunityID != claims.CommunityID {
			http.Error(w, "Not found", http.StatusNotFound)
			return
		}
		delete(g.store.permissions, permID)
		writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"})

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

// ============================================================================
// PLAYER NOTES & WARNINGS
// ============================================================================

func (g *Gateway) handlePlayerNotes(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	switch r.Method {
	case http.MethodGet:
		playerNameHash := r.URL.Query().Get("playerNameHash")
		if playerNameHash == "" {
			writeError(w, http.StatusBadRequest, "playerNameHash is required")
			return
		}

		g.store.mu.RLock()
		defer g.store.mu.RUnlock()

		var results []DevPlayerNote
		for _, note := range g.store.playerNotes {
			if note.CommunityID == claims.CommunityID && note.PlayerNameHash == playerNameHash {
				results = append(results, note)
			}
		}
		if results == nil {
			results = []DevPlayerNote{}
		}
		writeJSON(w, http.StatusOK, map[string]interface{}{"notes": results})

	case http.MethodPost:
		var req struct {
			PlayerNameHash string `json:"playerNameHash"`
			Category       string `json:"category"`
			Content        string `json:"content"`
		}
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, "Invalid request", http.StatusBadRequest)
			return
		}

		note := DevPlayerNote{
			ID:             uuid.NewString(),
			CommunityID:    claims.CommunityID,
			PlayerNameHash: req.PlayerNameHash,
			Category:       req.Category,
			Content:        req.Content,
			CreatedBy:      claims.Username,
			CreatedAt:      time.Now(),
		}

		g.store.mu.Lock()
		g.store.playerNotes = append(g.store.playerNotes, note)
		g.store.mu.Unlock()

		writeJSON(w, http.StatusCreated, note)

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func (g *Gateway) handlePlayers(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	g.store.mu.RLock()
	defer g.store.mu.RUnlock()

	type playerWithNotes struct {
		*DevPlayer
		WarningCount int `json:"warningCount"`
	}

	var result []playerWithNotes
	for _, p := range g.store.players {
		if p.CommunityID == claims.CommunityID {
			warnings := 0
			for _, n := range g.store.playerNotes {
				if n.CommunityID == claims.CommunityID && n.PlayerNameHash == p.NameHash && n.Category == "warning" {
					warnings++
				}
			}
			result = append(result, playerWithNotes{DevPlayer: p, WarningCount: warnings})
		}
	}

	if result == nil {
		result = []playerWithNotes{}
	}
	writeJSON(w, http.StatusOK, map[string]interface{}{"players": result})
}

func (g *Gateway) handleKick(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	var req struct {
		PlayerNameHash string `json:"playerNameHash"`
		Reason         string `json:"reason"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}
	if req.PlayerNameHash == "" {
		writeError(w, http.StatusBadRequest, "playerNameHash is required")
		return
	}

	key := claims.CommunityID + ":" + req.PlayerNameHash

	g.store.mu.Lock()
	_, exists := g.store.players[key]
	if !exists {
		g.store.mu.Unlock()
		writeError(w, http.StatusNotFound, "Player not online")
		return
	}
	delete(g.store.players, key)
	g.store.mu.Unlock()

	// Broadcast a LEAVE event so the log stream shows the kick
	reason := req.Reason
	if reason == "" {
		reason = "Kicked by admin"
	}
	leaveEvent := &parser.LogEvent{
		Type:    "LEAVE",
		Content: "Player left the server [KICK: " + reason + "]",
	}
	g.broadcastEvent(leaveEvent, "server-123")

	log.Printf("[DEV] Kicked player %s (reason: %s) by %s", req.PlayerNameHash, reason, claims.Username)
	writeJSON(w, http.StatusOK, map[string]string{"status": "kicked", "playerNameHash": req.PlayerNameHash})
}

func (g *Gateway) handleBan(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	var req struct {
		PlayerNameHash  string `json:"playerNameHash"`
		Data            string `json:"data"`            // Encrypted JSON
		DurationMinutes int    `json:"durationMinutes"` // 0 = permanent
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}
	if req.PlayerNameHash == "" {
		writeError(w, http.StatusBadRequest, "playerNameHash is required")
		return
	}

	var expiresAt *time.Time
	if req.DurationMinutes > 0 {
		t := time.Now().Add(time.Duration(req.DurationMinutes) * time.Minute)
		expiresAt = &t
	}

	ban := &DevBan{
		ID:             uuid.NewString(),
		CommunityID:    claims.CommunityID,
		PlayerNameHash: req.PlayerNameHash,
		Data:           req.Data,
		BannedAt:       time.Now(),
		ExpiresAt:      expiresAt,
	}

	g.store.mu.Lock()
	g.store.bans[ban.ID] = ban
	// Also remove from online players (kick)
	delete(g.store.players, claims.CommunityID+":"+req.PlayerNameHash)
	g.store.mu.Unlock()

	// Broadcast LEAVE event
	leaveEvent := &parser.LogEvent{
		Type:    "LEAVE",
		Content: "Player left the server [BAN]",
	}
	g.broadcastEvent(leaveEvent, "server-123")

	log.Printf("[DEV] Banned player %s (duration: %dmin) by %s", req.PlayerNameHash, req.DurationMinutes, claims.Username)
	writeJSON(w, http.StatusCreated, ban)
}

func (g *Gateway) handleBans(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	switch r.Method {
	case http.MethodGet:
		g.store.mu.RLock()
		var result []*DevBan
		for _, b := range g.store.bans {
			if b.CommunityID == claims.CommunityID {
				// Auto-expire
				if b.ExpiresAt == nil || b.ExpiresAt.After(time.Now()) {
					result = append(result, b)
				}
			}
		}
		g.store.mu.RUnlock()
		if result == nil {
			result = []*DevBan{}
		}
		writeJSON(w, http.StatusOK, map[string]interface{}{"bans": result})

	case http.MethodDelete:
		banID := r.URL.Query().Get("id")
		if banID == "" {
			http.Error(w, "Missing id", http.StatusBadRequest)
			return
		}
		g.store.mu.Lock()
		ban, exists := g.store.bans[banID]
		if !exists || ban.CommunityID != claims.CommunityID {
			g.store.mu.Unlock()
			http.Error(w, "Not found", http.StatusNotFound)
			return
		}
		delete(g.store.bans, banID)
		g.store.mu.Unlock()
		writeJSON(w, http.StatusOK, map[string]string{"status": "unbanned"})

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

// ============================================================================
// REMAINING ENDPOINTS
// ============================================================================

func (g *Gateway) handlePlayerSearch(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	g.store.mu.RLock()
	defer g.store.mu.RUnlock()

	type playerWithNotes struct {
		*DevPlayer
		WarningCount int `json:"warningCount"`
	}

	var result []playerWithNotes
	for _, p := range g.store.roster {
		if p.CommunityID == claims.CommunityID {
			warnings := 0
			for _, n := range g.store.playerNotes {
				if n.CommunityID == claims.CommunityID && n.PlayerNameHash == p.NameHash && n.Category == "warning" {
					warnings++
				}
			}
			result = append(result, playerWithNotes{DevPlayer: p, WarningCount: warnings})
		}
	}

	if result == nil {
		result = []playerWithNotes{}
	}
	writeJSON(w, http.StatusOK, map[string]interface{}{"results": result})
}

func (g *Gateway) handleLogs(w http.ResponseWriter, r *http.Request) {
	claims, err := g.requireAuth(r)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	g.store.mu.RLock()
	var result []LogEntry
	for _, entry := range g.store.logs {
		if entry.CommunityID == claims.CommunityID {
			result = append(result, entry)
		}
	}
	g.store.mu.RUnlock()

	if result == nil {
		result = []LogEntry{}
	}
	writeJSON(w, http.StatusOK, map[string]interface{}{"logs": result})
}

func (g *Gateway) handleDSGVOExport(w http.ResponseWriter, r *http.Request) {
	playerID := r.URL.Query().Get("playerId")
	if playerID == "" {
		http.Error(w, "Missing playerId", http.StatusBadRequest)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Content-Disposition", "attachment; filename=player-data.json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"playerId":   playerID,
		"exportDate": time.Now().Format(time.RFC3339),
		"logs":       []interface{}{},
	})
}

func (g *Gateway) handleDSGVODelete(w http.ResponseWriter, r *http.Request) {
	var req struct {
		PlayerID string `json:"playerId"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}

func (g *Gateway) handleIngest(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	apiKey := r.Header.Get("X-API-Key")
	if apiKey == "" {
		http.Error(w, "Missing API key", http.StatusUnauthorized)
		return
	}
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Failed to read body", http.StatusBadRequest)
		return
	}
	communityID := "comm-123-abc"
	g.natsClient.PublishLog(context.Background(), communityID, "api", body)
	w.WriteHeader(http.StatusAccepted)
	w.Write([]byte("OK"))
}

// ============================================================================
// MAIN
// ============================================================================

func main() {
	natsURL := os.Getenv("NATS_URL")
	if natsURL == "" {
		natsURL = "nats://localhost:4222"
	}

	devMode := os.Getenv("DEV_MODE") == "true"
	if devMode {
		log.Println("[DEV] Development mode enabled — using in-memory store")
	}

	nc, err := internal_nats.Connect(natsURL)
	if err != nil {
		log.Fatalf("NATS error: %v", err)
	}
	defer nc.Close()

	nc.SetupStream(context.Background(), "LOGS", []string{"logs.>"})

	gateway := &Gateway{
		natsClient: nc,
		dashboards: make(map[*websocket.Conn]bool),
		devMode:    devMode,
		store:      newDevStore(),
	}

	go func() {
		nc.Conn.Subscribe("logs.>", func(m *nats.Msg) {
			gateway.broadcast(websocket.BinaryMessage, m.Data)
		})
		nc.Conn.Subscribe("telemetry.>", func(m *nats.Msg) {
			gateway.broadcast(websocket.TextMessage, m.Data)
		})
	}()

	http.HandleFunc("/ws", gateway.handleWebSocket)

	http.HandleFunc("/api/auth/register", gateway.handleRegister)
	http.HandleFunc("/api/auth/register/passkey/begin", gateway.handleRegisterPasskeyBegin)
	http.HandleFunc("/api/auth/register/passkey/finish", gateway.handleRegisterPasskeyFinish)

	http.HandleFunc("/api/auth/login/password", gateway.handleLoginPassword)
	http.HandleFunc("/api/auth/login/passkey/begin", gateway.handleLoginPasskeyBegin)
	http.HandleFunc("/api/auth/login/passkey/finish", gateway.handleLoginPasskeyFinish)

	http.HandleFunc("/api/auth/logout", gateway.handleLogout)
	http.HandleFunc("/api/auth/me", gateway.handleMe)
	http.HandleFunc("/api/auth/profile", gateway.handleProfile)
	http.HandleFunc("/api/auth/passkeys", gateway.handlePasskeys)
	http.HandleFunc("/api/auth/passkeys/begin", gateway.handleAddPasskeyBegin)
	http.HandleFunc("/api/auth/passkeys/finish", gateway.handleAddPasskeyFinish)

	go gateway.monitorNodes()

	http.HandleFunc("/api/nodes", gateway.handleNodes)
	http.HandleFunc("/ws/node", gateway.handleNodeWebSocket)
	if devMode {
		http.HandleFunc("/api/dev/nodes/ping", gateway.handleDevNodePing)
	}

	http.HandleFunc("/api/servers", gateway.handleServers)
	http.HandleFunc("/api/users/search", gateway.handleUserSearch)
	http.HandleFunc("/api/permissions", gateway.handlePermissions)

	http.HandleFunc("/api/players", gateway.handlePlayers)
	http.HandleFunc("/api/players/kick", gateway.handleKick)
	http.HandleFunc("/api/players/ban", gateway.handleBan)
	http.HandleFunc("/api/bans", gateway.handleBans)
	http.HandleFunc("/api/players/search", gateway.handlePlayerSearch)
	http.HandleFunc("/api/logs", gateway.handleLogs)
	http.HandleFunc("/api/dsgvo/export", gateway.handleDSGVOExport)
	http.HandleFunc("/api/dsgvo/delete", gateway.handleDSGVODelete)
	http.HandleFunc("/api/ingest", gateway.handleIngest)

	log.Println("Gateway listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
