package packets

import (
	"encoding/binary"
	"fmt"
	"time"

	"git.sharkk.net/EQ2/Protocol/crypto"
	"git.sharkk.net/EQ2/Protocol/opcodes"
)

// ProtoPacket handles low-level protocol features (matches EQProtocolPacket/EQ2Packet)
type ProtoPacket struct {
	*Packet

	// Protocol state flags
	eq2Compressed   bool
	packetPrepared  bool
	packetEncrypted bool
	acked           bool

	// EQ2-specific
	LoginOp opcodes.EmuOpcode

	// Reliability and sequencing
	Sequence     int32
	SentTime     int32
	AttemptCount int8

	// Opcode manager for translation
	manager opcodes.Manager

	// Compression/encoding settings
	CompressThreshold int
	EncodeKey         int

	// Client version for protocol compatibility
	clientVersion int16
}

const DefaultCompressThreshold = 100

// NewProtoPacket creates a protocol packet with opcode and buffer
func NewProtoPacket(op uint16, buf []byte, manager opcodes.Manager) *ProtoPacket {
	return &ProtoPacket{
		Packet:            NewPacket(op, buf),
		manager:           manager,
		CompressThreshold: DefaultCompressThreshold,
	}
}

// NewEQ2Packet creates an EQ2 protocol packet (matches EQ2Packet constructor)
func NewEQ2Packet(loginOp opcodes.EmuOpcode, buf []byte, manager opcodes.Manager) *ProtoPacket {
	return &ProtoPacket{
		Packet:            NewPacket(opcodes.OP_Packet, buf),
		LoginOp:           loginOp,
		manager:           manager,
		CompressThreshold: DefaultCompressThreshold,
	}
}

// NewProtoPacketFromRaw creates from raw buffer (matches EQProtocolPacket constructor)
func NewProtoPacketFromRaw(buf []byte, opcodeOverride int, manager opcodes.Manager) *ProtoPacket {
	var offset uint32
	var opcode uint16

	if opcodeOverride >= 0 {
		opcode = uint16(opcodeOverride)
	} else if len(buf) >= 2 {
		opcode = binary.BigEndian.Uint16(buf[:2])
		offset = 2
	}

	var data []byte
	if uint32(len(buf)) > offset {
		data = make([]byte, len(buf)-int(offset))
		copy(data, buf[offset:])
	}

	pp := &ProtoPacket{
		Packet: &Packet{
			Opcode:    opcode,
			Buffer:    data,
			Timestamp: time.Now(),
		},
		manager:           manager,
		CompressThreshold: DefaultCompressThreshold,
	}

	if pp.manager != nil {
		pp.LoginOp = pp.manager.EQToEmu(opcode)
	}

	return pp
}

// SetVersion sets the client version
func (p *ProtoPacket) SetVersion(version int16) {
	p.clientVersion = version
	p.Version = version
}

// SetSequence sets the packet sequence number
func (p *ProtoPacket) SetSequence(seq int32) {
	p.Sequence = seq
}

// IsPrepared returns whether packet has been prepared
func (p *ProtoPacket) IsPrepared() bool {
	return p.packetPrepared
}

// IsEncrypted returns whether packet is encrypted
func (p *ProtoPacket) IsEncrypted() bool {
	return p.packetEncrypted
}

// PreparePacket prepares an EQ2 packet for transmission (matches EQ2Packet::PreparePacket)
func (p *ProtoPacket) PreparePacket(maxLen uint16) int8 {
	if p.packetPrepared {
		return 0
	}

	if p.manager == nil {
		return -1
	}

	p.packetPrepared = true

	// Convert emulator opcode to network opcode
	loginOpcode := p.manager.EmuToEQ(p.LoginOp)
	if loginOpcode == 0xcdcd { // Invalid opcode
		return -1
	}

	// NOTE: Compression is NOT done here in C++, it's done separately
	// via EQ2_Compress after PreparePacket

	// Calculate new size: sequence(2) + compressed_flag(1) + opcode + data
	var offset int8
	newSize := len(p.Buffer) + 2 + 1 // seq(2) + compress_flag(1)
	oversized := false

	// Handle different opcode sizes and formats
	if loginOpcode != 2 { // Not OP_SessionResponse
		newSize++ // for opcode type byte
		if loginOpcode >= 255 {
			newSize += 2 // oversized opcode needs extra 2 bytes
			oversized = true
		} else {
			// Swap bytes for network order
			loginOpcode = (loginOpcode>>8)&0xFF | (loginOpcode<<8)&0xFF00
		}
	}

	// Build new buffer
	newBuffer := make([]byte, newSize)
	// Leave first 2 bytes for sequence (filled by SequencedPush)
	ptr := 2

	// Space for compress flag (will be 0 for uncompressed)
	ptr++ // This is at position 2, will be overwritten if compressed

	if loginOpcode != 2 {
		if oversized {
			newBuffer[ptr] = 0xff // Oversized marker
			ptr++
		}
		// Write opcode as 2 bytes
		binary.BigEndian.PutUint16(newBuffer[ptr:], loginOpcode)
		ptr += 2
	} else {
		// OP_SessionResponse: just 1 byte
		newBuffer[ptr] = byte(loginOpcode)
		ptr++
	}

	// Copy original packet data
	copy(newBuffer[ptr:], p.Buffer)

	// Replace buffer
	p.Buffer = newBuffer
	offset = int8(newSize - len(p.Buffer) - 1)

	return offset
}

// IsCompressed returns whether the packet is compressed
func (p *ProtoPacket) IsCompressed() bool {
	return p.eq2Compressed
}

// EQ2Compress compresses packet data (matches EQStream::EQ2_Compress and C++ EQProtocolPacket::Compress)
func (p *ProtoPacket) EQ2Compress(offset int8) int8 {
	if offset <= 0 || int(offset) >= len(p.Buffer) {
		return 0
	}

	// Compress data from offset onwards
	dataToCompress := p.Buffer[offset:]
	dataLen := len(dataToCompress)
	
	// C++ uses 30 bytes as threshold - match this exactly
	if dataLen > 30 {
		// Use zlib compression for larger packets
		compressed, err := Compress(dataToCompress)
		if err != nil || len(compressed) >= dataLen {
			return 0 // Compression failed or didn't reduce size
		}

		// Rebuild buffer with zlib compression flag (0x5a)
		newSize := int(offset) + len(compressed)
		newBuffer := make([]byte, newSize)

		// Copy header bytes before offset
		copy(newBuffer[:offset], p.Buffer[:offset])

		// Set zlib compression flag at offset-1
		newBuffer[offset-1] = 0x5a

		// Copy compressed data
		copy(newBuffer[offset:], compressed)

		p.Buffer = newBuffer
		p.eq2Compressed = true

		return offset - 1 // Return compression flag position
	} else {
		// Use simple encoding for smaller packets (0xa5)
		// Simple encoding just adds a flag, doesn't change data
		newSize := len(p.Buffer) + 1
		newBuffer := make([]byte, newSize)

		// Copy header bytes before offset
		copy(newBuffer[:offset], p.Buffer[:offset])

		// Set simple encoding flag at offset-1
		newBuffer[offset-1] = 0xa5

		// Copy data after flag (shift by 1 byte)
		copy(newBuffer[offset:], p.Buffer[offset-1:])

		p.Buffer = newBuffer
		p.eq2Compressed = true

		return offset - 1 // Return compression flag position
	}
}

// EncryptPacket encrypts packet data (matches EQStream::EncryptPacket)
func (p *ProtoPacket) EncryptPacket(cipher *crypto.Ciphers, compressOffset int8, offset int8) {
	if cipher == nil || !cipher.IsEncrypted() || len(p.Buffer) <= 2 {
		return
	}

	p.packetEncrypted = true

	if p.eq2Compressed && compressOffset > 0 {
		// Encrypted compressed data from compress offset
		if int(compressOffset) < len(p.Buffer) {
			cipher.Encrypt(p.Buffer[compressOffset:])
		}
	} else {
		// Encrypt uncompressed data from 2 + offset
		startPos := 2 + int(offset)
		if startPos < len(p.Buffer) {
			cipher.Encrypt(p.Buffer[startPos:])
		}
	}
}

// Serialize writes the protocol packet to a destination buffer
func (p *ProtoPacket) Serialize(dest []byte, offset int8) uint32 {
	pos := 0

	// Write compression flag if compressed
	if p.eq2Compressed {
		dest[pos] = 0x5a
		pos++
	}

	// Write opcode (2 bytes)
	binary.BigEndian.PutUint16(dest[pos:], p.Opcode)
	pos += 2

	// Copy packet data after opcode
	if offset < int8(len(p.Buffer)) {
		copy(dest[pos:], p.Buffer[offset:])
		return uint32(len(p.Buffer)-int(offset)) + uint32(pos)
	}

	return uint32(pos)
}

// Combine combines this protocol packet with another (matches EQProtocolPacket::combine)
func (p *ProtoPacket) Combine(rhs *ProtoPacket) bool {
	const opCombined = 0x03 // OP_Combined

	// Case 1: This packet is already combined - append to it
	if p.Opcode == opCombined && p.Size()+rhs.Size()+3 < 256 {
		newSize := len(p.Buffer) + int(rhs.Size()) + 1
		newBuffer := make([]byte, newSize)

		// Copy existing combined data
		copy(newBuffer, p.Buffer)
		offset := len(p.Buffer)

		// Add size prefix for new packet
		newBuffer[offset] = byte(rhs.Size())
		offset++

		// Serialize and append new packet
		tmpBuf := make([]byte, rhs.Size())
		rhs.Serialize(tmpBuf, 0)
		copy(newBuffer[offset:], tmpBuf)

		p.Buffer = newBuffer
		return true
	}

	// Case 2: Neither packet is combined - create new combined packet
	if p.Size()+rhs.Size()+4 < 256 {
		totalSize := int(p.Size()) + int(rhs.Size()) + 2
		newBuffer := make([]byte, totalSize)
		offset := 0

		// Add first packet with size prefix
		newBuffer[offset] = byte(p.Size())
		offset++
		tmpBuf := make([]byte, p.Size())
		p.Serialize(tmpBuf, 0)
		copy(newBuffer[offset:], tmpBuf)
		offset += int(p.Size())

		// Add second packet with size prefix
		newBuffer[offset] = byte(rhs.Size())
		offset++
		tmpBuf = make([]byte, rhs.Size())
		rhs.Serialize(tmpBuf, 0)
		copy(newBuffer[offset:], tmpBuf)

		// Update buffer and mark as combined
		p.Buffer = newBuffer
		p.Opcode = opCombined
		return true
	}

	return false
}

// AppCombine combines app-level packets (matches EQ2Packet::AppCombine)
func (p *ProtoPacket) AppCombine(rhs *ProtoPacket) bool {
	const opAppCombined = 0x19 // OP_AppCombined

	lhsSize := p.Size()
	rhsSize := rhs.Size()

	// Check max combined size
	if lhsSize+rhsSize > MaxCombinedSize {
		return false
	}

	// If already combined, add to it
	if p.Opcode == opAppCombined {
		tmpSize := rhsSize - 2 // Subtract opcode bytes
		var newSize int

		if tmpSize >= 255 {
			newSize = len(p.Buffer) + int(tmpSize) + 3 // oversized header
		} else {
			newSize = len(p.Buffer) + int(tmpSize) + 1 // normal header
		}

		newBuffer := make([]byte, newSize)
		copy(newBuffer, p.Buffer)
		pos := len(p.Buffer)

		// Add size header
		if tmpSize >= 255 {
			newBuffer[pos] = 255 // Oversized marker
			pos++
			binary.BigEndian.PutUint16(newBuffer[pos:], uint16(tmpSize))
			pos += 2
		} else {
			newBuffer[pos] = byte(tmpSize)
			pos++
		}

		// Serialize rhs packet (skip first 2 bytes - opcode)
		if len(rhs.Buffer) > 2 {
			copy(newBuffer[pos:], rhs.Buffer[2:])
		}

		p.Buffer = newBuffer
		return true
	}

	// Create new combined packet
	lSize := lhsSize - 2
	rSize := rhsSize - 2
	totalSize := 0

	if lSize >= 255 {
		totalSize += 3 + int(lSize)
	} else {
		totalSize += 1 + int(lSize)
	}

	if rSize >= 255 {
		totalSize += 3 + int(rSize)
	} else {
		totalSize += 1 + int(rSize)
	}

	if totalSize > MaxCombinedSize {
		return false
	}

	newBuffer := make([]byte, totalSize)
	pos := 0

	// Add first packet
	if lSize >= 255 {
		newBuffer[pos] = 255
		pos++
		binary.BigEndian.PutUint16(newBuffer[pos:], uint16(lSize))
		pos += 2
	} else {
		newBuffer[pos] = byte(lSize)
		pos++
	}
	if len(p.Buffer) > 2 {
		copy(newBuffer[pos:], p.Buffer[2:])
		pos += int(lSize)
	}

	// Add second packet
	if rSize >= 255 {
		newBuffer[pos] = 255
		pos++
		binary.BigEndian.PutUint16(newBuffer[pos:], uint16(rSize))
		pos += 2
	} else {
		newBuffer[pos] = byte(rSize)
		pos++
	}
	if len(rhs.Buffer) > 2 {
		copy(newBuffer[pos:], rhs.Buffer[2:])
	}

	p.Opcode = opAppCombined
	p.Buffer = newBuffer
	p.packetPrepared = false

	return true
}

// MakeApplicationPacket converts to app packet (matches EQProtocolPacket::MakeApplicationPacket)
func (p *ProtoPacket) MakeApplicationPacket(opcodeSize uint8) *AppPacket {
	// Decompress if needed - handle both zlib and simple encoding
	if p.eq2Compressed && len(p.Buffer) > 2 {
		// Check compression type at position 2 (after sequence bytes)
		compressionFlag := byte(0)
		if len(p.Buffer) > 2 {
			compressionFlag = p.Buffer[2]
		}
		
		if compressionFlag == 0x5a {
			// Zlib compression - decompress data after flag
			if len(p.Buffer) > 3 {
				if decompressed, err := Decompress(p.Buffer[3:]); err == nil {
					// Rebuild buffer without compression
					newBuffer := make([]byte, 2+len(decompressed))
					copy(newBuffer[:2], p.Buffer[:2]) // Copy sequence
					copy(newBuffer[2:], decompressed)
					p.Buffer = newBuffer
					p.eq2Compressed = false
				}
			}
		} else if compressionFlag == 0xa5 {
			// Simple encoding - just remove the flag
			if len(p.Buffer) > 3 {
				newBuffer := make([]byte, len(p.Buffer)-1)
				copy(newBuffer[:2], p.Buffer[:2])  // Copy sequence
				copy(newBuffer[2:], p.Buffer[3:])  // Skip flag
				p.Buffer = newBuffer
				p.eq2Compressed = false
			}
		}
	}

	// Decode chat if needed
	if p.packetEncrypted && p.EncodeKey != 0 {
		ChatDecode(p.Buffer, p.EncodeKey)
		p.packetEncrypted = false
	}

	return NewAppPacketFromRaw(p.Buffer, opcodeSize, p.manager)
}

// Copy creates a deep copy
func (p *ProtoPacket) Copy() *ProtoPacket {
	newPacket := &ProtoPacket{
		Packet:            NewPacket(p.Opcode, p.Buffer),
		eq2Compressed:     p.eq2Compressed,
		packetPrepared:    p.packetPrepared,
		packetEncrypted:   p.packetEncrypted,
		acked:             p.acked,
		LoginOp:           p.LoginOp,
		Sequence:          p.Sequence,
		SentTime:          p.SentTime,
		AttemptCount:      p.AttemptCount,
		manager:           p.manager,
		CompressThreshold: p.CompressThreshold,
		EncodeKey:         p.EncodeKey,
	}
	newPacket.Packet.CopyInfo(p.Packet)
	return newPacket
}

// GetOpcodeName returns human-readable name for the opcode (matches C++ EQ2Packet::GetOpcodeName)
func (p *ProtoPacket) GetOpcodeName() string {
	// Use manager to get opcode name if available
	if p.manager != nil {
		if name := p.manager.EmuToName(p.LoginOp); name != "" {
			return name
		}
	}
	
	// Fall back to protocol opcode names
	switch p.Opcode {
	case opcodes.OP_SessionRequest:
		return "OP_SessionRequest"
	case opcodes.OP_SessionResponse:
		return "OP_SessionResponse"
	case opcodes.OP_Combined:
		return "OP_Combined"
	case opcodes.OP_SessionDisconnect:
		return "OP_SessionDisconnect"
	case opcodes.OP_KeepAlive:
		return "OP_KeepAlive"
	case opcodes.OP_SessionStatRequest:
		return "OP_SessionStatRequest"
	case opcodes.OP_SessionStatResponse:
		return "OP_SessionStatResponse"
	case opcodes.OP_Packet:
		return "OP_Packet"
	case opcodes.OP_Fragment:
		return "OP_Fragment"
	case opcodes.OP_Ack:
		return "OP_Ack"
	case opcodes.OP_AppCombined:
		return "OP_AppCombined"
	case opcodes.OP_OutOfOrderAck:
		return "OP_OutOfOrderAck"
	case opcodes.OP_OutOfSession:
		return "OP_OutOfSession"
	default:
		return fmt.Sprintf("Unknown(0x%04X)", p.Opcode)
	}
}
