package teamspeak

import (
	"context"
	"errors"
	"log/slog"
	"strings"
	"sync"
	"time"

	"github.com/honeybbq/teamspeak-go/crypto"
	"github.com/honeybbq/teamspeak-go/discovery"
	"github.com/honeybbq/teamspeak-go/transport"
)

var errAlreadyConnectingOrConnected = errors.New("already connecting or connected")

// ClientStatus represents the current connection state of the client.
type ClientStatus int

const (
	StatusDisconnected ClientStatus = iota
	StatusConnecting
	StatusConnected
)

// AddrResolver resolves a TeamSpeak server address to host:port endpoints.
// Implementations may replace the default chain (SRV, TSDNS, direct).
type AddrResolver interface {
	Resolve(ctx context.Context, addr string) ([]discovery.ResolvedAddr, error)
}

// CommandMiddleware wraps the final command sender; it may alter or drop commands.
type CommandMiddleware func(next func(string) error) func(string) error

// EventMiddleware wraps event dispatch; it may observe or replace notifications.
type EventMiddleware func(next func(any)) func(any)

type clientInitOptions struct {
	serverPassword         string
	defaultChannel         string
	defaultChannelPassword string
}

// Client is the TeamSpeak 3 client.
type Client struct {
	resolver             AddrResolver
	finalCmdHandler      func(string) error
	crypt                *crypto.Crypt
	connectedChan        chan struct{}
	connectedErr         error // handshake error stored before closing connectedChan
	evtQueue             []any
	evtQueueMu           sync.Mutex
	evtCond              *sync.Cond
	evtDone              chan struct{}
	ftTrack              *fileTransferTracker
	logger               *slog.Logger
	handler              *transport.PacketHandler
	cmdTrack             *commandTracker
	throttle             *commandThrottle
	clients              map[uint16]ClientInfo
	finalEvtHandler      func(any)
	addr                 string
	nickname             string
	clientInitOptions    clientInitOptions
	textMsgHandlers      []func(TextMessage)
	cmdMiddlewares       []CommandMiddleware
	eventMiddlewares     []EventMiddleware
	clientEnterHandlers  []func(ClientInfo)
	clientLeaveHandlers  []func(ClientLeftViewEvent)
	clientMoveHandlers   []func(ClientMovedEvent)
	connectedHandlers    []func()
	disconnectedHandlers []func(error)
	pokedHandlers        []func(PokeEvent)
	kickedHandlers       []func(string)
	voiceDataHandlers    []func(VoiceDataEvent)
	status               ClientStatus
	mu                   sync.Mutex
	clid                 uint16
}

// NewClient creates a new TeamSpeak 3 client.
func NewClient(identity *crypto.Identity, addr string, nickname string, options ...ClientOption) *Client {
	crypt := crypto.NewCrypt(identity)

	c := &Client{
		crypt:         crypt,
		status:        StatusDisconnected,
		logger:        slog.Default(),
		addr:          addr,
		nickname:      nickname,
		clients:       make(map[uint16]ClientInfo),
		throttle:      newCommandThrottle(),
		cmdTrack:      newCommandTracker(),
		ftTrack:       newFileTransferTracker(),
		connectedChan: make(chan struct{}),
		evtDone:       make(chan struct{}),
	}

	c.evtCond = sync.NewCond(&c.evtQueueMu)
	c.startEventLoop()

	for _, opt := range options {
		opt(c)
	}

	c.handler = transport.NewPacketHandler(c.crypt, c.logger)
	if c.resolver == nil {
		c.resolver = discovery.NewResolver(c.logger)
	}
	c.handler.OnPacket = c.handlePacket
	c.handler.OnClosed = c.handleConnectionClosed

	c.rebuildMiddlewareChains()

	return c
}

type ClientOption func(*Client)

func WithLogger(logger *slog.Logger) ClientOption {
	return func(c *Client) {
		if logger != nil {
			c.logger = logger
		}
	}
}

// WithResolver sets a custom address resolver used by Connect.
func WithResolver(r AddrResolver) ClientOption {
	return func(c *Client) {
		c.resolver = r
	}
}

func WithCommandMiddleware(mw ...CommandMiddleware) ClientOption {
	return func(c *Client) {
		c.cmdMiddlewares = append(c.cmdMiddlewares, mw...)
	}
}

func WithEventMiddleware(mw ...EventMiddleware) ClientOption {
	return func(c *Client) {
		c.eventMiddlewares = append(c.eventMiddlewares, mw...)
	}
}

// WithServerPassword configures the server password sent during clientinit.
func WithServerPassword(password string) ClientOption {
	return func(c *Client) {
		c.clientInitOptions.serverPassword = password
	}
}

// WithDefaultChannel configures the default channel requested during clientinit.
func WithDefaultChannel(channel string) ClientOption {
	return func(c *Client) {
		c.clientInitOptions.defaultChannel = channel
	}
}

// WithDefaultChannelPassword configures the password for the default channel.
func WithDefaultChannelPassword(password string) ClientOption {
	return func(c *Client) {
		c.clientInitOptions.defaultChannelPassword = password
	}
}

// Connect starts the UDP session and handshake to the server.
func (c *Client) Connect() error {
	c.mu.Lock()
	if c.status != StatusDisconnected {
		c.mu.Unlock()

		return errAlreadyConnectingOrConnected
	}

	finalAddr := c.resetForConnectLocked()

	c.status = StatusConnecting
	c.mu.Unlock()

	targetAddr, source := c.resolveConnectTarget(finalAddr)
	c.logger.Info("connecting to server", slog.String("address", targetAddr), slog.String("source", source))

	return c.handler.Connect(targetAddr)
}

// Disconnect gracefully closes the connection.
func (c *Client) Disconnect() error {
	c.mu.Lock()
	oldStatus := c.status
	if oldStatus == StatusDisconnected {
		c.mu.Unlock()

		return nil
	}
	connectErr := c.connectedErr
	c.status = StatusDisconnected
	handlers := c.disconnectedHandlers
	c.mu.Unlock()

	c.logger.Info("disconnecting from server")

	if oldStatus == StatusConnected {
		_ = c.ExecCommand("clientdisconnect reasonmsg=Shutdown", 1*time.Second)
	}

	err := c.handler.Close()

	// When disconnecting during the handshake phase, pass the stored handshake
	// error (e.g. wrong password, banned) so callbacks receive a useful message.
	handlerErr := error(nil)
	if oldStatus == StatusConnecting && connectErr != nil {
		handlerErr = connectErr
	}
	// Invoke disconnected handlers here; handleConnectionClosed skips them once
	// status is already Disconnected to avoid duplicate callbacks.
	for _, h := range handlers {
		go h(handlerErr)
	}

	return err
}

func (c *Client) resetForConnectLocked() string {
	if c.handler != nil {
		_ = c.handler.Close()
	}
	identity := c.crypt.Identity
	c.crypt = crypto.NewCrypt(identity)
	c.handler = transport.NewPacketHandler(c.crypt, c.logger)
	c.handler.OnPacket = c.handlePacket
	c.handler.OnClosed = c.handleConnectionClosed
	c.connectedChan = make(chan struct{})
	// Note: connectedErr is NOT reset here so that handleConnectionClosed
	// can still access the handshake error when the server closes the
	// connection after a failed clientinit (e.g. wrong password).
	c.cmdTrack.reset()
	c.ftTrack.reset()
	c.clients = make(map[uint16]ClientInfo)
	c.clid = 0

	finalAddr := c.addr
	if !strings.Contains(finalAddr, ":") {
		c.logger.Debug("no port specified, using default port 9987")
		finalAddr += ":9987"
	}

	return finalAddr
}

func (c *Client) resolveConnectTarget(fallbackAddr string) (string, string) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	resolved, err := c.resolver.Resolve(ctx, c.addr)
	if err != nil {
		c.logger.Warn("address resolution failed, falling back to direct", slog.Any("error", err))

		return fallbackAddr, "Fallback"
	}

	return resolved[0].Addr, resolved[0].Source
}

func (c *Client) handleConnectionClosed(err error) {
	c.mu.Lock()
	if c.status == StatusDisconnected {
		c.mu.Unlock()

		return
	}

	wasConnecting := c.status == StatusConnecting
	connectErr := c.connectedErr
	c.status = StatusDisconnected
	handlers := c.disconnectedHandlers
	c.mu.Unlock()

	// If we were still in the handshake phase, unblock WaitConnected so the
	// caller can observe the failure instead of blocking forever.
	if wasConnecting {
		c.signalConnected(err)
	}

	// When the connection drops after a handshake error (e.g. wrong password),
	// the transport-level err is typically nil. Use the stored handshake error
	// so the disconnected handlers receive a meaningful message.
	handlerErr := err
	if handlerErr == nil && connectErr != nil {
		handlerErr = connectErr
	}

	for _, h := range handlers {
		go h(handlerErr)
	}
}

// signalConnected closes connectedChan (if not already closed) and stores err.
// Safe to call from any goroutine; acquires c.mu internally.
// A non-nil error never overwrites an existing non-nil error, so the original
// handshake error (e.g. wrong password) is preserved even if a later transport
// close also calls signalConnected with a nil or different error.
func (c *Client) signalConnected(err error) {
	c.mu.Lock()
	c.logger.Info("signalConnected called",
		slog.Any("error", err),
		slog.Any("existingErr", c.connectedErr),
		slog.Int("status", int(c.status)))
	if err != nil || c.connectedErr == nil {
		c.connectedErr = err
	}
	select {
	case <-c.connectedChan:
		c.logger.Info("signalConnected: channel already closed")
	default:
		close(c.connectedChan)
		c.logger.Info("signalConnected: channel closed successfully")
	}
	c.mu.Unlock()
}

// startEventLoop 启动事件消费协程，保证所有事件在同一个协程中按顺序处理。
// 消费协程的生命周期与 Client 相同（NewClient 时启动），
// 通过 c.evtDone channel 控制退出。
func (c *Client) startEventLoop() {
	go func() {
		for {
			// 等待队列非空或 done 信号
			c.evtQueueMu.Lock()
			for len(c.evtQueue) == 0 {
				select {
				case <-c.evtDone:
					c.evtQueueMu.Unlock()
					return
				default:
				}
				c.evtCond.Wait()
			}
			// 取出所有待处理事件
			events := make([]any, len(c.evtQueue))
			copy(events, c.evtQueue)
			c.evtQueue = nil
			c.evtQueueMu.Unlock()

			// 在无锁状态下逐个分发事件
			for _, evt := range events {
				c.dispatchEvent(evt)
			}
		}
	}()
}

// notifyEvent 将事件放入队列，由事件消费协程顺序处理。
// 线程安全，可从任何 goroutine 调用。
func (c *Client) notifyEvent(evt any) {
	c.evtQueueMu.Lock()
	c.evtQueue = append(c.evtQueue, evt)
	c.evtQueueMu.Unlock()
	c.evtCond.Signal()
}
