// Package channelserver implements plate data (transmog) management.
//
// Plate Data Overview:
// - platedata: Main transmog appearance data (~140KB, compressed)
// - platebox: Plate storage/inventory (~4.8KB, compressed)
// - platemyset: Equipment set configurations (1920 bytes, uncompressed)
//
// Save Strategy:
// All plate data saves immediately when the client sends save packets.
// This differs from the main savedata which may use session caching.
// The logout flow includes a safety check via savePlateDataToDatabase()
// to ensure no data loss if packets are lost or client disconnects.
//
// Cache Management:
// When plate data is saved, the server's user binary cache (types 2-3)
// is invalidated to ensure other players see updated appearance immediately.
// This prevents stale transmog/armor being displayed after zone changes.
//
// Thread Safety:
// All handlers use session-scoped database operations, making them
// inherently thread-safe as each session is single-threaded.
package channelserver

import (
	"erupe-ce/network/mhfpacket"
	"erupe-ce/server/channelserver/compression/deltacomp"
	"erupe-ce/server/channelserver/compression/nullcomp"
	"go.uber.org/zap"
	"time"
)

func handleMsgMhfLoadPlateData(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfLoadPlateData)
	loadCharacterData(s, pkt.AckHandle, "platedata", nil)
}

// Plate data size constants
const (
	plateDataMaxPayload  = 262144 // max compressed platedata size
	plateDataEmptySize   = 140000 // empty platedata buffer
	plateBoxMaxPayload   = 32768  // max compressed platebox size
	plateBoxEmptySize    = 4800   // empty platebox buffer
	plateMysetDefaultLen = 1920   // default platemyset buffer
	plateMysetMaxPayload = 4096   // max platemyset payload size
)

func handleMsgMhfSavePlateData(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfSavePlateData)
	if len(pkt.RawDataPayload) > plateDataMaxPayload {
		s.logger.Warn("PlateData payload too large", zap.Int("len", len(pkt.RawDataPayload)))
		doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
		return
	}
	saveStart := time.Now()

	s.logger.Debug("PlateData save request",
		zap.Uint32("charID", s.charID),
		zap.Bool("is_diff", pkt.IsDataDiff),
		zap.Int("data_size", len(pkt.RawDataPayload)),
	)

	var dataSize int
	if pkt.IsDataDiff {
		var data []byte

		// Load existing save
		data, err := s.server.charRepo.LoadColumn(s.charID, "platedata")
		if err != nil {
			s.logger.Error("Failed to load platedata",
				zap.Error(err),
				zap.Uint32("charID", s.charID),
			)
			doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
			return
		}

		if len(data) > 0 {
			// Decompress
			s.logger.Debug("Decompressing PlateData", zap.Int("compressed_size", len(data)))
			data, err = nullcomp.DecompressWithLimit(data, plateDataMaxPayload)
			if err != nil {
				s.logger.Error("Failed to decompress platedata",
					zap.Error(err),
					zap.Uint32("charID", s.charID),
				)
				doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
				return
			}
		} else {
			// create empty save if absent
			data = make([]byte, plateDataEmptySize)
		}

		// Perform diff with bounds checking and compress it to write back to db
		s.logger.Debug("Applying PlateData diff", zap.Int("base_size", len(data)))
		patched, err := deltacomp.ApplyDataDiffWithLimit(pkt.RawDataPayload, data, plateDataMaxPayload)
		if err != nil {
			s.logger.Error("Failed to apply platedata diff",
				zap.Error(err),
				zap.Uint32("charID", s.charID),
			)
			doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
			return
		}
		saveOutput, err := nullcomp.Compress(patched)
		if err != nil {
			s.logger.Error("Failed to diff and compress platedata",
				zap.Error(err),
				zap.Uint32("charID", s.charID),
			)
			doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
			return
		}
		dataSize = len(saveOutput)

		err = s.server.charRepo.SaveColumn(s.charID, "platedata", saveOutput)
		if err != nil {
			s.logger.Error("Failed to save platedata",
				zap.Error(err),
				zap.Uint32("charID", s.charID),
			)
			doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
			return
		}
	} else {
		dumpSaveData(s, pkt.RawDataPayload, "platedata")
		dataSize = len(pkt.RawDataPayload)

		// simply update database, no extra processing
		err := s.server.charRepo.SaveColumn(s.charID, "platedata", pkt.RawDataPayload)
		if err != nil {
			s.logger.Error("Failed to save platedata",
				zap.Error(err),
				zap.Uint32("charID", s.charID),
			)
			doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
			return
		}
	}

	// Invalidate user binary cache so other players see updated appearance
	// User binary types 2 and 3 contain equipment/appearance data
	s.server.userBinary.Delete(s.charID, 2)
	s.server.userBinary.Delete(s.charID, 3)

	saveDuration := time.Since(saveStart)
	s.logger.Info("PlateData saved successfully",
		zap.Uint32("charID", s.charID),
		zap.Bool("was_diff", pkt.IsDataDiff),
		zap.Int("data_size", dataSize),
		zap.Duration("duration", saveDuration),
	)

	doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}

func handleMsgMhfLoadPlateBox(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfLoadPlateBox)
	loadCharacterData(s, pkt.AckHandle, "platebox", nil)
}

func handleMsgMhfSavePlateBox(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfSavePlateBox)
	if len(pkt.RawDataPayload) > plateBoxMaxPayload {
		s.logger.Warn("PlateBox payload too large", zap.Int("len", len(pkt.RawDataPayload)))
		doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
		return
	}

	if pkt.IsDataDiff {
		var data []byte

		// Load existing save
		data, err := s.server.charRepo.LoadColumn(s.charID, "platebox")
		if err != nil {
			s.logger.Error("Failed to load platebox", zap.Error(err))
			doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
			return
		}

		// Decompress
		if len(data) > 0 {
			// Decompress
			s.logger.Info("Decompressing...")
			data, err = nullcomp.DecompressWithLimit(data, plateBoxMaxPayload)
			if err != nil {
				s.logger.Error("Failed to decompress platebox", zap.Error(err))
				doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
				return
			}
		} else {
			// create empty save if absent
			data = make([]byte, plateBoxEmptySize)
		}

		// Perform diff with bounds checking and compress it to write back to db
		s.logger.Info("Diffing...")
		patched, err := deltacomp.ApplyDataDiffWithLimit(pkt.RawDataPayload, data, plateBoxMaxPayload)
		if err != nil {
			s.logger.Error("Failed to apply platebox diff", zap.Error(err))
			doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
			return
		}
		saveOutput, err := nullcomp.Compress(patched)
		if err != nil {
			s.logger.Error("Failed to diff and compress platebox", zap.Error(err))
			doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
			return
		}

		err = s.server.charRepo.SaveColumn(s.charID, "platebox", saveOutput)
		if err != nil {
			s.logger.Error("Failed to save platebox", zap.Error(err))
			doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
			return
		}

		s.logger.Info("Wrote recompressed platebox back to DB")
	} else {
		dumpSaveData(s, pkt.RawDataPayload, "platebox")
		// simply update database, no extra processing
		err := s.server.charRepo.SaveColumn(s.charID, "platebox", pkt.RawDataPayload)
		if err != nil {
			s.logger.Error("Failed to save platebox", zap.Error(err))
		}
	}

	// Invalidate user binary cache so other players see updated appearance
	s.server.userBinary.Delete(s.charID, 2)
	s.server.userBinary.Delete(s.charID, 3)

	doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}

func handleMsgMhfLoadPlateMyset(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfLoadPlateMyset)
	loadCharacterData(s, pkt.AckHandle, "platemyset", make([]byte, plateMysetDefaultLen))
}

func handleMsgMhfSavePlateMyset(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfSavePlateMyset)
	if len(pkt.RawDataPayload) > plateMysetMaxPayload {
		s.logger.Warn("PlateMyset payload too large", zap.Int("len", len(pkt.RawDataPayload)))
		doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
		return
	}
	saveStart := time.Now()

	s.logger.Debug("PlateMyset save request",
		zap.Uint32("charID", s.charID),
		zap.Int("data_size", len(pkt.RawDataPayload)),
	)

	// looks to always return the full thing, simply update database, no extra processing
	dumpSaveData(s, pkt.RawDataPayload, "platemyset")
	err := s.server.charRepo.SaveColumn(s.charID, "platemyset", pkt.RawDataPayload)
	if err != nil {
		s.logger.Error("Failed to save platemyset",
			zap.Error(err),
			zap.Uint32("charID", s.charID),
		)
	} else {
		saveDuration := time.Since(saveStart)
		s.logger.Info("PlateMyset saved successfully",
			zap.Uint32("charID", s.charID),
			zap.Int("data_size", len(pkt.RawDataPayload)),
			zap.Duration("duration", saveDuration),
		)
	}

	// Invalidate user binary cache so other players see updated appearance
	s.server.userBinary.Delete(s.charID, 2)
	s.server.userBinary.Delete(s.charID, 3)

	doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}

// savePlateDataToDatabase saves all plate-related data for a character to the database.
// This is called during logout as a safety net to ensure plate data persistence.
//
// Note: Plate data (platedata, platebox, platemyset) saves immediately when the client
// sends save packets via handleMsgMhfSavePlateData, handleMsgMhfSavePlateBox, and
// handleMsgMhfSavePlateMyset. Unlike other data types that use session-level caching,
// plate data does not require re-saving at logout since it's already persisted.
//
// This function exists as:
// 1. A defensive safety net matching the pattern used for other auxiliary data
// 2. A hook for future enhancements if session-level caching is added
// 3. A monitoring point for debugging plate data persistence issues
//
// Returns nil as plate data is already saved by the individual handlers.
func savePlateDataToDatabase(s *Session) error {
	saveStart := time.Now()

	// Since plate data is not cached in session and saves immediately when
	// packets arrive, we don't need to perform any database operations here.
	// The individual save handlers have already persisted the data.
	//
	// This function provides a logging checkpoint to verify the save flow
	// and maintains consistency with the defensive programming pattern used
	// for other data types like warehouse and hunter navi.

	s.logger.Debug("Plate data save check at logout",
		zap.Uint32("charID", s.charID),
		zap.Duration("check_duration", time.Since(saveStart)),
	)

	return nil
}
