package emu.grasscutter.game.managers.forging;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.excels.ForgeData;
import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.player.BasePlayerManager;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.net.proto.ForgeQueueDataOuterClass.ForgeQueueData;
import emu.grasscutter.net.proto.ForgeQueueManipulateReqOuterClass.ForgeQueueManipulateReq;
import emu.grasscutter.net.proto.ForgeQueueManipulateTypeOuterClass.ForgeQueueManipulateType;
import emu.grasscutter.net.proto.ForgeStartReqOuterClass.ForgeStartReq;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.server.packet.send.PacketForgeDataNotify;
import emu.grasscutter.server.packet.send.PacketForgeFormulaDataNotify;
import emu.grasscutter.server.packet.send.PacketForgeGetQueueDataRsp;
import emu.grasscutter.server.packet.send.PacketForgeQueueDataNotify;
import emu.grasscutter.server.packet.send.PacketForgeQueueManipulateRsp;
import emu.grasscutter.server.packet.send.PacketForgeStartRsp;
import emu.grasscutter.utils.Utils;

public class ForgingManager extends BasePlayerManager {

    public ForgingManager(Player player) {
        super(player);
    }

    /**********
        Blueprint unlocking.
    **********/
    public boolean unlockForgingBlueprint(int id) {
        // Tell the client that this blueprint is now unlocked and add the unlocked item to the player.
        if (!this.player.getUnlockedForgingBlueprints().add(id)) {
            return false;  // Already unlocked
        }
        this.player.sendPacket(new PacketForgeFormulaDataNotify(id));
        return true;
    }

    /**********
        Communicate forging information to the client.
    **********/
    private synchronized int determineNumberOfQueues() {
        int adventureRank = player.getLevel();
        return
            (adventureRank >= 15) ? 4
            : (adventureRank >= 10) ? 3
            : (adventureRank >= 5) ? 2
            : 1;
    }

    private synchronized Map<Integer, ForgeQueueData> determineCurrentForgeQueueData() {
        Map<Integer, ForgeQueueData> res = new HashMap<>();
        int currentTime = Utils.getCurrentSeconds();

        // Create queue information for all active forges.
        for (int i = 0; i < this.player.getActiveForges().size(); i++) {
            ActiveForgeData activeForge = this.player.getActiveForges().get(i);

            ForgeQueueData data = ForgeQueueData.newBuilder()
                .setQueueId(i + 1)
                .setForgeId(activeForge.getForgeId())
                .setFinishCount(activeForge.getFinishedCount(currentTime))
                .setUnfinishCount(activeForge.getUnfinishedCount(currentTime))
                .setTotalFinishTimestamp(activeForge.getTotalFinishTimestamp())
                .setNextFinishTimestamp(activeForge.getNextFinishTimestamp(currentTime))
                .setAvatarId(activeForge.getAvatarId())
                .build();

            res.put(i + 1, data);
        }

        return res;
    }

    public synchronized void sendForgeDataNotify() {
        // Determine the number of queues and unlocked items.
        int numQueues = this.determineNumberOfQueues();
        var unlockedItems = this.player.getUnlockedForgingBlueprints();
        var queueData = this.determineCurrentForgeQueueData();

        // Send notification.
        this.player.sendPacket(new PacketForgeDataNotify(unlockedItems, numQueues, queueData));
    }

    public synchronized void handleForgeGetQueueDataReq() {
        // Determine the number of queues.
        int numQueues = this.determineNumberOfQueues();
        var queueData = this.determineCurrentForgeQueueData();

        // Reply.
        this.player.sendPacket(new PacketForgeGetQueueDataRsp(Retcode.RET_SUCC, numQueues, queueData));
    }

    /**********
        Initiate forging process.
    **********/
    private synchronized void sendForgeQueueDataNotify() {
        var queueData = this.determineCurrentForgeQueueData();
        this.player.sendPacket(new PacketForgeQueueDataNotify(queueData, List.of()));
    }
    private synchronized void sendForgeQueueDataNotify(boolean hasRemoved) {
        var queueData = this.determineCurrentForgeQueueData();

        if (hasRemoved) {
            this.player.sendPacket(new PacketForgeQueueDataNotify(Map.of(), List.of(1, 2, 3, 4)));
        }

        this.player.sendPacket(new PacketForgeQueueDataNotify(queueData, List.of()));
    }

    public synchronized void handleForgeStartReq(ForgeStartReq req) {
        // Refuse if all queues are already full.
        if (this.player.getActiveForges().size() >= this.determineNumberOfQueues()) {
            this.player.sendPacket(new PacketForgeStartRsp(Retcode.RET_FORGE_QUEUE_FULL));
            return;
        }

        // Get the required forging information for the target item.
        if (!GameData.getForgeDataMap().containsKey(req.getForgeId())) {
            this.player.sendPacket(new PacketForgeStartRsp(Retcode.RET_FAIL)); //ToDo: Probably the wrong return code.
            return;
        }

        ForgeData forgeData = GameData.getForgeDataMap().get(req.getForgeId());

        //Check if the player has sufficient forge points.
        int requiredPoints = forgeData.getForgePoint() * req.getForgeCount();
        if (requiredPoints > this.player.getForgePoints()) {
            this.player.sendPacket(new PacketForgeStartRsp(Retcode.RET_FORGE_POINT_NOT_ENOUGH));
            return;
        }

        // Check if we have enough of each material and consume.
        List<ItemParamData> material = new ArrayList<>(forgeData.getMaterialItems());
        material.add(new ItemParamData(202, forgeData.getScoinCost()));

        boolean success = player.getInventory().payItems(material, req.getForgeCount(), ActionReason.ForgeCost);

        if (!success) {
            //TODO:I'm not sure this one is correct.
            this.player.sendPacket(new PacketForgeStartRsp(Retcode.RET_ITEM_COUNT_NOT_ENOUGH)); //ToDo: Probably the wrong return code.
        }

        // Consume forge points.
        this.player.setForgePoints(this.player.getForgePoints() - requiredPoints);

        // Create and add active forge.
        ActiveForgeData activeForge = new ActiveForgeData();
        activeForge.setForgeId(req.getForgeId());
        activeForge.setAvatarId(req.getAvatarId());
        activeForge.setCount(req.getForgeCount());
        activeForge.setStartTime(Utils.getCurrentSeconds());
        activeForge.setForgeTime(forgeData.getForgeTime());

        this.player.getActiveForges().add(activeForge);

        // Done.
        this.sendForgeQueueDataNotify();
        this.player.sendPacket(new PacketForgeStartRsp(Retcode.RET_SUCC));
    }

    /**********
        Forge queue manipulation (obtaining results and cancelling forges).
    **********/
    private synchronized void obtainItems(int queueId) {
        // Determine how many items are finished.
        int currentTime = Utils.getCurrentSeconds();
        ActiveForgeData forge = this.player.getActiveForges().get(queueId - 1);

        int finished = forge.getFinishedCount(currentTime);
        int unfinished = forge.getUnfinishedCount(currentTime);

        // Sanity check: Are any items finished?
        if (finished <= 0) {
            return;
        }

        // Give finished items to the player.
        ForgeData data = GameData.getForgeDataMap().get(forge.getForgeId());

        int resultId = data.getResultItemId() > 0 ? data.getResultItemId() : data.getShowItemId();
        ItemData resultItemData = GameData.getItemDataMap().get(resultId);
        GameItem addItem = new GameItem(resultItemData, data.getResultItemCount() * finished);
        this.player.getInventory().addItem(addItem);

        // Battle pass trigger handler
        this.player.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_DO_FORGE, 0, finished);

        // Replace active forge with a new one for the unfinished items, if there are any.
        if (unfinished > 0) {
            ActiveForgeData remainingForge = new ActiveForgeData();

            remainingForge.setForgeId(forge.getForgeId());
            remainingForge.setAvatarId(forge.getAvatarId());
            remainingForge.setCount(unfinished);
            remainingForge.setForgeTime(forge.getForgeTime());
            remainingForge.setStartTime(forge.getStartTime() + finished * forge.getForgeTime());

            this.player.getActiveForges().set(queueId - 1, remainingForge);
            this.sendForgeQueueDataNotify();
        }
        // Otherwise, completely remove it.
        else {
            this.player.getActiveForges().remove(queueId - 1);
            // this.sendForgeQueueDataNotify(queueId);
            this.sendForgeQueueDataNotify(true);
        }

        // Send response.
        this.player.sendPacket(new PacketForgeQueueManipulateRsp(Retcode.RET_SUCC, ForgeQueueManipulateType.FORGE_QUEUE_MANIPULATE_TYPE_RECEIVE_OUTPUT, List.of(addItem), List.of(), List.of()));
    }

    private synchronized void cancelForge(int queueId) {
        // Make sure there are no unfinished items.
        int currentTime = Utils.getCurrentSeconds();
        ActiveForgeData forge = this.player.getActiveForges().get(queueId - 1);

        if (forge.getFinishedCount(currentTime) > 0) {
            return;
        }

        // Return material items to the player.
        ForgeData data = GameData.getForgeDataMap().get(forge.getForgeId());

        var returnItems = new ArrayList<GameItem>();
        for (var material : data.getMaterialItems()) {
            if (material.getItemId() == 0) {
                continue;
            }

            ItemData resultItemData = GameData.getItemDataMap().get(material.getItemId());
            GameItem returnItem = new GameItem(resultItemData, material.getItemCount() * forge.getCount());

            this.player.getInventory().addItem(returnItem);
            returnItems.add(returnItem);
        }

        // Return Mora to the player.
        this.player.setMora(this.player.getMora() + data.getScoinCost() * forge.getCount());

        ItemData moraItem = GameData.getItemDataMap().get(202);
        GameItem returnMora = new GameItem(moraItem, data.getScoinCost() * forge.getCount());
        returnItems.add(returnMora);

        // Return forge points to the player.
        int requiredPoints = data.getForgePoint() * forge.getCount();
        int newPoints = Math.min(this.player.getForgePoints() + requiredPoints, 300_000);

        this.player.setForgePoints(newPoints);

        // Remove the forge queue.
        this.player.getActiveForges().remove(queueId - 1);
        this.sendForgeQueueDataNotify(true);

        // Send response.
        this.player.sendPacket(new PacketForgeQueueManipulateRsp(Retcode.RET_SUCC, ForgeQueueManipulateType.FORGE_QUEUE_MANIPULATE_TYPE_STOP_FORGE, List.of(), returnItems, List.of()));
    }

    public synchronized void handleForgeQueueManipulateReq(ForgeQueueManipulateReq req) {
        // Get info from the request.
        int queueId = req.getForgeQueueId();
        var manipulateType = req.getManipulateType();

        // Handle according to the manipulation type.
        switch (manipulateType) {
            case FORGE_QUEUE_MANIPULATE_TYPE_RECEIVE_OUTPUT -> this.obtainItems(queueId);
            case FORGE_QUEUE_MANIPULATE_TYPE_STOP_FORGE -> this.cancelForge(queueId);
            default -> {
            } //Should never happen.
        }
    }

    /**********
        Periodic forging updates.
    **********/
    public synchronized void sendPlayerForgingUpdate() {
        int currentTime = Utils.getCurrentSeconds();

        // Determine if sending an update is necessary.
        // We only send an update if there are forges in the forge queue
        // that have changed since the last notification.
        if (this.player.getActiveForges().size() <= 0) {
            return;
        }

        boolean hasChanges = this.player.getActiveForges().stream()
                                    .anyMatch(forge -> forge.updateChanged(currentTime));

        if (!hasChanges) {
            return;
        }

        // Send notification.
        this.sendForgeQueueDataNotify();

        // Reset changed flags.
        this.player.getActiveForges().stream()
            .forEach(forge -> forge.setChanged(false));
    }
}
