package channelserver

import (
	"erupe-ce/common/byteframe"
	"erupe-ce/common/stringsupport"
	cfg "erupe-ce/config"
	"erupe-ce/network/mhfpacket"
	"erupe-ce/server/channelserver/compression/deltacomp"
	"erupe-ce/server/channelserver/compression/nullcomp"
	"go.uber.org/zap"
	"io"
	"time"
)

func handleMsgMhfLoadPartner(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfLoadPartner)
	loadCharacterData(s, pkt.AckHandle, "partner", make([]byte, 9))
}

func handleMsgMhfSavePartner(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfSavePartner)
	saveCharacterData(s, pkt.AckHandle, "partner", pkt.RawDataPayload, 65536)
}

func handleMsgMhfLoadLegendDispatch(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfLoadLegendDispatch)
	bf := byteframe.NewByteFrame()
	legendDispatch := []struct {
		Unk       uint32
		Timestamp uint32
	}{
		{0, uint32(TimeMidnight().Add(-12 * time.Hour).Unix())},
		{0, uint32(TimeMidnight().Add(12 * time.Hour).Unix())},
		{0, uint32(TimeMidnight().Add(36 * time.Hour).Unix())},
	}
	bf.WriteUint8(uint8(len(legendDispatch)))
	for _, dispatch := range legendDispatch {
		bf.WriteUint32(dispatch.Unk)
		bf.WriteUint32(dispatch.Timestamp)
	}
	doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}

// Hunter Navi buffer sizes per game version
const (
	hunterNaviSizeG8 = 552 // G8+ navi buffer size
	hunterNaviSizeG7 = 280 // G7 and older navi buffer size
)

func handleMsgMhfLoadHunterNavi(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfLoadHunterNavi)
	naviLength := hunterNaviSizeG8
	if s.server.erupeConfig.RealClientMode <= cfg.G7 {
		naviLength = hunterNaviSizeG7
	}
	loadCharacterData(s, pkt.AckHandle, "hunternavi", make([]byte, naviLength))
}

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

	s.logger.Debug("Hunter Navi 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 {
		naviLength := hunterNaviSizeG8
		if s.server.erupeConfig.RealClientMode <= cfg.G7 {
			naviLength = hunterNaviSizeG7
		}
		// Load existing save
		data, err := s.server.charRepo.LoadColumn(s.charID, "hunternavi")
		if err != nil {
			s.logger.Error("Failed to load hunternavi",
				zap.Error(err),
				zap.Uint32("charID", s.charID),
			)
		}

		// Check if we actually had any hunternavi data, using a blank buffer if not.
		// This is requried as the client will try to send a diff after character creation without a prior MsgMhfSaveHunterNavi packet.
		if len(data) == 0 {
			data = make([]byte, naviLength)
		}

		// Perform diff and compress it to write back to db
		s.logger.Debug("Applying Hunter Navi diff",
			zap.Uint32("charID", s.charID),
			zap.Int("base_size", len(data)),
			zap.Int("diff_size", len(pkt.RawDataPayload)),
		)
		saveOutput := deltacomp.ApplyDataDiff(pkt.RawDataPayload, data)
		dataSize = len(saveOutput)

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

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

	saveDuration := time.Since(saveStart)
	s.logger.Info("Hunter Navi 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 handleMsgMhfMercenaryHuntdata(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfMercenaryHuntdata)
	if pkt.RequestType == 1 {
		// Format:
		// uint8 Hunts
		// struct Hunt
		//   uint32 HuntID
		//   uint32 MonID
		doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
	} else {
		doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0))
	}
}

func handleMsgMhfEnumerateMercenaryLog(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfEnumerateMercenaryLog)
	bf := byteframe.NewByteFrame()
	bf.WriteUint32(0)
	// Format:
	// struct Log
	//   uint32 Timestamp
	//   []byte Name (len 18)
	//   uint8 Unk
	//   uint8 Unk
	doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}

func handleMsgMhfCreateMercenary(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfCreateMercenary)
	nextID, err := s.server.mercenaryRepo.NextRastaID()
	if err != nil {
		s.logger.Error("Failed to get next rasta ID", zap.Error(err))
		doAckSimpleFail(s, pkt.AckHandle, nil)
		return
	}
	if err := s.server.charRepo.SaveInt(s.charID, "rasta_id", int(nextID)); err != nil {
		s.logger.Error("Failed to set rasta ID", zap.Error(err))
		doAckSimpleFail(s, pkt.AckHandle, nil)
		return
	}
	bf := byteframe.NewByteFrame()
	bf.WriteUint32(nextID)
	doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
}

func handleMsgMhfSaveMercenary(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfSaveMercenary)
	if len(pkt.MercData) > 65536 {
		s.logger.Warn("Mercenary payload too large", zap.Int("len", len(pkt.MercData)))
		doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
		return
	}
	dumpSaveData(s, pkt.MercData, "mercenary")
	if len(pkt.MercData) >= 4 {
		temp := byteframe.NewByteFrameFromBytes(pkt.MercData)
		rastaID := temp.ReadUint32()
		if rastaID == 0 {
			s.logger.Warn("Mercenary save with rasta_id=0, preserving existing value",
				zap.Uint32("charID", s.charID))
		}
		if err := s.server.charRepo.SaveMercenary(s.charID, pkt.MercData, rastaID); err != nil {
			s.logger.Error("Failed to save mercenary data", zap.Error(err))
		}
	}
	if err := s.server.charRepo.UpdateGCPAndPact(s.charID, pkt.GCP, pkt.PactMercID); err != nil {
		s.logger.Error("Failed to update GCP and pact ID", zap.Error(err))
	}
	doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}

func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfReadMercenaryW)
	bf := byteframe.NewByteFrame()

	pactID, pactErr := readCharacterInt(s, "pact_id")
	if pactErr != nil {
		s.logger.Warn("Failed to read pact_id", zap.Error(pactErr))
	}
	var cid uint32
	var name string
	if pactID > 0 {
		var findErr error
		cid, name, findErr = s.server.charRepo.FindByRastaID(pactID)
		if findErr != nil {
			s.logger.Warn("Failed to find character by rasta ID", zap.Error(findErr))
		}
		bf.WriteUint8(1) // numLends
		bf.WriteUint32(uint32(pactID))
		bf.WriteUint32(cid)
		bf.WriteBool(true) // Escort enabled
		bf.WriteUint32(uint32(TimeAdjusted().Unix()))
		bf.WriteUint32(uint32(TimeAdjusted().Add(time.Hour * 24 * 7).Unix()))
		bf.WriteBytes(stringsupport.PaddedString(name, 18, true))
	} else {
		bf.WriteUint8(0)
	}

	if pkt.Op != 2 && pkt.Op != 5 {
		loans, err := s.server.mercenaryRepo.GetMercenaryLoans(s.charID)
		if err != nil {
			s.logger.Error("Failed to query mercenary loans", zap.Error(err))
		}
		bf.WriteUint8(uint8(len(loans)))
		for _, loan := range loans {
			bf.WriteUint32(uint32(loan.PactID))
			bf.WriteUint32(loan.CharID)
			bf.WriteUint32(uint32(TimeAdjusted().Unix()))
			bf.WriteUint32(uint32(TimeAdjusted().Add(time.Hour * 24 * 7).Unix()))
			bf.WriteBytes(stringsupport.PaddedString(loan.Name, 18, true))
		}

		if pkt.Op != 1 && pkt.Op != 4 {
			data, dataErr := s.server.charRepo.LoadColumn(s.charID, "savemercenary")
			if dataErr != nil {
				s.logger.Warn("Failed to load savemercenary", zap.Error(dataErr))
			}
			gcp, gcpErr := readCharacterInt(s, "gcp")
			if gcpErr != nil {
				s.logger.Warn("Failed to read gcp", zap.Error(gcpErr))
			}

			if len(data) == 0 {
				bf.WriteBool(false)
			} else {
				bf.WriteBool(true)
				bf.WriteBytes(data)
			}
			bf.WriteUint32(uint32(gcp))
		}
	}

	doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}

func handleMsgMhfReadMercenaryM(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfReadMercenaryM)
	data, err := s.server.charRepo.LoadColumn(pkt.CharID, "savemercenary")
	if err != nil {
		s.logger.Warn("Failed to load savemercenary for other character", zap.Error(err))
	}
	resp := byteframe.NewByteFrame()
	if len(data) == 0 {
		resp.WriteBool(false)
	} else {
		resp.WriteBytes(data)
	}
	doAckBufSucceed(s, pkt.AckHandle, resp.Data())
}

func handleMsgMhfContractMercenary(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfContractMercenary)
	switch pkt.Op {
	case 0: // Form loan
		if err := s.server.charRepo.SaveInt(pkt.CID, "pact_id", int(pkt.PactMercID)); err != nil {
			s.logger.Error("Failed to form mercenary loan", zap.Error(err))
		}
	case 1: // Cancel lend
		if err := s.server.charRepo.SaveInt(s.charID, "pact_id", 0); err != nil {
			s.logger.Error("Failed to cancel mercenary lend", zap.Error(err))
		}
	case 2: // Cancel loan
		if err := s.server.charRepo.SaveInt(pkt.CID, "pact_id", 0); err != nil {
			s.logger.Error("Failed to cancel mercenary loan", zap.Error(err))
		}
	}
	doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}

func handleMsgMhfLoadOtomoAirou(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfLoadOtomoAirou)
	loadCharacterData(s, pkt.AckHandle, "otomoairou", make([]byte, 10))
}

func handleMsgMhfSaveOtomoAirou(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfSaveOtomoAirou)
	if len(pkt.RawDataPayload) < 2 {
		doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
		return
	}
	dumpSaveData(s, pkt.RawDataPayload, "otomoairou")
	decomp, err := nullcomp.Decompress(pkt.RawDataPayload[1:])
	if err != nil {
		s.logger.Error("Failed to decompress airou", zap.Error(err))
		doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
		return
	}
	bf := byteframe.NewByteFrameFromBytes(decomp)
	save := byteframe.NewByteFrame()
	var catsExist uint8
	save.WriteUint8(0)

	cats := bf.ReadUint8()
	for i := 0; i < int(cats); i++ {
		dataLen := bf.ReadUint32()
		catID := bf.ReadUint32()
		if catID == 0 {
			catID, err = s.server.mercenaryRepo.NextAirouID()
			if err != nil {
				s.logger.Error("Failed to get next airou ID", zap.Error(err))
				doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
				return
			}
		}
		exists := bf.ReadBool()
		data := bf.ReadBytes(uint(dataLen) - 5)
		if exists {
			catsExist++
			save.WriteUint32(dataLen)
			save.WriteUint32(catID)
			save.WriteBool(exists)
			save.WriteBytes(data)
		}
	}
	save.WriteBytes(bf.DataFromCurrent())
	_, _ = save.Seek(0, 0)
	save.WriteUint8(catsExist)
	comp, err := nullcomp.Compress(save.Data())
	if err != nil {
		s.logger.Error("Failed to compress airou", zap.Error(err))
	} else {
		comp = append([]byte{0x01}, comp...)
		if err := s.server.charRepo.SaveColumn(s.charID, "otomoairou", comp); err != nil {
			s.logger.Error("Failed to save otomoairou", zap.Error(err))
		}
	}
	doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}

func handleMsgMhfEnumerateAiroulist(s *Session, p mhfpacket.MHFPacket) {
	pkt := p.(*mhfpacket.MsgMhfEnumerateAiroulist)
	resp := byteframe.NewByteFrame()
	airouList := getGuildAirouList(s)
	resp.WriteUint16(uint16(len(airouList)))
	resp.WriteUint16(uint16(len(airouList)))
	for _, cat := range airouList {
		resp.WriteUint32(cat.ID)
		resp.WriteBytes(cat.Name)
		resp.WriteUint32(cat.Experience)
		resp.WriteUint8(cat.Personality)
		resp.WriteUint8(cat.Class)
		resp.WriteUint8(cat.WeaponType)
		resp.WriteUint16(cat.WeaponID)
		resp.WriteUint32(0) // 32 bit unix timestamp, either time at which the cat stops being fatigued or the time at which it started
	}
	doAckBufSucceed(s, pkt.AckHandle, resp.Data())
}

// Airou represents Airou (felyne companion) data.
type Airou struct {
	ID          uint32
	Name        []byte
	Task        uint8
	Personality uint8
	Class       uint8
	Experience  uint32
	WeaponType  uint8
	WeaponID    uint16
}

func getGuildAirouList(s *Session) []Airou {
	var guildCats []Airou
	bannedCats := make(map[uint32]int)
	guild, err := s.server.guildRepo.GetByCharID(s.charID)
	if err != nil {
		return guildCats
	}
	usages, err := s.server.mercenaryRepo.GetGuildHuntCatsUsed(s.charID)
	if err != nil {
		s.logger.Warn("Failed to get recently used airous", zap.Error(err))
		return guildCats
	}

	for _, usage := range usages {
		if usage.Start.Add(time.Second * time.Duration(s.server.erupeConfig.GameplayOptions.TreasureHuntPartnyaCooldown)).Before(TimeAdjusted()) {
			for i, j := range stringsupport.CSVElems(usage.CatsUsed) {
				bannedCats[uint32(j)] = i
			}
		}
	}

	airouData, err := s.server.mercenaryRepo.GetGuildAirou(guild.ID)
	if err != nil {
		s.logger.Warn("Selecting otomoairou based on guild failed", zap.Error(err))
		return guildCats
	}

	for _, data := range airouData {
		if len(data) == 0 {
			continue
		}
		// first byte has cat existence in general, can skip if 0
		if data[0] == 1 {
			decomp, err := nullcomp.Decompress(data[1:])
			if err != nil {
				s.logger.Warn("decomp failure", zap.Error(err))
				continue
			}
			bf := byteframe.NewByteFrameFromBytes(decomp)
			cats := GetAirouDetails(bf)
			for _, cat := range cats {
				_, exists := bannedCats[cat.ID]
				if cat.Task == 4 && !exists {
					guildCats = append(guildCats, cat)
				}
			}
		}
	}
	return guildCats
}

// GetAirouDetails parses Airou data from a ByteFrame.
func GetAirouDetails(bf *byteframe.ByteFrame) []Airou {
	catCount := bf.ReadUint8()
	cats := make([]Airou, catCount)
	for x := 0; x < int(catCount); x++ {
		var catDef Airou
		// cat sometimes has additional bytes for whatever reason, gift items? timestamp?
		// until actual variance is known we can just seek to end based on start
		catDefLen := bf.ReadUint32()
		catStart, _ := bf.Seek(0, io.SeekCurrent)

		catDef.ID = bf.ReadUint32()
		_, _ = bf.Seek(1, io.SeekCurrent) // unknown value, probably a bool
		catDef.Name = bf.ReadBytes(18)    // always 18 len, reads first null terminated string out of section and discards rest
		catDef.Task = bf.ReadUint8()
		_, _ = bf.Seek(16, io.SeekCurrent) // appearance data and what is seemingly null bytes
		catDef.Personality = bf.ReadUint8()
		catDef.Class = bf.ReadUint8()
		_, _ = bf.Seek(5, io.SeekCurrent)   // affection and colour sliders
		catDef.Experience = bf.ReadUint32() // raw cat rank points, doesn't have a rank
		_, _ = bf.Seek(1, io.SeekCurrent)   // bool for weapon being equipped
		catDef.WeaponType = bf.ReadUint8()  // weapon type, presumably always 6 for melee?
		catDef.WeaponID = bf.ReadUint16()   // weapon id
		_, _ = bf.Seek(catStart+int64(catDefLen), io.SeekStart)
		cats[x] = catDef
	}
	return cats
}
