package fr.bglands.client.fabric;

import fr.bglands.client.protocol.ClientHello;
import fr.bglands.client.protocol.HelloAck;
import fr.bglands.client.protocol.OpenScreen;
import fr.bglands.client.protocol.OpenForm;
import fr.bglands.client.protocol.ProtocolConstants;
import fr.bglands.client.protocol.ProtocolException;
import fr.bglands.client.protocol.ServerMessage;
import fr.bglands.client.protocol.ServerProbe;
import fr.bglands.client.protocol.UiAction;
import fr.bglands.client.protocol.UiControl;
import fr.bglands.client.protocol.UiProtocolCodec;
import fr.bglands.client.protocol.UiResult;
import fr.bglands.client.protocol.UiSubmit;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.minecraft.client.Minecraft;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

final class BgLandsConnection {
    private final UiProtocolCodec codec = new UiProtocolCodec();
    private final String modVersion;
    private final Map<String, Integer> revisions = new HashMap<>();

    private boolean connected;
    private boolean compatible;

    BgLandsConnection(String modVersion) {
        this.modVersion = modVersion;
    }

    void join(PacketSender sender) {
        connected = true;
        compatible = false;
        revisions.clear();

        if (!ClientPlayNetworking.canSend(BgLandsRawPayload.TYPE)) {
            BgLandsClient.LOGGER.info("Server has no BG Lands UI channel; native client UI stays disabled");
            return;
        }
        sendHello(sender);
    }

    void disconnect(Minecraft client) {
        connected = false;
        compatible = false;
        revisions.clear();
        if (client.gui.screen() instanceof BgLandsScreen || client.gui.screen() instanceof BgLandsFormScreen) {
            client.gui.setScreen(null);
        }
    }

    void receive(Minecraft client, byte[] bytes) {
        final ServerMessage message;
        try {
            message = codec.decodeServer(bytes);
        } catch (ProtocolException exception) {
            BgLandsClient.LOGGER.warn("Ignored invalid BG Lands UI payload: {}", exception.getMessage());
            return;
        }

        if (message instanceof ServerProbe probe) {
            handleProbe(client, probe);
        } else if (message instanceof HelloAck ack) {
            handleHelloAck(client, ack);
        } else if (message instanceof OpenScreen open) {
            handleOpen(client, open);
        } else if (message instanceof OpenForm form) {
            handleOpenForm(client, form);
        } else if (message instanceof UiResult result) {
            handleResult(client, result);
        }
    }

    Optional<String> activate(OpenScreen screen, UiControl control) {
        if (!connected || !compatible || !isCurrentControl(screen, control)) {
            return Optional.empty();
        }

        UiAction action;
        try {
            action = UiAction.create(screen.instanceId(), control.id(), control.token());
            ClientPlayNetworking.send(new BgLandsRawPayload(codec.encode(action)));
        } catch (RuntimeException exception) {
            BgLandsClient.LOGGER.warn("Could not send BG Lands UI action {}", control.id(), exception);
            return Optional.empty();
        }
        return Optional.of(action.requestId());
    }

    Optional<String> submit(OpenForm form, Map<String, String> values) {
        if (!connected || !compatible) {
            return Optional.empty();
        }
        UiSubmit submit;
        try {
            submit = UiSubmit.create(form.instanceId(), form.submitToken(), values);
            ClientPlayNetworking.send(new BgLandsRawPayload(codec.encode(submit)));
        } catch (RuntimeException exception) {
            BgLandsClient.LOGGER.warn("Could not submit BG Lands UI form {}", form.route(), exception);
            return Optional.empty();
        }
        return Optional.of(submit.requestId());
    }

    private void handleProbe(Minecraft client, ServerProbe probe) {
        if (!connected) {
            return;
        }
        compatible = false;
        revisions.clear();
        if (client.gui.screen() instanceof BgLandsScreen || client.gui.screen() instanceof BgLandsFormScreen) {
            client.gui.setScreen(null);
        }
        if (!ProtocolConstants.supports(probe.protocolMajor(), probe.protocolMinor())) {
            BgLandsClient.LOGGER.info(
                    "Backend {} probes unsupported protocol {}.{}; replying with our capabilities",
                    probe.serverVersion(), probe.protocolMajor(), probe.protocolMinor()
            );
        }
        try {
            sendHello(ClientPlayNetworking.getSender());
        } catch (RuntimeException exception) {
            compatible = false;
            BgLandsClient.LOGGER.warn("Could not answer BG Lands backend probe; using server fallback", exception);
        }
    }

    private void handleHelloAck(Minecraft client, HelloAck ack) {
        compatible = ack.accepted() && ProtocolConstants.supports(ack.protocolMajor(), ack.protocolMinor());
        if (!compatible) {
            BgLandsClient.LOGGER.warn(
                    "Server BG Lands UI protocol {}.{} is incompatible with client {}.{}; using server fallback",
                    ack.protocolMajor(), ack.protocolMinor(),
                    ProtocolConstants.PROTOCOL_MAJOR, ProtocolConstants.PROTOCOL_MINOR
            );
            if (!ack.reason().isBlank()) {
                BgLandsClient.LOGGER.warn("Server UI fallback reason: {}", ack.reason());
            }
            if (client.gui.screen() instanceof BgLandsScreen || client.gui.screen() instanceof BgLandsFormScreen) {
                client.gui.setScreen(null);
            }
            return;
        }
        BgLandsClient.LOGGER.info("BG Lands UI handshake accepted by server {}", ack.serverVersion());
    }

    private void handleOpen(Minecraft client, OpenScreen open) {
        if (!compatible || open.protocolMajor() != ProtocolConstants.PROTOCOL_MAJOR) {
            BgLandsClient.LOGGER.debug("Ignored UI open before a compatible handshake");
            return;
        }

        Integer knownRevision = revisions.get(open.instanceId());
        if (knownRevision != null && open.revision() < knownRevision) {
            BgLandsClient.LOGGER.debug("Ignored stale UI revision {} < {}", open.revision(), knownRevision);
            return;
        }
        revisions.put(open.instanceId(), open.revision());
        if (revisions.size() > 128) {
            revisions.remove(revisions.keySet().iterator().next());
        }
        client.gui.setScreen(new BgLandsScreen(open, this));
    }

    private void handleOpenForm(Minecraft client, OpenForm form) {
        if (!compatible || form.protocolMajor() != ProtocolConstants.PROTOCOL_MAJOR) {
            BgLandsClient.LOGGER.debug("Ignored form open before a compatible handshake");
            return;
        }
        client.gui.setScreen(new BgLandsFormScreen(form, this));
    }

    private void handleResult(Minecraft client, UiResult result) {
        if (client.gui.screen() instanceof BgLandsScreen screen) {
            screen.acceptResult(result);
        } else if (client.gui.screen() instanceof BgLandsFormScreen form) {
            form.acceptResult(result);
        }
    }

    private void sendHello(PacketSender sender) {
        try {
            sender.sendPacket(new BgLandsRawPayload(codec.encode(ClientHello.current(modVersion))));
        } catch (RuntimeException exception) {
            compatible = false;
            BgLandsClient.LOGGER.warn("Could not send BG Lands UI hello; using server fallback", exception);
        }
    }

    private static boolean isCurrentControl(OpenScreen screen, UiControl selected) {
        return screen.controls().stream().anyMatch(control ->
                control.id().equals(selected.id()) && control.token().equals(selected.token())
        );
    }
}
