package network

import (
	"encoding/binary"
	"errors"
	"fmt"

	"google.golang.org/protobuf/proto"

	pb "a301_game_server/proto/gen/pb"
)

// Message type IDs — the wire protocol uses 2-byte type prefixes.
const (
	// Auth
	MsgLoginRequest      uint16 = 0x0001
	MsgLoginResponse     uint16 = 0x0002
	MsgEnterWorldRequest uint16 = 0x0003
	MsgEnterWorldResponse uint16 = 0x0004

	// Movement
	MsgMoveRequest   uint16 = 0x0010
	MsgStateUpdate   uint16 = 0x0011
	MsgSpawnEntity   uint16 = 0x0012
	MsgDespawnEntity uint16 = 0x0013

	// Zone Transfer
	MsgZoneTransferNotify uint16 = 0x0014

	// System
	MsgPing uint16 = 0x0020
	MsgPong uint16 = 0x0021

	// Combat
	MsgUseSkillRequest  uint16 = 0x0040
	MsgUseSkillResponse uint16 = 0x0041
	MsgCombatEvent      uint16 = 0x0042
	MsgBuffApplied      uint16 = 0x0043
	MsgBuffRemoved      uint16 = 0x0044
	MsgRespawnRequest   uint16 = 0x0045
	MsgRespawnResponse  uint16 = 0x0046

	// Admin / Debug
	MsgAOIToggleRequest  uint16 = 0x0030
	MsgAOIToggleResponse uint16 = 0x0031
	MsgMetricsRequest    uint16 = 0x0032
	MsgServerMetrics     uint16 = 0x0033
)

var (
	ErrUnknownMessageType = errors.New("unknown message type")
	ErrMessageTooShort    = errors.New("message too short")
)

// Packet is a decoded network message.
type Packet struct {
	Type    uint16
	Payload proto.Message
}

// messageFactory maps type IDs to protobuf message constructors.
var messageFactory = map[uint16]func() proto.Message{
	MsgLoginRequest:       func() proto.Message { return &pb.LoginRequest{} },
	MsgLoginResponse:      func() proto.Message { return &pb.LoginResponse{} },
	MsgEnterWorldRequest:  func() proto.Message { return &pb.EnterWorldRequest{} },
	MsgEnterWorldResponse: func() proto.Message { return &pb.EnterWorldResponse{} },
	MsgMoveRequest:        func() proto.Message { return &pb.MoveRequest{} },
	MsgStateUpdate:        func() proto.Message { return &pb.StateUpdate{} },
	MsgSpawnEntity:        func() proto.Message { return &pb.SpawnEntity{} },
	MsgDespawnEntity:      func() proto.Message { return &pb.DespawnEntity{} },
	MsgPing:               func() proto.Message { return &pb.Ping{} },
	MsgPong:               func() proto.Message { return &pb.Pong{} },
	MsgZoneTransferNotify: func() proto.Message { return &pb.ZoneTransferNotify{} },
	MsgUseSkillRequest:    func() proto.Message { return &pb.UseSkillRequest{} },
	MsgUseSkillResponse:   func() proto.Message { return &pb.UseSkillResponse{} },
	MsgCombatEvent:        func() proto.Message { return &pb.CombatEvent{} },
	MsgBuffApplied:        func() proto.Message { return &pb.BuffApplied{} },
	MsgBuffRemoved:        func() proto.Message { return &pb.BuffRemoved{} },
	MsgRespawnRequest:     func() proto.Message { return &pb.RespawnRequest{} },
	MsgRespawnResponse:    func() proto.Message { return &pb.RespawnResponse{} },
	MsgAOIToggleRequest:   func() proto.Message { return &pb.AOIToggleRequest{} },
	MsgAOIToggleResponse:  func() proto.Message { return &pb.AOIToggleResponse{} },
	MsgMetricsRequest:     func() proto.Message { return &pb.MetricsRequest{} },
	MsgServerMetrics:      func() proto.Message { return &pb.ServerMetrics{} },
}

// Encode serializes a packet into a wire-format byte slice: [2-byte type][protobuf payload].
func Encode(msgType uint16, msg proto.Message) ([]byte, error) {
	payload, err := proto.Marshal(msg)
	if err != nil {
		return nil, fmt.Errorf("marshal message 0x%04X: %w", msgType, err)
	}

	buf := make([]byte, 2+len(payload))
	binary.BigEndian.PutUint16(buf[:2], msgType)
	copy(buf[2:], payload)
	return buf, nil
}

// Decode parses a wire-format byte slice into a Packet.
func Decode(data []byte) (*Packet, error) {
	if len(data) < 2 {
		return nil, ErrMessageTooShort
	}

	msgType := binary.BigEndian.Uint16(data[:2])

	factory, ok := messageFactory[msgType]
	if !ok {
		return nil, fmt.Errorf("%w: 0x%04X", ErrUnknownMessageType, msgType)
	}

	msg := factory()
	if len(data) > 2 {
		if err := proto.Unmarshal(data[2:], msg); err != nil {
			return nil, fmt.Errorf("unmarshal message 0x%04X: %w", msgType, err)
		}
	}

	return &Packet{Type: msgType, Payload: msg}, nil
}
