// Package server holds the RegionIO core: configuration, shared state, and the
// data shown to clients (status, MOTD).
package server

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"strings"
	"sync"

	"regionio/internal/protocol"
	"regionio/internal/world"
)

// Config holds operator-tunable settings for a RegionIO instance.
type Config struct {
	Host       string
	Port       int
	MOTD       string
	MaxPlayers int
	// CompressionThreshold is the minimum uncompressed packet size (bytes) that
	// triggers zlib compression. Negative disables compression entirely.
	CompressionThreshold int
	// WorldSeed seeds terrain generation.
	WorldSeed int64
	// WorldDir is the on-disk world directory. When non-empty, chunks are
	// read from and written to <WorldDir>/region/*.mca and player edits
	// survive restarts. Empty disables persistence (in-memory only).
	WorldDir string
	// MaxCachedChunks bounds the in-memory chunk+frame cache (LRU). 0 means
	// unbounded (use only for tests/flat worlds). At ~200KiB/chunk, 1024 ≈ 200MB.
	MaxCachedChunks int
	// MaxViewDistance caps the client-requested chunk radius. Generation is much
	// more expensive than vanilla's pregenerated worlds, so the server owns the
	// upper bound instead of accepting the client's render distance verbatim.
	MaxViewDistance int
}

// DefaultConfig returns sensible defaults matching vanilla expectations.
func DefaultConfig() Config {
	return Config{
		Host:                 "0.0.0.0",
		Port:                 25565,
		MOTD:                 "RegionIO — a Minecraft server in Go",
		MaxPlayers:           20,
		CompressionThreshold: 256,
		// WorldSeed defaults to 0 for backward compatibility; operators override
		// it via the REGIONIO_SEED env var or the -seed flag.
		WorldSeed: 0,
		// WorldDir defaults to "world" so the world persists by default;
		// set to "" for a throwaway in-memory world.
		WorldDir: "world",
		// MaxCachedChunks keeps the live cache near 200MB at the default; the
		// streamer's pre-gen ring and player view distance comfortably fit.
		MaxCachedChunks: 1024,
		MaxViewDistance: 2,
	}
}

// Server is the top-level core shared across all connections.
type Server struct {
	cfg      Config
	chunks   *world.Cache
	store    *world.Store // nil when persistence is disabled
	entities *world.EntityManager

	playersMu    sync.RWMutex
	players      map[[16]byte]*PlayerSession
	playerNames  map[string][16]byte
	nextPlayerID int32

	clock worldClock
}

// PacketSender is the connection capability retained by the session registry.
// The network package supplies Conn.Send without introducing an import cycle.
type PacketSender func(id int32, body []byte) error

// PlayerSession is one active play-state client. Position is guarded separately
// so entity ticks can inspect players without holding the server registry lock.
type PlayerSession struct {
	EntityID int32
	Profile  Profile
	send     PacketSender
	mu       sync.RWMutex
	position [3]float64
	yaw      float32
	pitch    float32
	onGround bool
	viewDist int
}

// PlayerSnapshot is an immutable view of one play-state session.
type PlayerSnapshot struct {
	EntityID     int32
	Profile      Profile
	X, Y, Z      float64
	Yaw, Pitch   float32
	OnGround     bool
	ViewDistance int
}

var (
	ErrServerFull      = errors.New("server: player limit reached")
	ErrDuplicatePlayer = errors.New("server: player is already connected")
)

// New constructs a Server from cfg. When cfg.WorldDir is set, the world is
// backed by an on-disk store under that directory; otherwise it is in-memory
// only. A returned error (e.g. the world dir cannot be created) is fatal.
func New(cfg Config) (*Server, error) {
	if err := validateConfig(cfg); err != nil {
		return nil, err
	}
	gen, batchGen := world.NewVanillaRegionGenerators(cfg.WorldSeed)
	em := world.NewEntityManager()
	if cfg.WorldDir == "" {
		chunks := world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, nil, cfg.MaxCachedChunks)
		chunks.SetBatchGenerator(batchGen)
		return newServerState(cfg, chunks, nil, em), nil
	}
	store, err := world.NewStoreForSeed(cfg.WorldDir, cfg.WorldSeed)
	if err != nil {
		return nil, err
	}
	chunks := world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, store, cfg.MaxCachedChunks)
	chunks.SetBatchGenerator(batchGen)
	return newServerState(cfg, chunks, store, em), nil
}

// NewWithCache constructs a server around an existing cache. It is useful for
// embedding and integration tests that provide a specialized world generator.
func NewWithCache(cfg Config, chunks *world.Cache) (*Server, error) {
	if err := validateConfig(cfg); err != nil {
		return nil, err
	}
	if chunks == nil {
		return nil, errors.New("server: nil chunk cache")
	}
	return newServerState(cfg, chunks, nil, world.NewEntityManager()), nil
}

func validateConfig(cfg Config) error {
	if cfg.Port < 1 || cfg.Port > 65535 {
		return fmt.Errorf("server: port %d out of range", cfg.Port)
	}
	if cfg.MaxPlayers < 1 {
		return fmt.Errorf("server: max players must be positive")
	}
	if cfg.MaxCachedChunks < 0 {
		return fmt.Errorf("server: max cached chunks must not be negative")
	}
	if cfg.MaxViewDistance < 2 || cfg.MaxViewDistance > 16 {
		return fmt.Errorf("server: max view distance must be between 2 and 16")
	}
	if cfg.CompressionThreshold < -1 || cfg.CompressionThreshold > protocol.MaxPacketSize {
		return fmt.Errorf("server: compression threshold must be -1 or between 0 and %d", protocol.MaxPacketSize)
	}
	return nil
}

func newServerState(cfg Config, chunks *world.Cache, store *world.Store, entities *world.EntityManager) *Server {
	s := &Server{
		cfg: cfg, chunks: chunks, store: store, entities: entities,
		players: make(map[[16]byte]*PlayerSession), playerNames: make(map[string][16]byte),
	}
	// Resume the world clock where it was saved, so a restart does not throw
	// the sky back to dawn.
	if store != nil {
		gameTime, dayTime := store.WorldTime()
		s.SetWorldTime(gameTime, dayTime)
	}
	return s
}

// SaveWorldTime persists the current clock. It is a no-op without a store.
func (s *Server) SaveWorldTime() error {
	if s.store == nil {
		return nil
	}
	gameTime, dayTime := s.WorldTime()
	return s.store.SaveWorldTime(gameTime, dayTime)
}

// Config returns the active configuration.
func (s *Server) Config() Config { return s.cfg }

// Chunks returns the shared chunk cache.
func (s *Server) Chunks() *world.Cache { return s.chunks }

// Entities returns the shared entity manager.
func (s *Server) Entities() *world.EntityManager { return s.entities }

// RegisterPlayer adds a profile to the active play-state registry.
func (s *Server) RegisterPlayer(profile Profile, send PacketSender) (*PlayerSession, error) {
	s.playersMu.Lock()
	defer s.playersMu.Unlock()
	nameKey := strings.ToLower(profile.Name)
	if _, exists := s.players[profile.UUID]; exists {
		return nil, ErrDuplicatePlayer
	}
	if _, exists := s.playerNames[nameKey]; exists {
		return nil, ErrDuplicatePlayer
	}
	if s.cfg.MaxPlayers > 0 && len(s.players) >= s.cfg.MaxPlayers {
		return nil, ErrServerFull
	}
	s.nextPlayerID++
	session := &PlayerSession{
		EntityID: s.nextPlayerID,
		Profile:  profile,
		send:     send,
		onGround: true,
		viewDist: 4,
	}
	s.players[profile.UUID] = session
	s.playerNames[nameKey] = profile.UUID
	return session, nil
}

// UnregisterPlayer removes exactly the supplied session. Pointer identity keeps
// a delayed disconnect from removing a future session with the same profile.
func (s *Server) UnregisterPlayer(session *PlayerSession) {
	if session == nil {
		return
	}
	s.playersMu.Lock()
	defer s.playersMu.Unlock()
	if current := s.players[session.Profile.UUID]; current == session {
		delete(s.players, session.Profile.UUID)
		delete(s.playerNames, strings.ToLower(session.Profile.Name))
	}
}

// SetPlayerPosition updates a session's authoritative position snapshot.
func (s *Server) SetPlayerPosition(session *PlayerSession, x, y, z float64) {
	if session == nil {
		return
	}
	session.mu.Lock()
	session.position = [3]float64{x, y, z}
	session.mu.Unlock()
}

// SetPlayerTransform updates all movement fields received from the client.
func (s *Server) SetPlayerTransform(session *PlayerSession, x, y, z float64, yaw, pitch float32, onGround bool) {
	if session == nil {
		return
	}
	session.mu.Lock()
	session.position = [3]float64{x, y, z}
	session.yaw = yaw
	session.pitch = pitch
	session.onGround = onGround
	session.mu.Unlock()
}

// SetPlayerViewDistance records the clamped chunk radius used for visibility.
func (s *Server) SetPlayerViewDistance(session *PlayerSession, distance int) {
	if session == nil {
		return
	}
	if distance < 2 {
		distance = 4
	}
	if distance > 16 {
		distance = 16
	}
	session.mu.Lock()
	session.viewDist = distance
	session.mu.Unlock()
}

// Snapshot returns a consistent copy of this session's gameplay state.
func (session *PlayerSession) Snapshot() PlayerSnapshot {
	if session == nil {
		return PlayerSnapshot{}
	}
	session.mu.RLock()
	snapshot := PlayerSnapshot{
		EntityID:     session.EntityID,
		Profile:      session.Profile,
		X:            session.position[0],
		Y:            session.position[1],
		Z:            session.position[2],
		Yaw:          session.yaw,
		Pitch:        session.pitch,
		OnGround:     session.onGround,
		ViewDistance: session.viewDist,
	}
	session.mu.RUnlock()
	return snapshot
}

// PlayerSnapshots returns consistent copies of all active play sessions.
func (s *Server) PlayerSnapshots() []PlayerSnapshot {
	s.playersMu.RLock()
	players := make([]*PlayerSession, 0, len(s.players))
	for _, player := range s.players {
		players = append(players, player)
	}
	s.playersMu.RUnlock()

	snapshots := make([]PlayerSnapshot, 0, len(players))
	for _, player := range players {
		snapshots = append(snapshots, player.Snapshot())
	}
	return snapshots
}

// PlayerCount returns the number of tracked players.
func (s *Server) PlayerCount() int {
	s.playersMu.RLock()
	defer s.playersMu.RUnlock()
	return len(s.players)
}

// Broadcast sends one already-encoded packet body to every active player. A
// failed recipient cannot make another player's gameplay handler fail.
func (s *Server) Broadcast(id int32, body []byte) {
	s.playersMu.RLock()
	senders := make([]PacketSender, 0, len(s.players))
	for _, player := range s.players {
		senders = append(senders, player.send)
	}
	s.playersMu.RUnlock()
	for _, send := range senders {
		if send != nil {
			_ = send(id, body)
		}
	}
}

// BroadcastChunk sends a packet only to players whose chunk view contains the
// target chunk. It is used for block and light changes.
func (s *Server) BroadcastChunk(cx, cz int32, id int32, body []byte) {
	s.playersMu.RLock()
	players := make([]*PlayerSession, 0, len(s.players))
	for _, player := range s.players {
		players = append(players, player)
	}
	s.playersMu.RUnlock()

	for _, player := range players {
		snapshot := player.Snapshot()
		pcx := int32(int64(math.Floor(snapshot.X)) >> 4)
		pcz := int32(int64(math.Floor(snapshot.Z)) >> 4)
		if chunkDistance(pcx, pcz, cx, cz) <= int32(snapshot.ViewDistance) && player.send != nil {
			_ = player.send(id, body)
		}
	}
}

func chunkDistance(ax, az, bx, bz int32) int32 {
	dx := ax - bx
	if dx < 0 {
		dx = -dx
	}
	dz := az - bz
	if dz < 0 {
		dz = -dz
	}
	if dz > dx {
		return dz
	}
	return dx
}

// NearestPlayer returns the position of the nearest player to (x, y, z).
// Returns false if no players are online.
func (s *Server) NearestPlayer(x, y, z float64) (pos [3]float64, ok bool) {
	minDist := float64(-1)
	s.playersMu.RLock()
	players := make([]*PlayerSession, 0, len(s.players))
	for _, player := range s.players {
		players = append(players, player)
	}
	s.playersMu.RUnlock()
	for _, player := range players {
		player.mu.RLock()
		p := player.position
		player.mu.RUnlock()
		dist := (p[0]-x)*(p[0]-x) + (p[1]-y)*(p[1]-y) + (p[2]-z)*(p[2]-z)
		if minDist < 0 || dist < minDist {
			minDist = dist
			pos = p
			ok = true
		}
	}
	return
}

// Store returns the on-disk world store, or nil if persistence is disabled.
func (s *Server) Store() *world.Store { return s.store }

// statusResponse mirrors the JSON shape the client expects for the server-list
// ping. Field names and nesting are part of the protocol contract.
type statusResponse struct {
	Version     statusVersion `json:"version"`
	Players     statusPlayers `json:"players"`
	Description statusText    `json:"description"`
	// EnforcesSecureChat is read by the client; false keeps unsigned chat working.
	EnforcesSecureChat bool `json:"enforcesSecureChat"`
}

type statusVersion struct {
	Name     string `json:"name"`
	Protocol int    `json:"protocol"`
}

type statusPlayers struct {
	Max    int `json:"max"`
	Online int `json:"online"`
}

type statusText struct {
	Text string `json:"text"`
}

// StatusJSON returns the marshaled status response for the server-list ping.
func (s *Server) StatusJSON(onlinePlayers int) ([]byte, error) {
	resp := statusResponse{
		Version: statusVersion{
			Name:     protocol.GameVersion,
			Protocol: protocol.ProtocolVersion,
		},
		Players: statusPlayers{
			Max:    s.cfg.MaxPlayers,
			Online: onlinePlayers,
		},
		Description:        statusText{Text: s.cfg.MOTD},
		EnforcesSecureChat: false,
	}
	return json.Marshal(resp)
}
