// testclient: 서버와 WebSocket 연결해서 전체 게임 흐름을 테스트하는 CLI 클라이언트.
//
// 사용법:
//
//	go run ./cmd/testclient                          # 기본 (ws://localhost:8080/ws, user1/pass1)
//	go run ./cmd/testclient -url ws://host:8080/ws  # 서버 주소 지정
//	go run ./cmd/testclient -user alice -pass secret
//	go run ./cmd/testclient -clients 5              # 5명 동시 접속 부하 테스트
//	go run ./cmd/testclient -scenario combat        # 전투 시나리오만 실행
package main

import (
	"encoding/binary"
	"flag"
	"fmt"
	"log"
	"math/rand"
	"net/url"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"github.com/gorilla/websocket"
	pb "a301_game_server/proto/gen/pb"
	"google.golang.org/protobuf/proto"
)

// ─── 메시지 타입 ID ────────────────────────────────────────────────
const (
	MsgLoginRequest       uint16 = 0x0001
	MsgLoginResponse      uint16 = 0x0002
	MsgEnterWorldRequest  uint16 = 0x0003
	MsgEnterWorldResponse uint16 = 0x0004

	MsgMoveRequest        uint16 = 0x0010
	MsgStateUpdate        uint16 = 0x0011
	MsgSpawnEntity        uint16 = 0x0012
	MsgDespawnEntity      uint16 = 0x0013
	MsgZoneTransferNotify uint16 = 0x0014

	MsgPing uint16 = 0x0020
	MsgPong uint16 = 0x0021

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

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

var msgTypeNames = map[uint16]string{
	MsgLoginRequest: "LoginRequest", MsgLoginResponse: "LoginResponse",
	MsgEnterWorldRequest: "EnterWorldRequest", MsgEnterWorldResponse: "EnterWorldResponse",
	MsgMoveRequest: "MoveRequest", MsgStateUpdate: "StateUpdate",
	MsgSpawnEntity: "SpawnEntity", MsgDespawnEntity: "DespawnEntity",
	MsgZoneTransferNotify: "ZoneTransferNotify",
	MsgPing: "Ping", MsgPong: "Pong",
	MsgUseSkillRequest: "UseSkillRequest", MsgUseSkillResponse: "UseSkillResponse",
	MsgCombatEvent: "CombatEvent", MsgBuffApplied: "BuffApplied",
	MsgBuffRemoved: "BuffRemoved",
	MsgRespawnRequest: "RespawnRequest", MsgRespawnResponse: "RespawnResponse",
	MsgAOIToggleRequest: "AOIToggleRequest", MsgAOIToggleResponse: "AOIToggleResponse",
	MsgMetricsRequest: "MetricsRequest", MsgServerMetrics: "ServerMetrics",
}

// ─── 클라이언트 ────────────────────────────────────────────────────
type Client struct {
	id       int
	username string
	password string
	conn     *websocket.Conn
	logger   *log.Logger

	sessionToken string
	playerID     uint64
	zoneID       uint32

	done    chan struct{}
	recvCh  chan recvMsg

	mu       sync.Mutex
	entities map[uint64]*pb.EntityState
}

type recvMsg struct {
	msgType uint16
	data    []byte
}

func newClient(id int, username, password string) *Client {
	return &Client{
		id:       id,
		username: username,
		password: password,
		logger:   log.New(os.Stdout, fmt.Sprintf("[client-%d] ", id), log.Ltime|log.Lmsgprefix),
		done:     make(chan struct{}),
		recvCh:   make(chan recvMsg, 64),
		entities: make(map[uint64]*pb.EntityState),
	}
}

func (c *Client) connect(serverURL string) error {
	u, err := url.Parse(serverURL)
	if err != nil {
		return err
	}

	conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
	if err != nil {
		return fmt.Errorf("dial failed: %w", err)
	}
	c.conn = conn
	c.logger.Printf("연결 성공: %s", serverURL)

	// 수신 루프
	go c.readLoop()
	return nil
}

func (c *Client) readLoop() {
	defer close(c.done)
	for {
		_, data, err := c.conn.ReadMessage()
		if err != nil {
			c.logger.Printf("연결 종료: %v", err)
			return
		}
		if len(data) < 2 {
			c.logger.Printf("패킷 너무 짧음 (%d bytes)", len(data))
			continue
		}
		msgType := binary.BigEndian.Uint16(data[:2])
		payload := data[2:]

		// Ping에는 자동으로 Pong 응답
		if msgType == MsgPing {
			var ping pb.Ping
			if err := proto.Unmarshal(payload, &ping); err == nil {
				_ = c.send(MsgPong, &pb.Pong{ClientTime: ping.ClientTime})
			}
			continue
		}

		select {
		case c.recvCh <- recvMsg{msgType, payload}:
		default:
			c.logger.Printf("recvCh 버퍼 풀 - 메시지 드롭: 0x%04X", msgType)
		}
	}
}

func (c *Client) send(msgType uint16, msg proto.Message) error {
	body, err := proto.Marshal(msg)
	if err != nil {
		return err
	}
	packet := make([]byte, 2+len(body))
	binary.BigEndian.PutUint16(packet[:2], msgType)
	copy(packet[2:], body)
	return c.conn.WriteMessage(websocket.BinaryMessage, packet)
}

// waitFor: 특정 메시지 타입이 올 때까지 대기 (timeout 적용)
func (c *Client) waitFor(msgType uint16, timeout time.Duration) ([]byte, error) {
	deadline := time.After(timeout)
	for {
		select {
		case msg := <-c.recvCh:
			name, ok := msgTypeNames[msg.msgType]
			if !ok {
				name = fmt.Sprintf("0x%04X", msg.msgType)
			}
			if msg.msgType == msgType {
				c.logger.Printf("  <- %s (받음)", name)
				return msg.data, nil
			}
			// 기다리는 타입이 아닌 것은 즉시 처리
			c.handleAsync(msg)
		case <-deadline:
			return nil, fmt.Errorf("타임아웃: 0x%04X 메시지 대기 중 %v 초과", msgType, timeout)
		case <-c.done:
			return nil, fmt.Errorf("연결 끊김")
		}
	}
}

// handleAsync: waitFor 중 받은 다른 메시지를 처리
func (c *Client) handleAsync(msg recvMsg) {
	name, ok := msgTypeNames[msg.msgType]
	if !ok {
		name = fmt.Sprintf("0x%04X", msg.msgType)
	}

	switch msg.msgType {
	case MsgStateUpdate:
		var upd pb.StateUpdate
		if err := proto.Unmarshal(msg.data, &upd); err == nil {
			c.mu.Lock()
			for _, e := range upd.Entities {
				c.entities[e.EntityId] = e
			}
			c.mu.Unlock()
		}
	case MsgSpawnEntity:
		var sp pb.SpawnEntity
		if err := proto.Unmarshal(msg.data, &sp); err == nil {
			c.mu.Lock()
			c.entities[sp.Entity.EntityId] = sp.Entity
			c.mu.Unlock()
			c.logger.Printf("  <- SpawnEntity: [%d] %s (HP:%d/%d) @ (%.1f,%.1f)",
				sp.Entity.EntityId, sp.Entity.Name, sp.Entity.Hp, sp.Entity.MaxHp,
				sp.Entity.Position.GetX(), sp.Entity.Position.GetZ())
		}
	case MsgDespawnEntity:
		var dp pb.DespawnEntity
		if err := proto.Unmarshal(msg.data, &dp); err == nil {
			c.mu.Lock()
			delete(c.entities, dp.EntityId)
			c.mu.Unlock()
			c.logger.Printf("  <- DespawnEntity: [%d]", dp.EntityId)
		}
	case MsgCombatEvent:
		var evt pb.CombatEvent
		if err := proto.Unmarshal(msg.data, &evt); err == nil {
			switch evt.EventType {
			case pb.CombatEventType_COMBAT_EVENT_DAMAGE:
				crit := ""
				if evt.IsCritical {
					crit = " [CRIT]"
				}
				c.logger.Printf("  <- CombatEvent: [%d] -> [%d] 데미지 %d%s (남은HP: %d/%d)",
					evt.CasterId, evt.TargetId, evt.Damage, crit, evt.TargetHp, evt.TargetMaxHp)
			case pb.CombatEventType_COMBAT_EVENT_HEAL:
				c.logger.Printf("  <- CombatEvent: [%d] 힐 +%d (HP: %d/%d)",
					evt.TargetId, evt.Heal, evt.TargetHp, evt.TargetMaxHp)
			case pb.CombatEventType_COMBAT_EVENT_DEATH:
				c.logger.Printf("  <- CombatEvent: [%d] 사망", evt.TargetId)
			case pb.CombatEventType_COMBAT_EVENT_RESPAWN:
				c.logger.Printf("  <- CombatEvent: [%d] 리스폰 (HP: %d/%d)",
					evt.TargetId, evt.TargetHp, evt.TargetMaxHp)
			}
		}
	case MsgBuffApplied:
		var b pb.BuffApplied
		if err := proto.Unmarshal(msg.data, &b); err == nil {
			debuff := ""
			if b.IsDebuff {
				debuff = "[디버프]"
			}
			c.logger.Printf("  <- BuffApplied%s: [%d] %s (%.1fs)", debuff, b.TargetId, b.BuffName, b.Duration)
		}
	case MsgBuffRemoved:
		var b pb.BuffRemoved
		if err := proto.Unmarshal(msg.data, &b); err == nil {
			c.logger.Printf("  <- BuffRemoved: [%d] buff#%d", b.TargetId, b.BuffId)
		}
	case MsgZoneTransferNotify:
		var z pb.ZoneTransferNotify
		if err := proto.Unmarshal(msg.data, &z); err == nil {
			c.zoneID = z.NewZoneId
			c.logger.Printf("  <- ZoneTransfer: 새 존 %d (주변 엔티티 %d개)", z.NewZoneId, len(z.NearbyEntities))
		}
	default:
		c.logger.Printf("  <- %s (비동기 수신)", name)
	}
}

// ─── 시나리오 ──────────────────────────────────────────────────────

// stepAuth: 로그인 → 월드 입장
func (c *Client) stepAuth() error {
	// 1. 로그인
	c.logger.Printf("→ LoginRequest (user=%s)", c.username)
	if err := c.send(MsgLoginRequest, &pb.LoginRequest{
		Username: c.username,
		Password: c.password,
	}); err != nil {
		return err
	}

	data, err := c.waitFor(MsgLoginResponse, 5*time.Second)
	if err != nil {
		return err
	}
	var resp pb.LoginResponse
	if err := proto.Unmarshal(data, &resp); err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("로그인 실패: %s", resp.ErrorMessage)
	}
	c.sessionToken = resp.SessionToken
	c.playerID = resp.PlayerId
	c.logger.Printf("  로그인 성공: playerID=%d, token=%s…", resp.PlayerId, resp.SessionToken[:8])

	// 2. 월드 입장
	c.logger.Printf("→ EnterWorldRequest")
	if err := c.send(MsgEnterWorldRequest, &pb.EnterWorldRequest{
		SessionToken: c.sessionToken,
	}); err != nil {
		return err
	}

	data, err = c.waitFor(MsgEnterWorldResponse, 5*time.Second)
	if err != nil {
		return err
	}
	var ewResp pb.EnterWorldResponse
	if err := proto.Unmarshal(data, &ewResp); err != nil {
		return err
	}
	if !ewResp.Success {
		return fmt.Errorf("월드 입장 실패: %s", ewResp.ErrorMessage)
	}
	c.zoneID = ewResp.ZoneId
	c.logger.Printf("  월드 입장 성공: zone=%d, pos=(%.1f, %.1f, %.1f)",
		ewResp.ZoneId,
		ewResp.Self.Position.GetX(),
		ewResp.Self.Position.GetY(),
		ewResp.Self.Position.GetZ())
	c.logger.Printf("  플레이어 정보: name=%s, HP=%d/%d, level=%d",
		ewResp.Self.Name, ewResp.Self.Hp, ewResp.Self.MaxHp, ewResp.Self.Level)
	return nil
}

// stepMove: 위치 이동 전송 후 StateUpdate 수신 확인
func (c *Client) stepMove() error {
	c.logger.Printf("─── 이동 테스트 ───")
	positions := [][2]float32{{5, 5}, {10, 10}, {15, 5}, {10, 0}}

	for _, pos := range positions {
		c.logger.Printf("→ MoveRequest (%.1f, 0, %.1f)", pos[0], pos[1])
		if err := c.send(MsgMoveRequest, &pb.MoveRequest{
			Position: &pb.Vector3{X: pos[0], Y: 0, Z: pos[1]},
			Velocity: &pb.Vector3{X: 1, Y: 0, Z: 0},
			Rotation: 0,
		}); err != nil {
			return err
		}
		time.Sleep(100 * time.Millisecond)
	}

	// StateUpdate 수신 대기 (이동 후 서버 틱 안에 옴)
	c.logger.Printf("StateUpdate 대기 중…")
	_, err := c.waitFor(MsgStateUpdate, 3*time.Second)
	if err != nil {
		c.logger.Printf("  경고: StateUpdate 없음 (혼자 접속 중이면 정상)")
		return nil
	}
	c.mu.Lock()
	c.logger.Printf("  현재 시야 내 엔티티 수: %d", len(c.entities))
	c.mu.Unlock()
	return nil
}

// stepCombat: 주변 몹 타겟팅 후 스킬 사용
func (c *Client) stepCombat() error {
	c.logger.Printf("─── 전투 테스트 ───")

	// 주변 몹 찾기 (SpawnEntity로 받은 것들 중 mob 타입)
	var mobID uint64
	c.mu.Lock()
	for id, e := range c.entities {
		if e.EntityType == pb.EntityType_ENTITY_TYPE_MOB {
			mobID = id
			c.logger.Printf("  타겟 몹 발견: [%d] %s (HP:%d/%d)", id, e.Name, e.Hp, e.MaxHp)
			break
		}
	}
	c.mu.Unlock()

	if mobID == 0 {
		c.logger.Printf("  주변에 몹이 없음 - 스킬 테스트 스킵")
		return nil
	}

	skills := []struct {
		id   uint32
		name string
	}{
		{1, "Basic Attack"},
		{2, "Fireball"},
	}

	for _, skill := range skills {
		c.logger.Printf("→ UseSkillRequest: %s (skillID=%d) → 타겟 [%d]", skill.name, skill.id, mobID)
		if err := c.send(MsgUseSkillRequest, &pb.UseSkillRequest{
			SkillId:  skill.id,
			TargetId: mobID,
		}); err != nil {
			return err
		}

		data, err := c.waitFor(MsgUseSkillResponse, 3*time.Second)
		if err != nil {
			return err
		}
		var resp pb.UseSkillResponse
		if err := proto.Unmarshal(data, &resp); err != nil {
			return err
		}
		if resp.Success {
			c.logger.Printf("  스킬 성공")
		} else {
			c.logger.Printf("  스킬 실패: %s", resp.ErrorMessage)
		}
		time.Sleep(200 * time.Millisecond) // 쿨다운 대기
	}

	return nil
}

// stepHeal: 자신 힐 스킬
func (c *Client) stepHeal() error {
	c.logger.Printf("─── 힐 테스트 ───")
	c.logger.Printf("→ UseSkillRequest: Heal (skillID=3, 셀프)")
	if err := c.send(MsgUseSkillRequest, &pb.UseSkillRequest{SkillId: 3}); err != nil {
		return err
	}
	data, err := c.waitFor(MsgUseSkillResponse, 3*time.Second)
	if err != nil {
		return err
	}
	var resp pb.UseSkillResponse
	if err := proto.Unmarshal(data, &resp); err != nil {
		return err
	}
	if resp.Success {
		c.logger.Printf("  힐 성공")
	} else {
		c.logger.Printf("  힐 실패: %s", resp.ErrorMessage)
	}
	return nil
}

// stepMetrics: 서버 메트릭 요청
func (c *Client) stepMetrics() error {
	c.logger.Printf("─── 서버 메트릭 ───")
	c.logger.Printf("→ MetricsRequest")
	if err := c.send(MsgMetricsRequest, &pb.MetricsRequest{}); err != nil {
		return err
	}
	data, err := c.waitFor(MsgServerMetrics, 3*time.Second)
	if err != nil {
		return err
	}
	var m pb.ServerMetrics
	if err := proto.Unmarshal(data, &m); err != nil {
		return err
	}
	c.logger.Printf("  온라인 플레이어: %d", m.OnlinePlayers)
	c.logger.Printf("  총 엔티티:       %d", m.TotalEntities)
	c.logger.Printf("  틱 시간:         %d µs", m.TickDurationUs)
	c.logger.Printf("  AOI 활성화:      %v", m.AoiEnabled)
	return nil
}

// stepAOIToggle: AOI on/off 토글 테스트
func (c *Client) stepAOIToggle(enabled bool) error {
	c.logger.Printf("─── AOI 토글 (enabled=%v) ───", enabled)
	if err := c.send(MsgAOIToggleRequest, &pb.AOIToggleRequest{Enabled: enabled}); err != nil {
		return err
	}
	data, err := c.waitFor(MsgAOIToggleResponse, 3*time.Second)
	if err != nil {
		return err
	}
	var resp pb.AOIToggleResponse
	if err := proto.Unmarshal(data, &resp); err != nil {
		return err
	}
	c.logger.Printf("  AOI 토글 결과: %s", resp.Message)
	return nil
}

// runScenario: 전체 또는 특정 시나리오 실행
func (c *Client) runScenario(scenario string) {
	// 공통: 인증
	if err := c.stepAuth(); err != nil {
		c.logger.Printf("[FAIL] 인증: %v", err)
		return
	}

	switch scenario {
	case "auth":
		c.logger.Printf("[OK] 인증 시나리오 완료")

	case "move":
		if err := c.stepMove(); err != nil {
			c.logger.Printf("[FAIL] 이동: %v", err)
			return
		}
		c.logger.Printf("[OK] 이동 시나리오 완료")

	case "combat":
		// 이동해서 몹 시야 안으로
		time.Sleep(500 * time.Millisecond) // SpawnEntity 수신 대기
		if err := c.stepCombat(); err != nil {
			c.logger.Printf("[FAIL] 전투: %v", err)
			return
		}
		if err := c.stepHeal(); err != nil {
			c.logger.Printf("[FAIL] 힐: %v", err)
			return
		}
		c.logger.Printf("[OK] 전투 시나리오 완료")

	case "metrics":
		if err := c.stepMetrics(); err != nil {
			c.logger.Printf("[FAIL] 메트릭: %v", err)
			return
		}
		if err := c.stepAOIToggle(false); err != nil {
			c.logger.Printf("[FAIL] AOI 토글: %v", err)
			return
		}
		if err := c.stepAOIToggle(true); err != nil {
			c.logger.Printf("[FAIL] AOI 토글: %v", err)
			return
		}
		c.logger.Printf("[OK] 메트릭 시나리오 완료")

	default: // "all"
		time.Sleep(500 * time.Millisecond) // SpawnEntity 수신 대기

		if err := c.stepMove(); err != nil {
			c.logger.Printf("[FAIL] 이동: %v", err)
		}
		if err := c.stepCombat(); err != nil {
			c.logger.Printf("[FAIL] 전투: %v", err)
		}
		if err := c.stepHeal(); err != nil {
			c.logger.Printf("[FAIL] 힐: %v", err)
		}
		if err := c.stepMetrics(); err != nil {
			c.logger.Printf("[FAIL] 메트릭: %v", err)
		}
		c.logger.Printf("[OK] 전체 시나리오 완료")
	}
}

// ─── 부하 테스트 ──────────────────────────────────────────────────
func loadTest(serverURL string, numClients int, duration time.Duration) {
	fmt.Printf("부하 테스트: %d 클라이언트, %v 동안\n", numClients, duration)
	var wg sync.WaitGroup
	start := time.Now()

	for i := 0; i < numClients; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			username := fmt.Sprintf("loadtest_%d_%d", id, rand.Intn(9999))
			c := newClient(id, username, "testpass")
			if err := c.connect(serverURL); err != nil {
				c.logger.Printf("[FAIL] 연결: %v", err)
				return
			}
			defer c.conn.Close()

			if err := c.stepAuth(); err != nil {
				c.logger.Printf("[FAIL] 인증: %v", err)
				return
			}

			// duration 동안 이동 패킷 전송
			ticker := time.NewTicker(50 * time.Millisecond)
			defer ticker.Stop()
			timeout := time.After(duration)
			x, z := float32(0), float32(0)

			for {
				select {
				case <-ticker.C:
					x += rand.Float32()*2 - 1
					z += rand.Float32()*2 - 1
					_ = c.send(MsgMoveRequest, &pb.MoveRequest{
						Position: &pb.Vector3{X: x, Y: 0, Z: z},
						Velocity: &pb.Vector3{X: 1, Y: 0, Z: 0},
					})
				case <-timeout:
					c.logger.Printf("완료 (%.1fs)", time.Since(start).Seconds())
					return
				case <-c.done:
					return
				}
			}
		}(i)
		time.Sleep(10 * time.Millisecond) // 동시 접속 분산
	}

	wg.Wait()
	fmt.Printf("부하 테스트 완료: %.1fs\n", time.Since(start).Seconds())
}

// ─── main ─────────────────────────────────────────────────────────
func main() {
	serverURL := flag.String("url", "ws://localhost:8080/ws", "WebSocket 서버 주소")
	username  := flag.String("user", "testuser1", "사용자 이름")
	password  := flag.String("pass", "password123", "비밀번호")
	scenario  := flag.String("scenario", "all", "실행할 시나리오: all | auth | move | combat | metrics")
	numClients := flag.Int("clients", 1, "동시 접속 클라이언트 수 (부하 테스트)")
	loadDuration := flag.Duration("duration", 10*time.Second, "부하 테스트 지속 시간")
	flag.Parse()

	// 부하 테스트 모드
	if *numClients > 1 {
		loadTest(*serverURL, *numClients, *loadDuration)
		return
	}

	// 단일 클라이언트 시나리오
	c := newClient(1, *username, *password)
	if err := c.connect(*serverURL); err != nil {
		log.Fatalf("서버 연결 실패: %v\n서버가 실행 중인지 확인하세요: make run", err)
	}
	defer c.conn.Close()

	c.runScenario(*scenario)

	// Ctrl+C 대기 (실시간 메시지 관찰용)
	if *scenario == "all" {
		fmt.Println("\n[Ctrl+C로 종료] 서버 메시지 수신 대기 중...")
		sig := make(chan os.Signal, 1)
		signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

		ticker := time.NewTicker(50 * time.Millisecond)
		defer ticker.Stop()
	loop:
		for {
			select {
			case <-sig:
				break loop
			case <-c.done:
				break loop
			case msg := <-c.recvCh:
				c.handleAsync(msg)
			case <-ticker.C:
			}
		}
	}
}
