package game

import (
	"sync/atomic"
	"time"

	"a301_game_server/config"
	"a301_game_server/internal/ai"
	"a301_game_server/internal/combat"
	"a301_game_server/internal/entity"
	"a301_game_server/internal/network"
	"a301_game_server/internal/world"
	"a301_game_server/pkg/logger"
	"a301_game_server/pkg/mathutil"
	pb "a301_game_server/proto/gen/pb"
	"google.golang.org/protobuf/proto"
)

const (
	maxMoveSpeed float32 = 10.0 // units per second
)

// PlayerMessage is a message from a player connection queued for zone processing.
type PlayerMessage struct {
	PlayerID uint64
	Packet   *network.Packet
}

// PlayerEntity wraps an entity.Entity that is also a connected player.
type PlayerEntity interface {
	entity.Entity
	Connection() *network.Connection
	Velocity() mathutil.Vec3
	SetVelocity(vel mathutil.Vec3)
}

// ZoneMessageHandler provides an extension point for handling custom message types in a zone.
type ZoneMessageHandler interface {
	HandleZoneMessage(zone *Zone, msg PlayerMessage) bool // returns true if handled
}

// Zone is a self-contained game area with its own game loop.
type Zone struct {
	id       uint32
	cfg      *config.Config
	entities map[uint64]entity.Entity
	players  map[uint64]PlayerEntity
	aoi      world.AOIManager
	incoming chan PlayerMessage
	stopCh   chan struct{}
	tick     int64

	// Metrics
	lastTickDuration atomic.Int64

	// AOI toggle support
	aoiEnabled   bool
	gridAOI      *world.GridAOI
	broadcastAOI *world.BroadcastAllAOI

	// Combat
	combatMgr *combat.Manager

	// AI / Mobs
	spawner      *ai.Spawner
	nextEntityID atomic.Uint64

	// External message handler for custom/internal messages.
	extHandler ZoneMessageHandler

	// Respawn position
	spawnPos mathutil.Vec3

	// Zone portals
	portals []world.ZonePortal

	// Zone transfer callback (set by GameServer)
	onZoneTransfer func(playerID uint64, targetZoneID uint32, targetPos mathutil.Vec3)
}

// NewZone creates a new zone with the given configuration.
func NewZone(id uint32, cfg *config.Config) *Zone {
	gridAOI := world.NewGridAOI(cfg.World.AOI.CellSize, cfg.World.AOI.ViewRange)
	broadcastAOI := world.NewBroadcastAllAOI()

	var activeAOI world.AOIManager
	if cfg.World.AOI.Enabled {
		activeAOI = gridAOI
	} else {
		activeAOI = broadcastAOI
	}

	cm := combat.NewManager()

	z := &Zone{
		id:           id,
		cfg:          cfg,
		entities:     make(map[uint64]entity.Entity),
		players:      make(map[uint64]PlayerEntity),
		aoi:          activeAOI,
		incoming:     make(chan PlayerMessage, 4096),
		stopCh:       make(chan struct{}),
		aoiEnabled:   cfg.World.AOI.Enabled,
		gridAOI:      gridAOI,
		broadcastAOI: broadcastAOI,
		combatMgr:    cm,
		spawnPos:     mathutil.NewVec3(0, 0, 0),
	}

	// Wire combat manager broadcast to zone AOI.
	cm.SetBroadcast(z.broadcastCombatEvent, z.sendToEntity)

	// Create mob spawner.
	z.spawner = ai.NewSpawner(
		&z.nextEntityID,
		func(m *ai.Mob) { z.addMob(m) },
		func(mobID uint64) { z.removeMob(mobID) },
	)

	return z
}

// ID returns the zone identifier.
func (z *Zone) ID() uint32 { return z.id }

// AddPortal registers a zone portal.
func (z *Zone) AddPortal(portal world.ZonePortal) {
	z.portals = append(z.portals, portal)
}

// SetZoneTransferCallback sets the function called when a player enters a portal.
func (z *Zone) SetZoneTransferCallback(fn func(playerID uint64, targetZoneID uint32, targetPos mathutil.Vec3)) {
	z.onZoneTransfer = fn
}

// PlayerCount returns the current number of players in this zone.
func (z *Zone) PlayerCount() int { return len(z.players) }

// EntityCount returns the current number of entities in this zone.
func (z *Zone) EntityCount() int { return len(z.entities) }

// LastTickDuration returns the duration of the last tick in microseconds.
func (z *Zone) LastTickDuration() int64 { return z.lastTickDuration.Load() }

// AOIEnabled returns whether grid-based AOI is currently active.
func (z *Zone) AOIEnabled() bool { return z.aoiEnabled }

// EnqueueMessage queues a player message for processing in the next tick.
func (z *Zone) EnqueueMessage(msg PlayerMessage) {
	select {
	case z.incoming <- msg:
	default:
		logger.Warn("zone message queue full, dropping", "zoneID", z.id, "playerID", msg.PlayerID)
	}
}

// AddPlayer adds a player to the zone.
// Must be called from the zone's goroutine or before Run() starts.
func (z *Zone) AddPlayer(p PlayerEntity) {
	z.entities[p.EntityID()] = p
	z.players[p.EntityID()] = p
	z.aoi.Add(p)

	// Notify existing nearby players about the new player.
	spawnData, _ := network.Encode(network.MsgSpawnEntity, &pb.SpawnEntity{Entity: p.ToProto()})
	for _, nearby := range z.aoi.GetNearby(p) {
		if np, ok := z.players[nearby.EntityID()]; ok {
			np.Connection().SendRaw(spawnData)
		}
	}

	logger.Info("player added to zone", "zoneID", z.id, "playerID", p.EntityID(), "players", len(z.players))
}

// RemovePlayer removes a player from the zone.
func (z *Zone) RemovePlayer(playerID uint64) {
	entity, ok := z.entities[playerID]
	if !ok {
		return
	}

	events := z.aoi.Remove(entity)
	z.handleAOIEvents(events)

	z.combatMgr.RemoveEntity(playerID)
	delete(z.entities, playerID)
	delete(z.players, playerID)

	logger.Info("player removed from zone", "zoneID", z.id, "playerID", playerID, "players", len(z.players))
}

// ToggleAOI switches between grid-based and broadcast-all AOI at runtime.
func (z *Zone) ToggleAOI(enabled bool) {
	if z.aoiEnabled == enabled {
		return
	}

	z.aoiEnabled = enabled

	// Rebuild the target AOI manager with current entities.
	var newAOI world.AOIManager
	if enabled {
		g := world.NewGridAOI(z.cfg.World.AOI.CellSize, z.cfg.World.AOI.ViewRange)
		for _, e := range z.entities {
			g.Add(e)
		}
		z.gridAOI = g
		newAOI = g
	} else {
		b := world.NewBroadcastAllAOI()
		for _, e := range z.entities {
			b.Add(e)
		}
		z.broadcastAOI = b
		newAOI = b
	}

	z.aoi = newAOI

	// After toggle, send full spawn list to all players so they see the correct set.
	for _, p := range z.players {
		z.sendNearbySnapshot(p)
	}

	logger.Info("AOI toggled", "zoneID", z.id, "enabled", enabled)
}

// Run starts the zone's game loop. Blocks until Stop() is called.
func (z *Zone) Run() {
	interval := z.cfg.TickInterval()
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	logger.Info("zone started", "zoneID", z.id, "tickInterval", interval)

	for {
		select {
		case <-ticker.C:
			start := time.Now()
			z.processTick()
			z.lastTickDuration.Store(time.Since(start).Microseconds())
		case <-z.stopCh:
			logger.Info("zone stopped", "zoneID", z.id)
			return
		}
	}
}

// Stop signals the zone's game loop to exit.
func (z *Zone) Stop() {
	close(z.stopCh)
}

func (z *Zone) processTick() {
	z.tick++
	z.processInputQueue()
	z.updateMovement()
	z.updateAI()
	z.updateCombat()
	z.checkDeaths()
	z.spawner.Update(time.Now())
	z.broadcastState()
}

func (z *Zone) updateCombat() {
	dt := z.cfg.TickInterval()
	z.combatMgr.UpdateBuffs(dt, func(id uint64) combat.Combatant {
		if p, ok := z.players[id]; ok {
			if c, ok := p.(combat.Combatant); ok {
				return c
			}
		}
		return nil
	})
}

func (z *Zone) processInputQueue() {
	for {
		select {
		case msg := <-z.incoming:
			z.handleMessage(msg)
		default:
			return
		}
	}
}

// SetMessageHandler sets an external handler for custom message types.
func (z *Zone) SetMessageHandler(h ZoneMessageHandler) {
	z.extHandler = h
}

func (z *Zone) handleMessage(msg PlayerMessage) {
	// Try external handler first (for internal messages like disconnect/enter).
	if z.extHandler != nil && z.extHandler.HandleZoneMessage(z, msg) {
		return
	}

	switch msg.Packet.Type {
	case network.MsgMoveRequest:
		z.handleMoveRequest(msg)
	case network.MsgUseSkillRequest:
		z.handleUseSkill(msg)
	case network.MsgRespawnRequest:
		z.handleRespawn(msg)
	case network.MsgPing:
		z.handlePing(msg)
	case network.MsgAOIToggleRequest:
		z.handleAOIToggle(msg)
	case network.MsgMetricsRequest:
		z.handleMetrics(msg)
	}
}

func (z *Zone) handleMoveRequest(msg PlayerMessage) {
	p, ok := z.players[msg.PlayerID]
	if !ok {
		return
	}

	req := msg.Packet.Payload.(*pb.MoveRequest)

	newPos := mathutil.NewVec3(req.Position.X, req.Position.Y, req.Position.Z)
	vel := mathutil.NewVec3(req.Velocity.X, req.Velocity.Y, req.Velocity.Z)

	// Server-side speed validation.
	if vel.Length() > maxMoveSpeed*1.1 { // 10% tolerance
		vel = vel.Normalize().Scale(maxMoveSpeed)
	}

	oldPos := p.Position()
	p.SetPosition(newPos)
	p.SetRotation(req.Rotation)
	p.SetVelocity(vel)

	// Update AOI and handle events.
	events := z.aoi.UpdatePosition(p, oldPos, newPos)
	z.handleAOIEvents(events)

	// Check portal triggers.
	z.checkPortals(p, newPos)
}

func (z *Zone) handlePing(msg PlayerMessage) {
	p, ok := z.players[msg.PlayerID]
	if !ok {
		return
	}
	ping := msg.Packet.Payload.(*pb.Ping)
	p.Connection().Send(network.MsgPong, &pb.Pong{
		ClientTime: ping.ClientTime,
		ServerTime: time.Now().UnixMilli(),
	})
}

func (z *Zone) handleAOIToggle(msg PlayerMessage) {
	p, ok := z.players[msg.PlayerID]
	if !ok {
		return
	}
	req := msg.Packet.Payload.(*pb.AOIToggleRequest)
	z.ToggleAOI(req.Enabled)

	status := "disabled"
	if req.Enabled {
		status = "enabled"
	}
	p.Connection().Send(network.MsgAOIToggleResponse, &pb.AOIToggleResponse{
		Enabled: req.Enabled,
		Message: "AOI " + status,
	})
}

func (z *Zone) handleMetrics(msg PlayerMessage) {
	p, ok := z.players[msg.PlayerID]
	if !ok {
		return
	}
	p.Connection().Send(network.MsgServerMetrics, &pb.ServerMetrics{
		OnlinePlayers:  int32(len(z.players)),
		TotalEntities:  int32(len(z.entities)),
		TickDurationUs: z.lastTickDuration.Load(),
		AoiEnabled:     z.aoiEnabled,
	})
}

func (z *Zone) updateMovement() {
	// Movement is applied immediately in handleMoveRequest (client-authoritative position
	// with server validation). Future: add server-side physics/collision here.
}

func (z *Zone) broadcastState() {
	if len(z.players) == 0 {
		return
	}

	// For each player, send state updates of nearby entities.
	for _, p := range z.players {
		nearby := z.aoi.GetNearby(p)
		if len(nearby) == 0 {
			continue
		}

		states := make([]*pb.EntityState, 0, len(nearby))
		for _, e := range nearby {
			states = append(states, e.ToProto())
		}

		p.Connection().Send(network.MsgStateUpdate, &pb.StateUpdate{
			Entities:   states,
			ServerTick: z.tick,
		})
	}
}

func (z *Zone) handleAOIEvents(events []world.AOIEvent) {
	for _, evt := range events {
		observerPlayer, ok := z.players[evt.Observer.EntityID()]
		if !ok {
			continue
		}

		switch evt.Type {
		case world.AOIEnter:
			observerPlayer.Connection().Send(network.MsgSpawnEntity, &pb.SpawnEntity{
				Entity: evt.Target.ToProto(),
			})
		case world.AOILeave:
			observerPlayer.Connection().Send(network.MsgDespawnEntity, &pb.DespawnEntity{
				EntityId: evt.Target.EntityID(),
			})
		}
	}
}

func (z *Zone) sendNearbySnapshot(p PlayerEntity) {
	nearby := z.aoi.GetNearby(p)
	states := make([]*pb.EntityState, 0, len(nearby))
	for _, e := range nearby {
		states = append(states, e.ToProto())
	}
	p.Connection().Send(network.MsgStateUpdate, &pb.StateUpdate{
		Entities:   states,
		ServerTick: z.tick,
	})
}

// ─── Combat Handlers ────────────────────────────────────────

func (z *Zone) handleUseSkill(msg PlayerMessage) {
	p, ok := z.players[msg.PlayerID]
	if !ok {
		return
	}

	req := msg.Packet.Payload.(*pb.UseSkillRequest)

	var targetPos mathutil.Vec3
	if req.TargetPos != nil {
		targetPos = mathutil.NewVec3(req.TargetPos.X, req.TargetPos.Y, req.TargetPos.Z)
	}

	caster, ok := p.(combat.Combatant)
	if !ok {
		return
	}

	success, errMsg := z.combatMgr.UseSkill(
		caster,
		req.SkillId,
		req.TargetId,
		targetPos,
		func(id uint64) entity.Entity { return z.entities[id] },
		z.getEntitiesInRadius,
	)

	p.Connection().Send(network.MsgUseSkillResponse, &pb.UseSkillResponse{
		Success:      success,
		ErrorMessage: errMsg,
	})
}

func (z *Zone) handleRespawn(msg PlayerMessage) {
	p, ok := z.players[msg.PlayerID]
	if !ok {
		return
	}

	c, ok := p.(combat.Combatant)
	if !ok || c.IsAlive() {
		return
	}

	oldPos := p.Position()
	z.combatMgr.Respawn(c, z.spawnPos)

	// Update AOI for the new position.
	events := z.aoi.UpdatePosition(p, oldPos, z.spawnPos)
	z.handleAOIEvents(events)

	// Notify respawn.
	respawnEvt := &pb.CombatEvent{
		TargetId:    p.EntityID(),
		TargetHp:    c.HP(),
		TargetMaxHp: c.MaxHP(),
		EventType:   pb.CombatEventType_COMBAT_EVENT_RESPAWN,
	}
	z.broadcastCombatEvent(p, network.MsgCombatEvent, respawnEvt)

	p.Connection().Send(network.MsgRespawnResponse, &pb.RespawnResponse{
		Self: p.ToProto(),
	})
}

// broadcastCombatEvent sends a combat event to all players who can see the entity.
func (z *Zone) broadcastCombatEvent(ent entity.Entity, msgType uint16, msg interface{}) {
	protoMsg, ok := msg.(proto.Message)
	if !ok {
		return
	}
	data, err := network.Encode(msgType, protoMsg)
	if err != nil {
		return
	}

	// Send to the entity itself if it's a player.
	if p, ok := z.players[ent.EntityID()]; ok {
		p.Connection().SendRaw(data)
	}

	// Send to nearby players.
	for _, nearby := range z.aoi.GetNearby(ent) {
		if p, ok := z.players[nearby.EntityID()]; ok {
			p.Connection().SendRaw(data)
		}
	}
}

// sendToEntity sends a message to a specific entity (if it's a player).
func (z *Zone) sendToEntity(entityID uint64, msgType uint16, msg interface{}) {
	p, ok := z.players[entityID]
	if !ok {
		return
	}
	protoMsg, ok := msg.(proto.Message)
	if !ok {
		return
	}
	p.Connection().Send(msgType, protoMsg)
}

// getEntitiesInRadius returns all entities within a radius of a point.
func (z *Zone) getEntitiesInRadius(center mathutil.Vec3, radius float32) []entity.Entity {
	radiusSq := radius * radius
	var result []entity.Entity
	for _, e := range z.entities {
		if e.Position().DistanceSqTo(center) <= radiusSq {
			result = append(result, e)
		}
	}
	return result
}

// ─── AI / Mob Management ────────────────────────────────────

// Spawner returns the zone's mob spawner for external configuration.
func (z *Zone) Spawner() *ai.Spawner { return z.spawner }

func (z *Zone) addMob(m *ai.Mob) {
	z.entities[m.EntityID()] = m
	z.aoi.Add(m)

	// Notify nearby players about the new mob.
	spawnData, _ := network.Encode(network.MsgSpawnEntity, &pb.SpawnEntity{Entity: m.ToProto()})
	for _, nearby := range z.aoi.GetNearby(m) {
		if p, ok := z.players[nearby.EntityID()]; ok {
			p.Connection().SendRaw(spawnData)
		}
	}
}

func (z *Zone) removeMob(mobID uint64) {
	ent, ok := z.entities[mobID]
	if !ok {
		return
	}

	events := z.aoi.Remove(ent)
	z.handleAOIEvents(events)
	z.combatMgr.RemoveEntity(mobID)
	delete(z.entities, mobID)
}

func (z *Zone) updateAI() {
	dt := z.cfg.TickInterval()
	for _, m := range z.spawner.AliveMobs() {
		oldPos := m.Position()
		ai.UpdateMob(m, dt, z, z)
		newPos := m.Position()

		// Update AOI if mob moved.
		if oldPos != newPos {
			events := z.aoi.UpdatePosition(m, oldPos, newPos)
			z.handleAOIEvents(events)
		}
	}
}

func (z *Zone) checkDeaths() {
	for _, m := range z.spawner.AliveMobs() {
		if !m.IsAlive() {
			z.spawner.NotifyDeath(m.EntityID())
		}
	}
}

// ─── ai.EntityProvider implementation ───────────────────────

func (z *Zone) GetEntity(id uint64) entity.Entity {
	return z.entities[id]
}

func (z *Zone) GetPlayersInRange(center mathutil.Vec3, radius float32) []entity.Entity {
	radiusSq := radius * radius
	var result []entity.Entity
	for _, p := range z.players {
		if p.Position().DistanceSqTo(center) <= radiusSq {
			result = append(result, p)
		}
	}
	return result
}

// ─── ai.SkillUser implementation ────────────────────────────

func (z *Zone) UseSkill(casterID uint64, skillID uint32, targetID uint64, targetPos mathutil.Vec3) (bool, string) {
	ent := z.entities[casterID]
	if ent == nil {
		return false, "caster not found"
	}
	caster, ok := ent.(combat.Combatant)
	if !ok {
		return false, "caster cannot fight"
	}

	return z.combatMgr.UseSkill(
		caster, skillID, targetID, targetPos,
		func(id uint64) entity.Entity { return z.entities[id] },
		z.getEntitiesInRadius,
	)
}

// ─── Zone Portals ───────────────────────────────────────────

func (z *Zone) checkPortals(p PlayerEntity, pos mathutil.Vec3) {
	if z.onZoneTransfer == nil || len(z.portals) == 0 {
		return
	}
	for _, portal := range z.portals {
		if portal.IsInRange(pos) {
			z.onZoneTransfer(p.EntityID(), portal.TargetZoneID, portal.TargetPos)
			return
		}
	}
}
