package session

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"sync"
	"sync/atomic"
	"time"

	"github.com/df-mc/dragonfly/server/block/cube"
	"github.com/df-mc/dragonfly/server/cmd"
	"github.com/df-mc/dragonfly/server/item/inventory"
	"github.com/df-mc/dragonfly/server/item/recipe"
	"github.com/df-mc/dragonfly/server/player/chat"
	"github.com/df-mc/dragonfly/server/player/debug"
	"github.com/df-mc/dragonfly/server/player/form"
	"github.com/df-mc/dragonfly/server/player/hud"
	"github.com/df-mc/dragonfly/server/player/skin"
	"github.com/df-mc/dragonfly/server/world"
	"github.com/go-gl/mathgl/mgl64"
	"github.com/google/uuid"
	"github.com/sandertv/gophertunnel/minecraft"
	"github.com/sandertv/gophertunnel/minecraft/nbt"
	"github.com/sandertv/gophertunnel/minecraft/protocol"
	"github.com/sandertv/gophertunnel/minecraft/protocol/login"
	"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)

// Session handles incoming packets from connections and sends outgoing packets by providing a thin layer
// of abstraction over direct packets. A Session basically 'controls' an entity.
type Session struct {
	conf           Config
	once, connOnce sync.Once

	ent      *world.EntityHandle
	conn     Conn
	handlers map[uint32]packetHandler
	packets  chan packet.Packet

	currentScoreboard atomic.Pointer[string]
	currentLines      atomic.Pointer[[]string]

	chunkLoader                 *world.Loader
	chunkRadius, maxChunkRadius int32

	emoteChatMuted bool

	teleportPos atomic.Pointer[mgl64.Vec3]

	entityMutex sync.RWMutex
	// currentEntityRuntimeID holds the runtime ID assigned to the last entity. It is incremented for every
	// entity spawned to the session.
	currentEntityRuntimeID uint64
	// entityRuntimeIDs holds the runtime IDs of entities shown to the session.
	entityRuntimeIDs map[*world.EntityHandle]uint64
	entities         map[uint64]*world.EntityHandle
	hiddenEntities   map[uuid.UUID]struct{}

	// heldSlot is the slot in the inventory that the controllable is holding.
	heldSlot                     *uint32
	inv, offHand, enderChest, ui *inventory.Inventory
	armour                       *inventory.Armour

	// joinSkin is the first skin that the player joined with. It is sent on
	// spawn for the player list, but otherwise updated immediately when the
	// player is viewed.
	joinSkin skin.Skin

	breakingPos cube.Pos

	inTransaction, containerOpened atomic.Bool
	openedWindowID                 atomic.Uint32
	openedContainerID              atomic.Uint32
	openedWindow                   atomic.Pointer[inventory.Inventory]
	openedPos                      atomic.Pointer[cube.Pos]
	swingingArm                    atomic.Bool
	changingSlot                   atomic.Bool
	changingDimension              atomic.Bool
	moving                         bool

	lastChunkPos world.ChunkPos

	recipes map[uint32]recipe.Recipe

	blobMu                sync.Mutex
	blobs                 map[uint64][]byte
	openChunkTransactions []map[uint64]struct{}
	invOpened             bool

	hudMu      sync.RWMutex
	hudUpdates map[hud.Element]bool
	hiddenHud  map[hud.Element]struct{}

	debugShapesMu     sync.RWMutex
	debugShapes       map[int]debug.Shape
	debugShapeUpdates []debugShapeUpdate

	viewLayer *world.ViewLayer

	closeBackground chan struct{}

	br world.BlockRegistry
}

// debugShapeUpdate represents a pending debug shape mutation. If shape is nil, the update removes the
// debug shape with the matching ID. Updates are applied in order when the session sends debug shapes.
type debugShapeUpdate struct {
	id    int
	shape debug.Shape
}

// Conn represents a connection that packets are read from and written to by a Session. In addition, it holds some
// information on the identity of the Session.
type Conn interface {
	io.Closer
	// IdentityData returns the login.IdentityData of a Conn. It contains the UUID, XUID and username of the connection.
	IdentityData() login.IdentityData
	// ClientData returns the login.ClientData of a Conn. This includes less sensitive data of the player like its skin,
	// language code and other non-essential information.
	ClientData() login.ClientData
	// ClientCacheEnabled specifies if the Conn has the client cache, used for caching chunks client-side, enabled or
	// not. Some platforms, like the Nintendo Switch, have this disabled at all times.
	ClientCacheEnabled() bool
	// ChunkRadius returns the chunk radius as requested by the client at the other end of the Conn.
	ChunkRadius() int
	// Latency returns the current latency measured over the Conn.
	Latency() time.Duration
	// Flush flushes the packets buffered by the Conn, sending all of them out immediately.
	Flush() error
	// RemoteAddr returns the remote network address.
	RemoteAddr() net.Addr
	// ReadPacket reads a packet.Packet from the Conn. An error is returned if a deadline was set that was
	// exceeded or if the Conn was closed while awaiting a packet.
	ReadPacket() (pk packet.Packet, err error)
	// WritePacket writes a packet.Packet to the Conn. An error is returned if the Conn was closed before sending the
	// packet.
	WritePacket(pk packet.Packet) error
	// StartGameContext starts the game for the Conn with a context to cancel it.
	StartGameContext(ctx context.Context, data minecraft.GameData) error
}

// Nop represents a no-operation session. It does not do anything when sending a packet to it.
var Nop = &Session{conf: Config{Log: slog.New(slog.DiscardHandler)}}

// selfEntityRuntimeID is the entity runtime (or unique) ID of the controllable that the session holds.
const selfEntityRuntimeID = 1

// errSelfRuntimeID is an error returned during packet handling for fields that refer to the player itself and
// must therefore always be 1.
var errSelfRuntimeID = errors.New("invalid entity runtime ID: runtime ID for self must always be 1")

type Config struct {
	Log *slog.Logger

	MaxChunkRadius int

	EmoteChatMuted bool

	JoinMessage, QuitMessage chat.Translation

	HandleStop func(*world.Tx, Controllable)
	// BlockRegistry overrides the registry used for network serialization. If nil, world.DefaultBlockRegistry is used.
	BlockRegistry world.BlockRegistry
}

func (conf Config) New(conn Conn) *Session {
	r := conn.ChunkRadius()
	if r > conf.MaxChunkRadius {
		r = conf.MaxChunkRadius
		_ = conn.WritePacket(&packet.ChunkRadiusUpdated{ChunkRadius: int32(r)})
	}
	if conf.Log == nil {
		conf.Log = slog.Default()
	}
	conf.Log = conf.Log.With("name", conn.IdentityData().DisplayName, "uuid", conn.IdentityData().Identity, "raddr", conn.RemoteAddr().String())

	s := &Session{}
	*s = Session{
		openChunkTransactions:  make([]map[uint64]struct{}, 0, 8),
		closeBackground:        make(chan struct{}),
		handlers:               map[uint32]packetHandler{},
		packets:                make(chan packet.Packet, 256),
		entityRuntimeIDs:       map[*world.EntityHandle]uint64{},
		entities:               map[uint64]*world.EntityHandle{},
		hiddenEntities:         map[uuid.UUID]struct{}{},
		blobs:                  map[uint64][]byte{},
		chunkRadius:            int32(r),
		maxChunkRadius:         int32(conf.MaxChunkRadius),
		emoteChatMuted:         conf.EmoteChatMuted,
		conn:                   conn,
		currentEntityRuntimeID: 1,
		heldSlot:               new(uint32),
		recipes:                make(map[uint32]recipe.Recipe),
		conf:                   conf,
		hudUpdates:             make(map[hud.Element]bool),
		hiddenHud:              make(map[hud.Element]struct{}),
		debugShapes:            make(map[int]debug.Shape),
		debugShapeUpdates:      make([]debugShapeUpdate, 0, 256),
	}
	s.viewLayer = world.NewViewLayer(s)
	s.openedWindow.Store(inventory.New(1, nil))
	s.openedPos.Store(&cube.Pos{})

	var scoreboardName string
	var scoreboardLines []string
	s.currentScoreboard.Store(&scoreboardName)
	s.currentLines.Store(&scoreboardLines)

	if conf.BlockRegistry == nil {
		s.br = world.DefaultBlockRegistry
	} else {
		s.br = conf.BlockRegistry
	}

	s.registerHandlers()
	s.sendBiomes()
	groups, items := creativeContent(s.br)
	s.writePacket(&packet.CreativeContent{Groups: groups, Items: items})
	s.sendRecipes()
	s.sendArmourTrimData()
	s.SendSpeed(0.1)
	go func() {
		for {
			select {
			case <-s.closeBackground:
				return
			case pk := <-s.packets:
				_ = conn.WritePacket(pk)
			}
		}
	}()
	return s
}

// SetHandle sets the world.EntityHandle of the Session and attaches a skin to
// other players on join.
func (s *Session) SetHandle(handle *world.EntityHandle, skin skin.Skin) {
	s.ent = handle
	s.entityRuntimeIDs[handle] = selfEntityRuntimeID
	s.entities[selfEntityRuntimeID] = handle

	s.joinSkin = skin
	sessions.Add(s)
}

// Spawn makes the Controllable passed spawn in the world.World.
// The function passed will be called when the session stops running.
func (s *Session) Spawn(c Controllable, tx *world.Tx) {
	s.SendHealth(c.Health(), c.MaxHealth(), c.Absorption())
	s.SendExperience(c.ExperienceLevel(), c.ExperienceProgress())
	s.SendFood(c.Food(), 0, 0)

	pos := c.Position()
	s.chunkLoader = world.NewLoader(int(s.chunkRadius), tx.World(), s)
	s.chunkLoader.Move(tx, pos)
	s.writePacket(&packet.NetworkChunkPublisherUpdate{
		Position: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])},
		Radius:   uint32(s.chunkRadius) << 4,
	})

	s.sendAvailableEntities(tx.World())

	c.SetGameMode(c.GameMode())
	for _, e := range c.Effects() {
		s.SendEffect(e)
	}
	s.ViewEntityState(c)

	s.sendInv(s.inv, protocol.WindowIDInventory)
	s.sendInv(s.ui, protocol.WindowIDUI)
	s.sendInv(s.offHand, protocol.WindowIDOffHand)
	s.sendInv(s.armour.Inventory(), protocol.WindowIDArmour)

	chat.Global.Subscribe(c)
	if !s.conf.JoinMessage.Zero() {
		chat.Global.Writet(s.conf.JoinMessage, s.conn.IdentityData().DisplayName)
	}

	go s.background()
	go s.handlePackets()
}

// Close closes the session, which in turn closes the controllable and the connection that the session
// manages. Close ensures the method only runs code on the first call.
func (s *Session) Close(tx *world.Tx, c Controllable) {
	s.once.Do(func() {
		s.close(tx, c)
	})
}

// close closes the session, which in turn closes the controllable and the connection that the session
// manages.
func (s *Session) close(tx *world.Tx, c Controllable) {
	c.MoveItemsToInventory()
	s.closeCurrentContainer(tx, false)
	if s.viewLayer != nil {
		_ = s.viewLayer.Close()
	}

	s.conf.HandleStop(tx, c)

	// Clear the inventories so that they no longer hold references to the connection.
	_ = s.inv.Close()
	_ = s.offHand.Close()
	_ = s.armour.Close()

	s.chunkLoader.Close(tx)

	if !s.conf.QuitMessage.Zero() {
		chat.Global.Writet(s.conf.QuitMessage, s.conn.IdentityData().DisplayName)
	}
	chat.Global.Unsubscribe(c)

	// Note: Be aware of where RemoveEntity is called. This must not be done too
	// early.
	tx.RemoveEntity(c)
	_ = s.ent.Close()

	// This should always be called last due to the timing of the removal of
	// entity runtime IDs.
	sessions.Remove(s, c)
	s.entityMutex.Lock()
	clear(s.entityRuntimeIDs)
	clear(s.entities)
	s.entityMutex.Unlock()
}

// CloseConnection closes the underlying connection of the session so that the session ends up being closed
// eventually.
func (s *Session) CloseConnection() {
	s.connOnce.Do(func() {
		_ = s.conn.Close()
		close(s.closeBackground)
	})
}

// Addr returns the net.Addr of the client.
func (s *Session) Addr() net.Addr {
	return s.conn.RemoteAddr()
}

// Latency returns the latency of the connection.
func (s *Session) Latency() time.Duration {
	return s.conn.Latency()
}

// ClientData returns the login.ClientData of the underlying *minecraft.Conn.
func (s *Session) ClientData() login.ClientData {
	return s.conn.ClientData()
}

// handlePackets continuously handles incoming packets from the connection. It processes them accordingly.
// Once the connection is closed, handlePackets will return.
func (s *Session) handlePackets() {
	defer func() {
		// First close the Controllable. This might lead to a world change
		// (player might be dead while disconnecting, in which case it will
		// respawn first).
		s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
			_ = e.(Controllable).Close()
		})
		// Because the player might no longer be in the same world after
		// closing, we create a new transaction
		s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
			s.Close(tx, e.(Controllable))
		})
	}()
	for {
		pk, err := s.conn.ReadPacket()
		if err != nil {
			return
		}
		s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
			err = s.handlePacket(pk, tx, e.(Controllable))
		})
		if err != nil {
			s.conf.Log.Debug("process packet: " + err.Error())
			return
		}
	}
}

// background performs background tasks of the Session. This includes chunk sending and automatic command updating.
// background returns when the Session's connection is closed using CloseConnection.
func (s *Session) background() {
	var (
		r          map[string]map[int]cmd.Runnable
		enums      map[string]cmd.Enum
		enumValues map[string][]string
		softEnums  = make(map[string]struct{})
		ok         bool
		i          int
	)

	s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
		co := e.(Controllable)
		r = s.sendAvailableCommands(co, softEnums)
		enums, enumValues = s.enums(co)
	})

	t := time.NewTicker(time.Second / 20)
	defer t.Stop()
	for {
		select {
		case <-t.C:
			s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
				c := e.(Controllable)

				if i++; i%20 == 0 {
					// Enum resending happens relatively often and frequent updates are more important than with full
					// command changes. Those are generally only related to permission changes, which doesn't happen often.
					r = s.resendEnums(enums, enumValues, softEnums, r, c)
				}
				if i%100 == 0 {
					// Try to resend commands only every 5 seconds.
					if r, ok = s.resendCommands(r, c, softEnums); ok {
						enums, enumValues = s.enums(c)
					}
				}
				s.sendChunks(tx, c)
			})
		case <-s.closeBackground:
			return
		}
	}
}

// sendChunks sends the next up to 4 chunks to the connection. What chunks are loaded depends on the connection of
// the chunk loader and the chunks that were previously loaded.
func (s *Session) sendChunks(tx *world.Tx, c Controllable) {
	var worldSwitched bool
	if w := tx.World(); s.chunkLoader.World() != w && w != nil {
		worldSwitched = true
		s.handleWorldSwitch(w, tx, c)
	}
	pos := c.Position()
	s.chunkLoader.Move(tx, pos)
	chunkPos := world.ChunkPos{int32(pos[0]) << 4, int32(pos[2]) << 4}
	if s.lastChunkPos != chunkPos || worldSwitched {
		s.lastChunkPos = chunkPos
		s.writePacket(&packet.NetworkChunkPublisherUpdate{
			Position: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])},
			Radius:   uint32(s.chunkRadius) << 4,
		})
	}

	s.blobMu.Lock()
	const maxChunkTransactions = 8
	toLoad := maxChunkTransactions - len(s.openChunkTransactions)
	s.blobMu.Unlock()
	if toLoad > 4 {
		toLoad = 4
	}
	s.chunkLoader.Load(tx, toLoad)
}

// handleWorldSwitch handles the player of the Session switching worlds.
func (s *Session) handleWorldSwitch(w *world.World, tx *world.Tx, c Controllable) {
	if s.conn.ClientCacheEnabled() {
		s.blobMu.Lock()
		s.blobs = map[uint64][]byte{}
		s.openChunkTransactions = nil
		s.blobMu.Unlock()
	}

	dim, _ := world.DimensionID(w.Dimension())
	same := w.Dimension() == s.chunkLoader.World().Dimension()
	if !same {
		s.changeDimension(int32(dim), false, c)
	}
	s.ViewEntityTeleport(c, c.Position())
	s.chunkLoader.ChangeWorld(tx, w)
}

// changeDimension changes the dimension of the client. If silent is set to true, the portal noise will be stopped
// immediately.
func (s *Session) changeDimension(dim int32, silent bool, c Controllable) {
	s.changingDimension.Store(true)
	h := s.handlers[packet.IDServerBoundLoadingScreen].(*ServerBoundLoadingScreenHandler)
	id := h.currentID.Add(1)
	h.expectedID.Store(id)

	s.writePacket(&packet.ChangeDimension{
		Dimension:       dim,
		Position:        vec64To32(c.Position().Add(entityOffset(c))),
		LoadingScreenID: protocol.Option(id),
	})
	s.writePacket(&packet.StopSound{StopAll: silent})
	s.writePacket(&packet.PlayStatus{Status: packet.PlayStatusPlayerSpawn})

	// As of v1.19.50, the dimension ack that is meant to be sent by the client is now sent by the server. The client
	// still sends the ack, but after the server has sent it. Thanks to Mojang for another groundbreaking change.
	s.writePacket(&packet.PlayerAction{
		EntityRuntimeID: selfEntityRuntimeID,
		ActionType:      protocol.PlayerActionDimensionChangeDone,
	})
}

// ChangingDimension returns whether the session is currently changing dimension or not.
func (s *Session) ChangingDimension() bool {
	return s.changingDimension.Load()
}

// ChunkRadius returns the chunk radius of the session.
func (s *Session) ChunkRadius() int32 {
	return s.chunkRadius
}

// handlePacket handles an incoming packet, processing it accordingly. If the packet had invalid data or was
// otherwise not valid in its context, an error is returned.
func (s *Session) handlePacket(pk packet.Packet, tx *world.Tx, c Controllable) (err error) {
	handler, ok := s.handlers[pk.ID()]
	if !ok {
		s.conf.Log.Debug("unhandled packet", "packet", fmt.Sprintf("%T", pk), "data", fmt.Sprintf("%+v", pk)[1:])
		return nil
	}
	if handler == nil {
		// A nil handler means it was explicitly unhandled.
		return nil
	}
	if err := handler.Handle(pk, s, tx, c); err != nil {
		return fmt.Errorf("%T: %w", pk, err)
	}
	return nil
}

// registerHandlers registers all packet handlers found in the packetHandler package.
func (s *Session) registerHandlers() {
	s.handlers = map[uint32]packetHandler{
		packet.IDActorEvent:                nil,
		packet.IDAdventureSettings:         nil, // Deprecated, the client still sends this though.
		packet.IDAnimate:                   nil,
		packet.IDAnvilDamage:               nil,
		packet.IDBlockActorData:            &BlockActorDataHandler{},
		packet.IDBlockPickRequest:          &BlockPickRequestHandler{},
		packet.IDBookEdit:                  &BookEditHandler{},
		packet.IDBossEvent:                 nil,
		packet.IDClientCacheBlobStatus:     &ClientCacheBlobStatusHandler{},
		packet.IDCommandRequest:            &CommandRequestHandler{},
		packet.IDContainerClose:            &ContainerCloseHandler{},
		packet.IDEmote:                     &EmoteHandler{},
		packet.IDEmoteList:                 nil,
		packet.IDFilterText:                nil,
		packet.IDInteract:                  &InteractHandler{},
		packet.IDInventoryTransaction:      &InventoryTransactionHandler{},
		packet.IDItemStackRequest:          &ItemStackRequestHandler{changes: map[byte]map[byte]changeInfo{}, responseChanges: map[int32]map[*inventory.Inventory]map[byte]responseChange{}},
		packet.IDLecternUpdate:             &LecternUpdateHandler{},
		packet.IDMobEquipment:              &MobEquipmentHandler{},
		packet.IDModalFormResponse:         &ModalFormResponseHandler{forms: make(map[uint32]form.Form)},
		packet.IDMovePlayer:                nil,
		packet.IDNPCRequest:                &NPCRequestHandler{},
		packet.IDPlayerAction:              &PlayerActionHandler{},
		packet.IDPlayerAuthInput:           &PlayerAuthInputHandler{},
		packet.IDPlayerSkin:                &PlayerSkinHandler{},
		packet.IDRequestAbility:            &RequestAbilityHandler{},
		packet.IDRequestChunkRadius:        &RequestChunkRadiusHandler{},
		packet.IDRespawn:                   &RespawnHandler{},
		packet.IDSetPlayerInventoryOptions: nil,
		packet.IDSubChunkRequest:           &SubChunkRequestHandler{},
		packet.IDText:                      &TextHandler{},
		packet.IDServerBoundLoadingScreen:  &ServerBoundLoadingScreenHandler{},
		packet.IDServerBoundDiagnostics:    &ServerBoundDiagnosticsHandler{},
	}
}

// writePacket writes a packet to the session's connection if it is not Nop.
func (s *Session) writePacket(pk packet.Packet) {
	if s == Nop {
		return
	}
	select {
	case s.packets <- pk:
	case <-s.closeBackground:
	}
}

// actorIdentifier represents the structure of an actor identifier sent over the network.
type actorIdentifier struct {
	// ID is a unique namespaced identifier for the entity.
	ID string `nbt:"id"`
}

// sendAvailableEntities sends all registered entities to the player.
func (s *Session) sendAvailableEntities(w *world.World) {
	var identifiers []actorIdentifier
	for _, t := range w.EntityRegistry().Types() {
		identifiers = append(identifiers, actorIdentifier{ID: t.EncodeEntity()})
	}
	serialisedEntityData, err := nbt.Marshal(map[string]any{"idlist": identifiers})
	if err != nil {
		panic("should never happen")
	}
	s.writePacket(&packet.AvailableActorIdentifiers{SerialisedEntityIdentifiers: serialisedEntityData})
}
