package game

import (
	"context"
	"sync"
	"sync/atomic"
	"time"

	"a301_game_server/config"
	"a301_game_server/internal/ai"
	"a301_game_server/internal/auth"
	"a301_game_server/internal/db"
	"a301_game_server/internal/db/repository"
	"a301_game_server/internal/network"
	"a301_game_server/internal/player"
	"a301_game_server/internal/world"
	"a301_game_server/pkg/logger"
	"a301_game_server/pkg/mathutil"
	pb "a301_game_server/proto/gen/pb"
)

const defaultZoneID uint32 = 1

// GameServer is the top-level orchestrator that connects networking with game logic.
type GameServer struct {
	cfg      *config.Config
	world    *World
	sessions *player.SessionManager
	dbPool   *db.Pool
	authSvc  *auth.Service
	charRepo *repository.CharacterRepo

	mu         sync.RWMutex
	connPlayer map[uint64]*player.Player // connID -> player
	playerConn map[uint64]uint64         // playerID -> connID

	nextPlayerID atomic.Uint64
	cancelSave   context.CancelFunc
}

// NewGameServer creates the game server.
func NewGameServer(cfg *config.Config, dbPool *db.Pool) *GameServer {
	gs := &GameServer{
		cfg:        cfg,
		world:      NewWorld(cfg),
		sessions:   player.NewSessionManager(),
		dbPool:     dbPool,
		authSvc:    auth.NewService(dbPool),
		charRepo:   repository.NewCharacterRepo(dbPool),
		connPlayer: make(map[uint64]*player.Player),
		playerConn: make(map[uint64]uint64),
	}

	// Create zones, portals, and mobs.
	gs.setupWorld()

	return gs
}

// World returns the game world.
func (gs *GameServer) World() *World { return gs.world }

// Start launches all zone game loops and periodic save.
func (gs *GameServer) Start() {
	gs.world.mu.RLock()
	for _, zone := range gs.world.zones {
		zone.SetMessageHandler(gs)
		zone.SetZoneTransferCallback(gs.handleZoneTransfer)
	}
	gs.world.mu.RUnlock()

	gs.world.StartAll()

	// Start periodic character save.
	ctx, cancel := context.WithCancel(context.Background())
	gs.cancelSave = cancel
	go gs.periodicSave(ctx)
}

// Stop shuts down all zone game loops and saves all players.
func (gs *GameServer) Stop() {
	if gs.cancelSave != nil {
		gs.cancelSave()
	}

	// Final save of all online players.
	gs.saveAllPlayers()

	gs.world.StopAll()
}

// OnPacket handles incoming packets from a connection.
func (gs *GameServer) OnPacket(conn *network.Connection, pkt *network.Packet) {
	switch pkt.Type {
	case network.MsgLoginRequest:
		gs.handleLogin(conn, pkt)
	case network.MsgEnterWorldRequest:
		gs.handleEnterWorld(conn, pkt)
	default:
		gs.mu.RLock()
		p, ok := gs.connPlayer[conn.ID()]
		gs.mu.RUnlock()
		if !ok {
			return
		}

		zone, err := gs.world.GetZone(p.ZoneID())
		if err != nil {
			return
		}
		zone.EnqueueMessage(PlayerMessage{PlayerID: p.EntityID(), Packet: pkt})
	}
}

// OnDisconnect handles a connection closing.
func (gs *GameServer) OnDisconnect(conn *network.Connection) {
	gs.mu.Lock()
	p, ok := gs.connPlayer[conn.ID()]
	if !ok {
		gs.mu.Unlock()
		return
	}
	delete(gs.connPlayer, conn.ID())
	delete(gs.playerConn, p.EntityID())
	gs.mu.Unlock()

	// Save character to DB on disconnect.
	if p.CharID() != 0 {
		if err := gs.charRepo.Save(context.Background(), p.ToCharacterData()); err != nil {
			logger.Error("failed to save player on disconnect", "playerID", p.EntityID(), "error", err)
		}
	}

	zone, err := gs.world.GetZone(p.ZoneID())
	if err == nil {
		zone.EnqueueMessage(PlayerMessage{
			PlayerID: p.EntityID(),
			Packet:   &network.Packet{Type: msgPlayerDisconnect},
		})
	}

	logger.Info("player disconnected", "connID", conn.ID(), "playerID", p.EntityID())
}

// Internal message types.
const (
	msgPlayerDisconnect uint16 = 0xFFFF
	msgPlayerEnterWorld uint16 = 0xFFFE
)

// HandleZoneMessage implements ZoneMessageHandler.
func (gs *GameServer) HandleZoneMessage(zone *Zone, msg PlayerMessage) bool {
	switch msg.Packet.Type {
	case msgPlayerDisconnect:
		zone.RemovePlayer(msg.PlayerID)
		return true
	case msgPlayerEnterWorld:
		gs.mu.RLock()
		var found *player.Player
		for _, p := range gs.connPlayer {
			if p.EntityID() == msg.PlayerID {
				found = p
				break
			}
		}
		gs.mu.RUnlock()
		if found != nil {
			zone.AddPlayer(found)
		}
		return true
	default:
		return false
	}
}

func (gs *GameServer) handleLogin(conn *network.Connection, pkt *network.Packet) {
	req := pkt.Payload.(*pb.LoginRequest)
	ctx := context.Background()

	// Try login first.
	accountID, err := gs.authSvc.Login(ctx, req.Username, req.Password)
	if err != nil {
		// Auto-register if account doesn't exist.
		accountID, err = gs.authSvc.Register(ctx, req.Username, req.Password)
		if err != nil {
			conn.Send(network.MsgLoginResponse, &pb.LoginResponse{
				Success:      false,
				ErrorMessage: "login failed: " + err.Error(),
			})
			return
		}

		// Create default character on first registration.
		_, charErr := gs.charRepo.Create(ctx, accountID, req.Username)
		if charErr != nil {
			logger.Error("failed to create default character", "accountID", accountID, "error", charErr)
		}
	}

	session := gs.sessions.Create(uint64(accountID), req.Username)

	conn.Send(network.MsgLoginResponse, &pb.LoginResponse{
		Success:      true,
		SessionToken: session.Token,
		PlayerId:     uint64(accountID),
	})

	logger.Info("player logged in", "username", req.Username, "accountID", accountID)
}

func (gs *GameServer) handleEnterWorld(conn *network.Connection, pkt *network.Packet) {
	req := pkt.Payload.(*pb.EnterWorldRequest)

	session := gs.sessions.Get(req.SessionToken)
	if session == nil {
		conn.Send(network.MsgEnterWorldResponse, &pb.EnterWorldResponse{
			Success:      false,
			ErrorMessage: "invalid session",
		})
		return
	}

	ctx := context.Background()

	// Load character from DB.
	chars, err := gs.charRepo.GetByAccountID(ctx, int64(session.PlayerID))
	if err != nil || len(chars) == 0 {
		conn.Send(network.MsgEnterWorldResponse, &pb.EnterWorldResponse{
			Success:      false,
			ErrorMessage: "no character found",
		})
		return
	}

	charData := chars[0] // Use first character for now.
	p := player.NewPlayerFromDB(charData, conn)

	// Register connection-player mapping.
	gs.mu.Lock()
	gs.connPlayer[conn.ID()] = p
	gs.playerConn[p.EntityID()] = conn.ID()
	gs.mu.Unlock()

	zoneID := p.ZoneID()
	zone, err := gs.world.GetZone(zoneID)
	if err != nil {
		// Fall back to default zone.
		zoneID = defaultZoneID
		p.SetZoneID(defaultZoneID)
		zone, _ = gs.world.GetZone(defaultZoneID)
	}

	zone.EnqueueMessage(PlayerMessage{
		PlayerID: p.EntityID(),
		Packet:   &network.Packet{Type: msgPlayerEnterWorld},
	})

	conn.Send(network.MsgEnterWorldResponse, &pb.EnterWorldResponse{
		Success: true,
		Self:    p.ToProto(),
		ZoneId:  zoneID,
	})

	logger.Info("player entered world", "playerID", p.EntityID(), "charID", charData.ID, "zone", zoneID)
}

// periodicSave saves all dirty player data to DB at configured intervals.
func (gs *GameServer) periodicSave(ctx context.Context) {
	ticker := time.NewTicker(gs.cfg.Database.SaveInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			gs.saveAllPlayers()
		case <-ctx.Done():
			return
		}
	}
}

func (gs *GameServer) saveAllPlayers() {
	gs.mu.RLock()
	var dirty []*player.Player
	for _, p := range gs.connPlayer {
		if p.IsDirty() && p.CharID() != 0 {
			dirty = append(dirty, p)
		}
	}
	gs.mu.RUnlock()

	if len(dirty) == 0 {
		return
	}

	chars := make([]*repository.CharacterData, 0, len(dirty))
	for _, p := range dirty {
		chars = append(chars, p.ToCharacterData())
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := gs.charRepo.SaveBatch(ctx, chars); err != nil {
		logger.Error("periodic save failed", "count", len(chars), "error", err)
		return
	}

	for _, p := range dirty {
		p.ClearDirty()
	}

	logger.Debug("periodic save completed", "count", len(chars))
}

// setupWorld creates all zones, portals, and mob spawn points.
func (gs *GameServer) setupWorld() {
	zone1 := gs.world.CreateZone(1) // Starting zone - plains
	zone2 := gs.world.CreateZone(2) // Forest zone - medium difficulty
	zone3 := gs.world.CreateZone(3) // Volcano zone - hard

	// Set spawn positions.
	zone1.spawnPos = mathutil.NewVec3(0, 0, 0)
	zone2.spawnPos = mathutil.NewVec3(5, 0, 5)
	zone3.spawnPos = mathutil.NewVec3(10, 0, 10)

	// Portals: Zone 1 <-> Zone 2
	zone1.AddPortal(world.ZonePortal{
		SourceZoneID:  1,
		TriggerPos:    mathutil.NewVec3(300, 0, 150),
		TriggerRadius: 5.0,
		TargetZoneID:  2,
		TargetPos:     mathutil.NewVec3(5, 0, 5),
	})
	zone2.AddPortal(world.ZonePortal{
		SourceZoneID:  2,
		TriggerPos:    mathutil.NewVec3(0, 0, 0),
		TriggerRadius: 5.0,
		TargetZoneID:  1,
		TargetPos:     mathutil.NewVec3(295, 0, 150),
	})

	// Portals: Zone 2 <-> Zone 3
	zone2.AddPortal(world.ZonePortal{
		SourceZoneID:  2,
		TriggerPos:    mathutil.NewVec3(300, 0, 300),
		TriggerRadius: 5.0,
		TargetZoneID:  3,
		TargetPos:     mathutil.NewVec3(10, 0, 10),
	})
	zone3.AddPortal(world.ZonePortal{
		SourceZoneID:  3,
		TriggerPos:    mathutil.NewVec3(0, 0, 0),
		TriggerRadius: 5.0,
		TargetZoneID:  2,
		TargetPos:     mathutil.NewVec3(295, 0, 295),
	})

	// Populate zones with mobs.
	gs.setupZoneMobs(zone1, []mobSpawnConfig{
		{mobID: 1, count: 3, baseX: 20, baseZ: 30, spacing: 15},  // Goblins
		{mobID: 2, count: 2, baseX: 80, baseZ: 80, spacing: 12},  // Wolves
	})
	gs.setupZoneMobs(zone2, []mobSpawnConfig{
		{mobID: 2, count: 4, baseX: 50, baseZ: 50, spacing: 15},  // Wolves
		{mobID: 3, count: 2, baseX: 150, baseZ: 150, spacing: 20}, // Trolls
		{mobID: 4, count: 1, baseX: 200, baseZ: 50, spacing: 0},   // Fire Elemental
	})
	gs.setupZoneMobs(zone3, []mobSpawnConfig{
		{mobID: 4, count: 3, baseX: 80, baseZ: 80, spacing: 25},   // Fire Elementals
		{mobID: 5, count: 1, baseX: 200, baseZ: 200, spacing: 0},  // Dragon Whelp
	})
}

type mobSpawnConfig struct {
	mobID   uint32
	count   int
	baseX   float32
	baseZ   float32
	spacing float32
}

// setupZoneMobs configures mob spawn points for a zone.
func (gs *GameServer) setupZoneMobs(zone *Zone, configs []mobSpawnConfig) {
	registry := ai.NewMobRegistry()
	spawner := zone.Spawner()

	for _, cfg := range configs {
		def := registry.Get(cfg.mobID)
		if def == nil {
			continue
		}

		respawn := time.Duration(15+def.Level*3) * time.Second

		for i := 0; i < cfg.count; i++ {
			spawner.AddSpawnPoint(ai.SpawnPoint{
				MobDef:       def,
				Position:     mathutil.NewVec3(cfg.baseX+float32(i)*cfg.spacing, 0, cfg.baseZ+float32(i)*cfg.spacing),
				RespawnDelay: respawn,
				MaxCount:     1,
			})
		}
	}

	spawner.InitialSpawn()
}

// handleZoneTransfer moves a player between zones.
func (gs *GameServer) handleZoneTransfer(playerID uint64, targetZoneID uint32, targetPos mathutil.Vec3) {
	gs.mu.RLock()
	var p *player.Player
	var connID uint64
	for cid, pl := range gs.connPlayer {
		if pl.EntityID() == playerID {
			p = pl
			connID = cid
			break
		}
	}
	gs.mu.RUnlock()

	if p == nil {
		return
	}
	_ = connID

	sourceZone, err := gs.world.GetZone(p.ZoneID())
	if err != nil {
		return
	}
	targetZone, err := gs.world.GetZone(targetZoneID)
	if err != nil {
		logger.Warn("zone transfer target not found", "targetZone", targetZoneID)
		return
	}

	// Remove from source zone.
	sourceZone.RemovePlayer(playerID)

	// Update player state.
	p.SetPosition(targetPos)
	p.SetZoneID(targetZoneID)

	// Add to target zone via message queue.
	targetZone.EnqueueMessage(PlayerMessage{
		PlayerID: playerID,
		Packet:   &network.Packet{Type: msgPlayerEnterWorld},
	})

	// Notify client of zone change.
	p.Connection().Send(network.MsgZoneTransferNotify, &pb.ZoneTransferNotify{
		NewZoneId: targetZoneID,
		Self:      p.ToProto(),
	})

	logger.Info("zone transfer",
		"playerID", playerID,
		"from", sourceZone.ID(),
		"to", targetZoneID,
	)
}
