package teamspeak

import (
	"context"
	"errors"
	"fmt"
	"log"
	"log/slog"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/honeybbq/teamspeak-go/commands"
	"github.com/honeybbq/teamspeak-go/handshake"
	"github.com/honeybbq/teamspeak-go/transport"
)

var (
	errTeamSpeakCommand = errors.New("TeamSpeak server error")
	errCommandTimed     = errors.New("command timeout")
	voicePacketCount    uint64
)

type commandResult struct {
	Err  error
	Data []map[string]string
}

// commandTracker matches return_code values to pending commands and response rows.
type commandTracker struct {
	pending    map[uint32]chan commandResult
	collecting map[uint32][]map[string]string
	mu         sync.Mutex
	nextRC     uint32
}

func newCommandTracker() *commandTracker {
	return &commandTracker{
		pending:    make(map[uint32]chan commandResult),
		collecting: make(map[uint32][]map[string]string),
	}
}

func (t *commandTracker) register() (uint32, <-chan commandResult) {
	rc := atomic.AddUint32(&t.nextRC, 1)
	ch := make(chan commandResult, 1)
	t.mu.Lock()
	t.pending[rc] = ch
	t.mu.Unlock()

	return rc, ch
}

func (t *commandTracker) unregister(rc uint32) {
	t.mu.Lock()
	delete(t.pending, rc)
	t.mu.Unlock()
}

// collect appends a parameter row to the pending command with the largest return_code.
func (t *commandTracker) collect(params map[string]string) {
	t.mu.Lock()
	defer t.mu.Unlock()
	var maxRC uint32
	for rc := range t.pending {
		if rc > maxRC {
			maxRC = rc
		}
	}
	if maxRC > 0 {
		t.collecting[maxRC] = append(t.collecting[maxRC], params)
	}
}

func (t *commandTracker) resolve(rc uint32, err error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	if ch, ok := t.pending[rc]; ok {
		data := t.collecting[rc]
		delete(t.collecting, rc)
		ch <- commandResult{Data: data, Err: err}
	}
}

func (t *commandTracker) reset() {
	t.mu.Lock()
	t.pending = make(map[uint32]chan commandResult)
	t.collecting = make(map[uint32][]map[string]string)
	t.mu.Unlock()
}

func (c *Client) handlePacket(p *transport.Packet) {
	c.logger.Debug("received packet", slog.Uint64("type", uint64(p.Type())), slog.Int("length", len(p.Data)))
	switch p.Type() {
	case transport.PacketTypeInit1:
		c.logger.Debug("processing init1 packet")
		response := handshake.ProcessInit1(c.crypt, p.Data)
		if response != nil {
			c.logger.Debug("sending init1 response")
			err := c.handler.SendPacket(byte(transport.PacketTypeInit1), response, 0)
			if err != nil {
				c.logger.Warn("failed to send init1 response", slog.Any("error", err))
			}
		}
	case transport.PacketTypeCommand, transport.PacketTypeCommandLow:
		if len(p.Data) == 0 {
			return
		}
		dataStr := string(p.Data)
		c.logger.Debug("received command data", slog.String("data", dataStr))
		c.handleCommandLines(dataStr)
	case transport.PacketTypeVoice, transport.PacketTypeVoiceWhisper:
		// Voice payload format: [packetID(2 BE)] [clientID(2 BE)] [codec(1)] [opusData...].
		if len(p.Data) >= 5 {
			packetID := uint16(p.Data[0])<<8 | uint16(p.Data[1])
			clientID := uint16(p.Data[2])<<8 | uint16(p.Data[3])
			codec := p.Data[4]
			count := atomic.AddUint64(&voicePacketCount, 1)
			if count <= 5 || count%250 == 0 {
				log.Printf("[TSVoice] packet count=%d client=%d codec=%d sequence=%d payload=%d whisper=%t", count, clientID, codec, packetID, len(p.Data)-5, p.Type() == transport.PacketTypeVoiceWhisper)
			}
			opusData := make([]byte, len(p.Data)-5)
			copy(opusData, p.Data[5:])
			c.notifyEvent(VoiceDataEvent{
				ClientID:  clientID,
				Data:      opusData,
				Codec:     codec,
				Sequence:  packetID,
				IsWhisper: p.Type() == transport.PacketTypeVoiceWhisper,
			})
		}
	case transport.PacketTypePing, transport.PacketTypePong,
		transport.PacketTypeAck, transport.PacketTypeAckLow:
		return
	}
}

func (c *Client) handleCommandLines(s string) {
	if s == "" {
		return
	}
	start := 0
	for i := 0; i <= len(s); i++ {
		if i == len(s) || s[i] == '\n' || s[i] == 0x00 {
			part := strings.TrimSuffix(s[start:i], "\r")
			if part != "" {
				rows := splitCommandRows(part)
				for _, row := range rows {
					c.handleCommand(row)
				}
			}
			start = i + 1
		}
	}
}

func (c *Client) handleCommand(s string) {
	cmd := commands.ParseCommand(s)
	if cmd == nil {
		return
	}

	// Nameless rows are the data rows of list responses (clientlist,
	// channellist), which arrive as bare key=value pairs. Collect them for the
	// pending command instead of dropping them.
	if cmd.Name == "" {
		if len(cmd.Params) > 0 {
			c.cmdTrack.collect(cmd.Params)
		}

		return
	}

	c.logger.Debug("processing command", slog.String("name", cmd.Name), slog.Any("params", cmd.Params))

	if strings.HasPrefix(cmd.Name, "notify") {
		c.handleNotification(cmd)

		return
	}

	switch cmd.Name {
	case "clientinitiv":
		c.handleHandshakeInitIV(cmd)
	case "initivexpand2":
		c.handleHandshakeExpand2(cmd)
	case "initserver":
		c.handleInitServer(cmd)
	case "error":
		c.handleError(cmd)
	default:
		c.cmdTrack.collect(cmd.Params)
		c.logger.Debug("unhandled or data command", slog.String("name", cmd.Name), slog.Any("params", cmd.Params))
	}
}

func (c *Client) handleError(cmd *commands.Command) {
	id := cmd.Params["id"]
	msg := cmd.Params["msg"]
	rcStr := cmd.Params["return_code"]

	var err error
	if id != "0" {
		err = fmt.Errorf("%w: %s (id=%s)", errTeamSpeakCommand, msg, id)
		c.logger.Error("server returned error", slog.String("id", id), slog.String("message", msg))

		// Fatal server errors (e.g. banned) during the handshake are handled
		// below via signalConnected + transport close. The separate
		// go c.Disconnect() is only needed when already fully connected.
		if id == "3329" {
			c.mu.Lock()
			isConnecting := c.status == StatusConnecting
			c.mu.Unlock()
			if !isConnecting {
				c.logger.Warn("fatal connection error detected, closing connection", slog.String("id", id))
				go func() {
					disconnectErr := c.Disconnect()
					if disconnectErr != nil {
						c.logger.Warn("disconnect after fatal error failed", slog.Any("error", disconnectErr))
					}
				}()
			}
		}
	}

	// Handshake-phase errors (e.g. password error 1028, banned 3329) arrive
	// without a return_code.  When we are still in the connecting phase,
	// store the error, unblock WaitConnected, and close the transport so
	// the connection is cleaned up promptly.
	if rcStr == "" && id != "0" {
		c.mu.Lock()
		isConnecting := c.status == StatusConnecting
		c.mu.Unlock()
		if isConnecting {
			c.logger.Info("handshake error detected, unblocking WaitConnected",
				slog.String("id", id), slog.String("message", msg))
			c.signalConnected(err)
			// Close transport to trigger handleConnectionClosed and clean up.
			// handleConnectionClosed will fire disconnected handlers with the
			// stored handshake error.
			go func() {
				if closeErr := c.handler.Close(); closeErr != nil {
					c.logger.Warn("transport close after handshake error failed", slog.Any("error", closeErr))
				}
			}()
		}
	}

	if rcStr != "" {
		rc, parseErr := strconv.ParseUint(rcStr, 10, 32)
		if parseErr == nil {
			c.cmdTrack.resolve(uint32(rc), err)
		}
	}
}

// SendCommandNoWait sends a command without waiting for return_code.
func (c *Client) SendCommandNoWait(cmd string) error {
	err := c.throttle.wait(context.Background())
	if err != nil {
		return err
	}
	c.logger.Debug("sending command without waiting", slog.String("raw", cmd))

	return c.finalCmdHandler(cmd)
}

// ExecCommand sends a command and waits for its return_code response.
func (c *Client) ExecCommand(cmd string, timeout time.Duration) error {
	_, err := c.ExecCommandWithResponse(cmd, timeout)

	return err
}

// ExecCommandWithResponse sends a command and waits for its return_code response and data.
func (c *Client) ExecCommandWithResponse(cmd string, timeout time.Duration) ([]map[string]string, error) {
	rc, ch := c.cmdTrack.register()
	defer c.cmdTrack.unregister(rc)

	withReturnCode := cmd
	if !strings.Contains(cmd, "return_code=") {
		withReturnCode = fmt.Sprintf("%s return_code=%d", cmd, rc)
	}

	c.logger.Debug("sending command", slog.String("raw", withReturnCode))

	err := c.throttle.wait(context.Background())
	if err != nil {
		return nil, err
	}

	err = c.finalCmdHandler(withReturnCode)
	if err != nil {
		return nil, err
	}

	select {
	case res := <-ch:
		return res.Data, res.Err
	case <-time.After(timeout):
		return nil, fmt.Errorf("%w: %s", errCommandTimed, cmd)
	}
}
