package main

import (
	cryptoRand "crypto/rand"
	"encoding/hex"
	"flag"
	"log"
	"math/rand"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/websocket"
	"github.com/vmihailenco/msgpack/v5"
)

// -------------------- Config --------------------
const (
	AddrHTTP      = ":8080"
	MaxPlayers    = 10
	TickRateHz    = 15
	NameMaxLen    = 25
	ChatMaxLen    = 500
	StateBufLimit = 256

	// Chat Anti-spam
	MinChatInterval = 400 * time.Millisecond
)

// Allowed specified origins for browser WS connections
var allowedOrigins = map[string]bool{
	"https://world.seppjm.com": true,
	"https://seppjm.com":       true,
	"https://cv.seppjm.com":    true,
	"https://seppdroid.com":    true,
	"https://sepp.mx":          true,
	//"http://world.seppjm.com":  true, // (dev)
	//"http://localhost:5173":    true, // (dev)
	//"http://localhost:8080":    true, // (dev)
}

// -------------------- Types --------------------
type Role string

const (
	RolePlayer    Role = "player"
	RoleSpectator Role = "spectator"
)

type Vec3 [3]float64

type Player struct {
	ID   string  `msgpack:"id"`
	Name string  `msgpack:"name"`
	Pos  Vec3    `msgpack:"pos"`
	RotY float64 `msgpack:"rotY"`
}

type Client struct {
	Conn      *websocket.Conn
	Hub       *Hub
	ID        string
	Name      string
	Role      Role
	Send      chan []byte
	Closed    chan struct{}
	LastChat  time.Time
	connected time.Time
}

type Hub struct {
	mu        sync.RWMutex
	clients   map[string]*Client // id -> client
	players   map[string]*Player // id -> player
	joinCh    chan *Client
	leaveCh   chan *Client
	broadcast chan []byte
}

func NewHub() *Hub {
	return &Hub{
		clients:   make(map[string]*Client),
		players:   make(map[string]*Player),
		joinCh:    make(chan *Client, 64),
		leaveCh:   make(chan *Client, 64),
		broadcast: make(chan []byte, 1024),
	}
}

func (h *Hub) Run() {
	ticker := time.NewTicker(time.Second / TickRateHz)
	defer ticker.Stop()
	for {
		select {
		case c := <-h.joinCh:
			h.addClient(c)
		case c := <-h.leaveCh:
			h.removeClient(c)
		case msg := <-h.broadcast:
			h.mu.RLock()
			for _, cl := range h.clients {
				select {
				case cl.Send <- msg:
				default:
				}
			}
			h.mu.RUnlock()
		case <-ticker.C:
			h.tickState()
		}
	}
}

func (h *Hub) addClient(c *Client) {
	h.mu.Lock()
	h.clients[c.ID] = c
	h.mu.Unlock()
}

func (h *Hub) removeClient(c *Client) {
	h.mu.Lock()
	defer h.mu.Unlock()
	delete(h.clients, c.ID)
	if _, ok := h.players[c.ID]; ok {
		delete(h.players, c.ID)
		packet := map[string]any{"type": "player_leave", "id": c.ID}
		if b, _ := msgpack.Marshal(packet); b != nil {
			for _, cl := range h.clients {
				select {
				case cl.Send <- b:
				default:
				}
			}
		}
	}
}

func (h *Hub) tickState() {
	h.mu.RLock()
	if len(h.players) == 0 || len(h.clients) == 0 {
		h.mu.RUnlock()
		return
	}
	updates := make([]map[string]any, 0, len(h.players))
	for _, p := range h.players {
		updates = append(updates, map[string]any{
			"id":   p.ID,
			"pos":  p.Pos,
			"rotY": p.RotY,
		})
	}
	h.mu.RUnlock()

	packet := map[string]any{"type": "state", "updates": updates}
	if b, _ := msgpack.Marshal(packet); b != nil {
		select {
		case h.broadcast <- b:
		default:
		}
	}
}

// -------------------- WS Handling --------------------
var upgrader = websocket.Upgrader{
	ReadBufferSize:    1024,
	WriteBufferSize:   1024,
	EnableCompression: true,
	HandshakeTimeout:  10 * time.Second,
	CheckOrigin: func(r *http.Request) bool {
		origin := r.Header.Get("Origin")
		return allowedOrigins[origin]
	},
}

type inbound struct {
	Type string   `msgpack:"type"`
	Name string   `msgpack:"name"`
	Role Role     `msgpack:"role"`
	Pos  *Vec3    `msgpack:"pos"`
	RotY *float64 `msgpack:"rotY"`
	Text string   `msgpack:"text"`
}

func wsHandler(h *Hub) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			log.Println("upgrade:", err)
			return
		}
		id := genID()
		c := &Client{
			Conn:      conn,
			Hub:       h,
			ID:        id,
			Send:      make(chan []byte, StateBufLimit),
			Closed:    make(chan struct{}),
			connected: time.Now(),
		}
		h.joinCh <- c

		go writer(c)
		reader(c)
	}
}

func reader(c *Client) {
	defer func() {
		c.Hub.leaveCh <- c
		close(c.Closed)
		_ = c.Conn.Close()
	}()

	c.Conn.SetReadLimit(1 << 20)
	_ = c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
	c.Conn.SetPongHandler(func(string) error {
		return c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
	})

	// Expect a join first
	mt, data, err := c.Conn.ReadMessage()
	if err != nil {
		return
	}
	if mt != websocket.BinaryMessage && mt != websocket.TextMessage {
		return
	}
	var first inbound
	if msgpack.Unmarshal(data, &first) != nil || first.Type != "join" {
		sendPacket(c, map[string]any{"type": "error", "message": "expected join"})
		return
	}
	name := sanitizeName(first.Name)
	if name == "" {
		name = randomName()
	}
	role := first.Role
	if role != RoleSpectator {
		role = RolePlayer
	}

	// Enforce player cap
	c.Hub.mu.Lock()
	nPlayers := 0
	for _, cl := range c.Hub.clients {
		if cl.Role == RolePlayer {
			nPlayers++
		}
	}
	if role == RolePlayer && nPlayers >= MaxPlayers {
		c.Hub.mu.Unlock()
		sendPacket(c, map[string]any{"type": "full", "maxPlayers": MaxPlayers})
		return
	}
	c.Name = name
	c.Role = role

	// Register player entity
	if c.Role == RolePlayer {
		spawn := Player{ID: c.ID, Name: c.Name, Pos: Vec3{0, 1.6, 0}, RotY: 0}
		c.Hub.players[c.ID] = &spawn
	}

	// Welcome snapshot
	players := make([]Player, 0, len(c.Hub.players))
	for _, p := range c.Hub.players {
		players = append(players, *p)
	}
	c.Hub.mu.Unlock()

	sendPacket(c, map[string]any{
		"type":       "welcome",
		"id":         c.ID,
		"players":    players,
		"maxPlayers": MaxPlayers,
	})

	if c.Role == RolePlayer {
		announce := map[string]any{"type": "player_join", "player": Player{ID: c.ID, Name: c.Name, Pos: Vec3{0, 1.6, 0}, RotY: 0}}
		if b, _ := msgpack.Marshal(announce); b != nil {
			select {
			case c.Hub.broadcast <- b:
			default:
			}
		}
	}

	// loop
	for {
		mt, msg, err := c.Conn.ReadMessage()
		if err != nil {
			return
		}
		if mt != websocket.BinaryMessage && mt != websocket.TextMessage {
			continue
		}
		var in inbound
		if msgpack.Unmarshal(msg, &in) != nil {
			continue
		}
		switch in.Type {
		case "state":
			if c.Role != RolePlayer || in.Pos == nil || in.RotY == nil {
				continue
			}
			c.Hub.mu.Lock()
			if p, ok := c.Hub.players[c.ID]; ok {
				p.Pos = *in.Pos
				p.RotY = *in.RotY
			}
			c.Hub.mu.Unlock()
		case "chat":
			now := time.Now()
			if now.Sub(c.LastChat) < MinChatInterval {
				continue
			}
			c.LastChat = now
			text := trimLen(strings.TrimSpace(in.Text), ChatMaxLen)
			if text == "" {
				continue
			}
			packet := map[string]any{
				"type": "chat", "id": c.ID, "name": c.Name, "text": text, "ts": now.Unix(),
			}
			if b, _ := msgpack.Marshal(packet); b != nil {
				select {
				case c.Hub.broadcast <- b:
				default:
				}
			}
		}
	}
}

func writer(c *Client) {
	ping := time.NewTicker(20 * time.Second)
	defer ping.Stop()
	for {
		select {
		case msg := <-c.Send:
			_ = c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if err := c.Conn.WriteMessage(websocket.BinaryMessage, msg); err != nil {
				return
			}
		case <-ping.C:
			_ = c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		case <-c.Closed:
			return
		}
	}
}

func sendPacket(c *Client, v any) {
	b, _ := msgpack.Marshal(v)
	_ = c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
	_ = c.Conn.WriteMessage(websocket.BinaryMessage, b)
}

// -------------------- Utis --------------------
func genID() string {
	var b [8]byte
	_, _ = cryptoRand.Read(b[:])
	return "p_" + hex.EncodeToString(b[:])
}

func sanitizeName(s string) string {
	s = strings.TrimSpace(s)
	s = strings.ReplaceAll(s, "\n", " ")
	s = strings.ReplaceAll(s, "\r", " ")
	if len(s) > NameMaxLen {
		s = s[:NameMaxLen]
	}
	return s
}

func trimLen(s string, max int) string {
	if len(s) > max {
		return s[:max]
	}
	return s
}

func randomName() string {
	adjs := []string{"Blue", "Crimson", "Silent", "Swift", "Lucky", "Rusty", "Cosmic", "Mossy"}
	nouns := []string{"Fox", "Raven", "Badger", "Otter", "Falcon", "Lynx", "Marmot", "Golem"}
	return adjs[rand.Intn(len(adjs))] + nouns[rand.Intn(len(nouns))]
}

// -------------------- main --------------------
func main() {
	rand.Seed(time.Now().UnixNano())
	addr := flag.String("addr", AddrHTTP, "http listen addr")
	flag.Parse()

	h := NewHub()
	go h.Run()

	mux := http.NewServeMux()
	mux.HandleFunc("/ws", wsHandler(h))

	srv := &http.Server{
		Addr:              *addr,
		Handler:           mux,
		ReadHeaderTimeout: 10 * time.Second,
		IdleTimeout:       75 * time.Second,
	}

	log.Println("SEPPJM-World WebSocket server listening on", *addr, "path /ws")
	if err := srv.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}
