package client

import (
	"encoding/binary"
	"fmt"
	"log"
	"strings"
	"unicode"

	"go-ts/pkg/protocol"

	"github.com/dgryski/go-quicklz"
)

// sanitizeForLog removes control characters that can corrupt terminal output
func sanitizeForLog(s string) string {
	var result strings.Builder
	result.Grow(len(s))
	for _, r := range s {
		if r >= 32 && r < 127 {
			// Printable ASCII
			result.WriteRune(r)
		} else if unicode.IsPrint(r) && r < 256 {
			// Printable extended ASCII
			result.WriteRune(r)
		} else if r == '\n' || r == '\r' || r == '\t' {
			// Keep whitespace
			result.WriteRune(r)
		} else {
			// Replace control characters with placeholder
			result.WriteRune('.')
		}
	}
	return result.String()
}

func (c *Client) handleCommand(pkt *protocol.Packet) error {
	// Check if Encrypted
	// PacketTypeCommand is usually encrypted.
	// Flag check? The flag is in the Header (e.g. Unencrypted flag).
	// If Unencrypted flag is SET, it's cleartext.
	// Spec: "Command ... Encrypted: ✓". So Unencrypted flag is CLEARED.

	// Decrypt if necessary
	var data []byte
	var err error

	if pkt.Header.FlagUnencrypted() {
		data = pkt.Data
	} else {
		var key, nonce []byte
		decrypted := false

		// 1. Try SharedSecret if available
		if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 {
			// Use SharedSecret-based encryption
			crypto := &protocol.CryptoState{
				SharedIV:     c.Handshake.SharedIV,
				SharedMac:    c.Handshake.SharedMac,
				GenerationID: 0,
			}
			// Server->Client = false
			key, nonce = crypto.GenerateKeyNonce(&pkt.Header, false)

			// AAD for Server->Client: PacketID (2) + Type|Flags (1)
			meta := make([]byte, 3)
			binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
			meta[2] = pkt.Header.Type // Type includes Flags

			data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:])
			if err == nil {
				decrypted = true
			} else {
				log.Printf("SharedSecret decrypt failed (PID=%d): %v. Trying HandshakeKey...", pkt.Header.PacketID, err)
			}
		}

		// 2. Fallback to HandshakeKey
		if !decrypted {
			key = protocol.HandshakeKey[:]
			nonce = protocol.HandshakeNonce[:]

			// AAD matching KeyNonce derivation context?
			// HandshakeKey usage usually has same AAD requirements?
			meta := make([]byte, 3)
			binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
			meta[2] = pkt.Header.Type // Type includes Flags

			data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:])
			if err != nil {
				log.Printf("All decryption attempts failed for PID=%d: %v", pkt.Header.PacketID, err)
				return fmt.Errorf("decryption failed: %v", err)
			}
		}
	}
	// On first encrypted command set Connected = true (Fallback if ACK missed)
	if !c.Connected && pkt.Header.PacketID > 2 {
		c.Connected = true
	}

	// Queue-based fragment reassembly (like ts3j)
	// Store packet in queue
	c.CommandQueue[pkt.Header.PacketID] = &protocol.Packet{
		Header: pkt.Header,
		Data:   append([]byte{}, data...), // Clone data (already decrypted)
	}

	// Try to process packets in order
	for {
		nextPkt, ok := c.CommandQueue[c.ExpectedCommandPID]
		if !ok {
			// Missing packet, wait for it
			break
		}

		isFragmented := nextPkt.Header.FlagFragmented()

		if isFragmented {
			// Toggle fragment state
			c.FragmentState = !c.FragmentState

			if c.FragmentState {
				// Starting a new fragment sequence
				// Don't process yet, wait for more
				c.ExpectedCommandPID++
				continue
			} else {
				// Ending fragment sequence - reassemble all
				reassembled, compressed := c.reassembleFragments()
				if reassembled == nil {
					log.Printf("Fragment reassembly failed")
					break
				}
				data = reassembled

				// Decompress if first packet was compressed
				if compressed {
					decompressed, err := quicklz.Decompress(data)
					if err != nil {
						log.Printf("QuickLZ decompression of fragmented data failed: %v", err)
					} else {
						log.Printf("Decompressed fragmented: %d -> %d bytes", len(data), len(decompressed))
						data = decompressed
					}
				}
			}
		} else if c.FragmentState {
			// Middle fragment - keep collecting
			c.ExpectedCommandPID++
			continue
		} else {
			// Non-fragmented packet - process normally
			data = nextPkt.Data

			// Decompress if needed
			if nextPkt.Header.FlagCompressed() {
				decompressed, err := quicklz.Decompress(data)
				if err != nil {
					log.Printf("QuickLZ decompression failed: %v (falling back to raw)", err)
				} else {
					log.Printf("Decompressed: %d -> %d bytes", len(data), len(decompressed))
					data = decompressed
				}
			}
		}

		// Remove processed packet from queue
		delete(c.CommandQueue, c.ExpectedCommandPID)
		c.ExpectedCommandPID++

		// Process the command
		if err := c.processCommand(data, nextPkt); err != nil {
			log.Printf("Error processing command: %v", err)
		}
	}

	return nil
}

// reassembleFragments collects all buffered fragments in order and returns reassembled data
func (c *Client) reassembleFragments() ([]byte, bool) {
	var result []byte
	compressed := false

	// Find the start of the fragment sequence (scan backwards from current)
	startPID := c.ExpectedCommandPID
	for {
		prevPID := startPID - 1
		pkt, ok := c.CommandQueue[prevPID]
		if !ok {
			break
		}
		// Check if this is the start (has Fragmented flag)
		if pkt.Header.FlagFragmented() {
			startPID = prevPID
			break
		}
		startPID = prevPID
	}

	// Now collect from startPID to ExpectedCommandPID (inclusive)
	for pid := startPID; pid <= c.ExpectedCommandPID; pid++ {
		pkt, ok := c.CommandQueue[pid]
		if !ok {
			log.Printf("Missing fragment PID=%d during reassembly", pid)
			return nil, false
		}

		// First fragment may have compressed flag
		if pid == startPID && pkt.Header.FlagCompressed() {
			compressed = true
		}

		result = append(result, pkt.Data...)
		delete(c.CommandQueue, pid)
	}

	log.Printf("Reassembled fragments PID %d-%d, total %d bytes, compressed=%v",
		startPID, c.ExpectedCommandPID, len(result), compressed)

	return result, compressed
}

// processCommand handles a single fully reassembled command
func (c *Client) processCommand(data []byte, pkt *protocol.Packet) error {
	cmdStr := string(data)

	// Debug: Log packet flags and raw command preview (sanitized)
	log.Printf("Debug Packet: Compressed=%v, Fragmented=%v, RawLen=%d, Preview=%q",
		pkt.Header.FlagCompressed(), pkt.Header.FlagFragmented(), len(data),
		func() string {
			preview := cmdStr
			if len(preview) > 100 {
				preview = preview[:100]
			}
			return sanitizeForLog(preview)
		}())

	// Fix Garbage Headers (TS3 often sends binary garbage before command)
	// Scan for first valid lower case [a-z] char (Most commands are lowercase)
	validStart := strings.IndexFunc(cmdStr, func(r rune) bool {
		return (r >= 'a' && r <= 'z')
	})

	if validStart > 0 && validStart < 50 {
		cmdStr = cmdStr[validStart:]
	}

	log.Printf("Command: %s", sanitizeForLog(cmdStr))

	// Parse Commands (possibly multiple piped items)
	commands := protocol.ParseCommands([]byte(cmdStr))

	// State for piped commands (TS3 optimization omits repeated keys like ctid)
	var lastCtid uint64

	for _, command := range commands {
		cmd := command.Name
		args := command.Params

		switch cmd {
		case "initivexpand2":
			err := c.Handshake.ProcessInitivexpand2(args)
			if err != nil {
				log.Printf("Error processing initivexpand2: %v", err)
			}
		case "initserver":
			// Server sends this after clientinit - contains our clientID
			if cid, ok := args["aclid"]; ok {
				var id uint64
				fmt.Sscanf(cid, "%d", &id)
				c.ClientID = uint16(id)
				log.Printf("Assigned ClientID: %d", c.ClientID)
			}
			if name, ok := args["virtualserver_name"]; ok {
				c.ServerName = protocol.Unescape(name)
				log.Printf("Server Name: %s", c.ServerName)
			}
			c.emitEvent("connected", map[string]any{
				"clientID":   c.ClientID,
				"serverName": c.ServerName,
			})
		case "channellist":
			// Parse channel info
			ch := &Channel{}
			if cid, ok := args["cid"]; ok {
				fmt.Sscanf(cid, "%d", &ch.ID)
			}
			if pid, ok := args["cpid"]; ok {
				fmt.Sscanf(pid, "%d", &ch.ParentID)
			}
			if name, ok := args["channel_name"]; ok {
				ch.Name = protocol.Unescape(name)
			}
			if order, ok := args["channel_order"]; ok {
				fmt.Sscanf(order, "%d", &ch.Order)
			}
			c.Channels[ch.ID] = ch
			log.Printf("Channel: [%d] NameRaw=%q Order=%d Args=%v", ch.ID, ch.Name, ch.Order, args)
		case "channellistfinished":
			log.Printf("=== Channel List Complete (%d channels) ===", len(c.Channels))
			var channelList []*Channel
			var targetChan *Channel
			for _, ch := range c.Channels {
				log.Printf("  - [%d] %s (parent=%d)", ch.ID, ch.Name, ch.ParentID)
				channelList = append(channelList, ch)
				if ch.Name == "Test" {
					targetChan = ch
				}
			}
			c.emitEvent("channel_list", map[string]any{
				"channels": channelList,
			})

			if targetChan == nil {
				if ch, ok := c.Channels[2]; ok {
					log.Printf("Name parsing failed. Defaulting to Channel 2 as 'Test'.")
					targetChan = ch
				}
			}

			if targetChan != nil {
				log.Printf("Found target channel 'Test' (ID=%d). Joining...", targetChan.ID)

				if c.ClientID == 0 {
					log.Println("ERROR: ClientID is 0. Cannot join channel. 'initserver' missing?")
					return nil
				}

				moveCmd := protocol.NewCommand("clientmove")
				moveCmd.AddParam("clid", fmt.Sprintf("%d", c.ClientID))
				moveCmd.AddParam("cid", fmt.Sprintf("%d", targetChan.ID))
				moveCmd.AddParam("cpw", "")

				return c.SendCommand(moveCmd)
			}
		case "clientlist":
			// Parse client info (usually received after connection for all clients)
			nick := ""
			clientID := uint16(0)
			channelID := uint64(0)
			if n, ok := args["client_nickname"]; ok {
				nick = protocol.Unescape(n)
			}
			if cid, ok := args["clid"]; ok {
				var id uint64
				fmt.Sscanf(cid, "%d", &id)
				clientID = uint16(id)
			}
			if ctid, ok := args["cid"]; ok {
				fmt.Sscanf(ctid, "%d", &channelID)
			}

			// Don't emit for ourselves if we already handle it in initserver
			if clientID != c.ClientID && clientID != 0 {
				log.Printf("Existing client: %s (ID=%d) in Channel %d", nick, clientID, channelID)
				c.emitEvent("client_enter", map[string]any{
					"clientID":  clientID,
					"nickname":  nick,
					"channelID": channelID,
				})
			}
		case "notifycliententerview":
			// A client entered the server
			nick := ""
			clientID := uint16(0)
			channelID := uint64(0)
			if n, ok := args["client_nickname"]; ok {
				nick = protocol.Unescape(n)
			}
			if cid, ok := args["clid"]; ok {
				var id uint64
				fmt.Sscanf(cid, "%d", &id)
				clientID = uint16(id)
			}
			if ctid, ok := args["ctid"]; ok {
				fmt.Sscanf(ctid, "%d", &lastCtid)
			}
			channelID = lastCtid
			log.Printf("Client entered: %s (ID=%d)", nick, clientID)
			c.emitEvent("client_enter", map[string]any{
				"clientID":  clientID,
				"nickname":  nick,
				"channelID": channelID,
			})
		case "notifytextmessage":
			// targetmode: 1=Private, 2=Channel, 3=Server
			msg := ""
			invoker := "Unknown"
			var invokerID uint16
			var targetModeInt int
			if m, ok := args["msg"]; ok {
				msg = protocol.Unescape(m)
			}
			if name, ok := args["invokername"]; ok {
				invoker = protocol.Unescape(name)
			}
			if iid, ok := args["invokerid"]; ok {
				var id uint64
				fmt.Sscanf(iid, "%d", &id)
				invokerID = uint16(id)
			}

			targetMode := "Unknown"
			if tm, ok := args["targetmode"]; ok {
				switch tm {
				case "1":
					targetMode = "Private"
					targetModeInt = 1
				case "2":
					targetMode = "Channel"
					targetModeInt = 2
				case "3":
					targetMode = "Server"
					targetModeInt = 3
				}
			}

			log.Printf("[Chat][%s] %s: %s", targetMode, invoker, msg)
			c.emitEvent("message", map[string]any{
				"senderID":   invokerID,
				"senderName": invoker,
				"message":    msg,
				"targetMode": targetModeInt,
			})

		case "notifyclientchatcomposing":
			// Someone is typing
			// We only get clid, need to map to name if possible, or just log clid
			clid := "Unknown"
			if id, ok := args["clid"]; ok {
				clid = id
			}
			log.Printf("Client %s is typing...", clid)

		case "notifyclientmoved":
			// Client moved to another channel
			var clientID uint16
			var channelID uint64
			if cid, ok := args["clid"]; ok {
				var id uint64
				fmt.Sscanf(cid, "%d", &id)
				clientID = uint16(id)
			}
			if ctid, ok := args["ctid"]; ok {
				fmt.Sscanf(ctid, "%d", &channelID)
			}
			log.Printf("Client %d moved to Channel %d", clientID, channelID)
			c.emitEvent("client_moved", map[string]any{
				"clientID":  clientID,
				"channelID": channelID,
			})

		case "notifyclientchannelgroupchanged":
			// Client channel group changed
			// invokerid=0 invokername=Server cgid=8 cid=1 clid=3 cgi=1
			invoker := "Unknown"
			if name, ok := args["invokername"]; ok {
				invoker = protocol.Unescape(name)
			}
			log.Printf("Client %s channel group changed to %s in Channel %s by %s",
				args["clid"], args["cgid"], args["cid"], invoker)

		case "notifyconnectioninforequest":
			// Server asking for connection info. We MUST reply to update Ping in UI and avoid timeout.
			log.Println("Server requested connection info. Sending 'setconnectioninfo'...")

			cmd := protocol.NewCommand("setconnectioninfo")

			// Use real ping values if available, otherwise default to 50ms
			pingMs := c.PingRTT
			pingDev := c.PingDeviation
			if pingMs == 0 {
				pingMs = 50.0 // Default before first measurement
				pingDev = 5.0
			}

			cmd.AddParam("connection_ping", fmt.Sprintf("%.4f", pingMs))
			cmd.AddParam("connection_ping_deviation", fmt.Sprintf("%.4f", pingDev))

			// Detailed stats for each kind as seen in ts3j (KEEPALIVE, SPEECH, CONTROL)
			kinds := []string{"keepalive", "speech", "control"}
			for _, k := range kinds {
				cmd.AddParam("connection_packets_sent_"+k, "500")
				cmd.AddParam("connection_packets_received_"+k, "500")
				cmd.AddParam("connection_bytes_sent_"+k, "25000")
				cmd.AddParam("connection_bytes_received_"+k, "25000")
				cmd.AddParam("connection_bandwidth_sent_last_second_"+k, "200")
				cmd.AddParam("connection_bandwidth_received_last_second_"+k, "200")
				cmd.AddParam("connection_bandwidth_sent_last_minute_"+k, "200")
				cmd.AddParam("connection_bandwidth_received_last_minute_"+k, "200")
				cmd.AddParam("connection_server2client_packetloss_"+k, "0")
			}
			cmd.AddParam("connection_server2client_packetloss_total", "0")

			return c.SendCommand(cmd)

		case "notifyclientleftview":
			// Client left the server
			var clientID uint16
			reason := ""
			if cid, ok := args["clid"]; ok {
				var id uint64
				fmt.Sscanf(cid, "%d", &id)
				clientID = uint16(id)
			}
			if rid, ok := args["reasonid"]; ok {
				switch rid {
				case "3":
					reason = "connection lost"
				case "5":
					reason = "kicked"
				case "6":
					reason = "banned"
				case "8":
					reason = "leaving"
				default:
					reason = "unknown"
				}
			}
			if rmsg, ok := args["reasonmsg"]; ok {
				if rmsg != "" {
					reason = protocol.Unescape(rmsg)
				}
			}
			log.Printf("Client %d left: %s", clientID, reason)
			c.emitEvent("client_left", map[string]any{
				"clientID": clientID,
				"reason":   reason,
			})

		case "notifyclientupdated":
			// Client updated (e.g. muted/unmuted)
			clid := args["clid"]
			log.Printf("Client %s updated: %v", clid, args)

		case "error":
			// Server reported an error
			id := args["id"]
			msg := protocol.Unescape(args["msg"])
			log.Printf("SERVER ERROR: ID=%s MSG=%s", id, msg)
			c.emitEvent("error", map[string]any{
				"id":      id,
				"message": msg,
			})

		case "notifyservergrouplist", "notifychannelgrouplist", "notifyclientneededpermissions":
			// Ignore verbose noisy setup commands
		default:
			log.Printf("Unhandled command: %s Args: %v", cmd, args)
		}
	} // End for loop

	return nil
}
