package leigh.minecraft.link;

import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.domains.DefaultDomain;
import com.sk89q.worldguard.protection.ApplicableRegionSet;
import com.sk89q.worldguard.protection.managers.RegionManager;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import com.sk89q.worldguard.protection.regions.RegionContainer;
import leigh.eo.EO;
import leigh.eo.json.JsonInput;
import leigh.eo.json.JsonOutput;
import leigh.esp.sdk.ContentTypes;
import leigh.esp.sdk.link23.schema.HeartbeatElementDAO;
import leigh.esp.sdk.service.MicroService;
import leigh.mecha.http.server.WebTransaction;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.mq.Fanout;
import leigh.mecha.mq.Queue;
import leigh.mecha.util.StringAccumulatorV2;
import leigh.minecraft.link.bukkit.BukkitPlayerNameProvider;
import leigh.minecraft.link.cmd.PlayerCommand;
import leigh.minecraft.link.event.ChatMessage;
import leigh.minecraft.link.land.Land;
import leigh.minecraft.link.quickshop.QuickShopReader;
import leigh.minecraft.link.sdk.schema.v1.LandMemberAddRequestElementDAO;
import leigh.minecraft.link.sdk.schema.v1.LandMemberRemoveRequestElementDAO;
import leigh.minecraft.link.sdk.schema.v1.SimpleResponseElementDAO;
import net.milkbowl.vault.economy.Economy;
import net.milkbowl.vault.economy.EconomyResponse;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.UUID;

import static org.bukkit.Bukkit.getServer;

/**
 * This class implements an Enhanced Services Platform
 */
public class LinkMicroService extends MicroService {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(LinkMicroService.class);
    private final Fanout<PlayerCommand> commandFanout;
    private final Fanout<ChatMessage> chatFanout;
    private final MinecraftLinkPlugin plugin;
    private final HashMap<String , Integer > cnts = new HashMap<>();

    public LinkMicroService(EO serviceCfg, Fanout<PlayerCommand> commandFanout,
                            Fanout<ChatMessage> chatFanout, MinecraftLinkPlugin plugin) throws IOException {
        super(serviceCfg);
        this.commandFanout = commandFanout;
        this.chatFanout = chatFanout;
        this.plugin = plugin;

        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    logger.info("Request counts: {}", cnts);
                    try {
                        Thread.sleep(10*1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    @Override
    public void serviceRequest(WebTransaction request) throws Throwable {
        try {
            request.httpServletResponse.setContentType(ContentTypes.CONTENT_TYPE_JSON);

            String url = request.httpServletRequest.getRequestURI();
            Integer cnt = cnts.get(url);
            if (cnt == null) cnt = 0;
            cnt++;
            cnts.put(url, cnt);

            try (JsonOutput out = new JsonOutput(request.httpServletResponse.getOutputStream())) {
                switch (request.getPathInfo()) {
                    case "/v1/cmd/read":
                        cmdRead(out);
                        break;
                    case "/v1/chat/read":
                        chatRead(out);
                        break;
                    case "/v1/msg/write":
                        msgSend(new JsonInput(request.httpServletRequest.getInputStream()));
                        break;
                    case "/v1/land/create":
                        landBuy(out, new JsonInput(request.httpServletRequest.getInputStream()));
                        break;
                    case "/v1/shops/read":
                        shopsRead(out, new JsonInput(request.httpServletRequest.getInputStream()));
                        break;
                    case "/v1/land/member/add":
                        landMemberAdd(out, new JsonInput(request.httpServletRequest.getInputStream()));
                        break;
                    case "/v1/land/member/remove":
                        landMemberRemove(out, new JsonInput(request.httpServletRequest.getInputStream()));
                        break;
                    default:
                        request.httpServletResponse.setStatus(404);
                }
            }

            cnt = cnts.get(url);
            if (cnt != null) {
                if (cnt > 0) {
                    cnt--;
                    cnts.put(url, cnt);
                }
            }

            // Experiment
            request.httpServletResponse.getOutputStream().close();
        } catch (Exception e) {
            logger.error("Request failed: {}", e);
            throw e;
        }
    }

    /**
     * This API call returns a continuously streaming response of every /lc command which is entered by a user.
     */
    public void cmdRead(JsonOutput out) throws IOException, InterruptedException {
        EO heartbeat = HeartbeatElementDAO.create();

        try (Queue<PlayerCommand> q = commandFanout.createQueue()) {
            while (true) {
                PlayerCommand cmd = q.read();
                if(cmd==null) {
                   out.writeEO(heartbeat);
                } else {
                    out.writeEO(cmd.makeEo());
                }
                out.flush();
            }
        }
    }

    public void chatRead(JsonOutput out) throws IOException, InterruptedException {
        try (Queue<ChatMessage> q = chatFanout.createQueue()) {
            while (true) {
                if (q == null) return;
                ChatMessage cmd = q.read();
                if (cmd == null) return;
                EO cmdEo = cmd.getAsEO();
                out.writeEO(cmdEo);
                out.flush();
            }
        }
    }

    private void msgSend(JsonInput in) throws IOException {
        EO request = in.readEo();
        logger.info("Got msg send request: {}", request);
        UUID dstId = request.getValueUUID("dstId");
        if (dstId != null) {
            String msg = request.getValueString(MinecraftLinkConstants.MSG_KEY);
            plugin.sendMessage(dstId, msg);
        } else {
            String dst = request.getValueString(MinecraftLinkConstants.DST_KEY);
            String msg = request.getValueString(MinecraftLinkConstants.MSG_KEY);
            plugin.sendMessage(dst, msg);
        }
    }

    /**
     * Enumerate the contents of the QuickShops export file to the caller. This data will be current as-of whenever
     * the export file was created, which gives the opportunity to introduce market reporting delays and other
     * effects & manipulations.
     */
    private void shopsRead(JsonOutput out, JsonInput in) throws IOException {
        QuickShopReader qsr = new QuickShopReader(new File("/opt/mc/plugins/QuickShop/export.txt"),
              new BukkitPlayerNameProvider());
        for (EO obj : qsr.getShops()) {
            out.writeEO(obj);
        }
    }

    private void landMemberRemove(JsonOutput out, JsonInput in) throws IOException {
        EO request = in.readEo();
        EO reply = SimpleResponseElementDAO.create();

        UUID owner = LandMemberRemoveRequestElementDAO.getSrc(request);
        String player = LandMemberRemoveRequestElementDAO.getPlayer(request);
        String lotName = LandMemberRemoveRequestElementDAO.getLot(request);

        // TODO: Add support for other worlds
        World world = Bukkit.getWorld("world");
        RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer();
        RegionManager regions = container.get(BukkitAdapter.adapt(world));

        ProtectedRegion lot = regions.getRegions().get(lotName);
        if (lot == null) {
            SimpleResponseElementDAO.setMsg(reply, "Could not find lot: " + lotName);
            out.writeEO(reply);
            return;
        }

        DefaultDomain owners = lot.getOwners();
        if (!owners.contains(owner)) {
            SimpleResponseElementDAO.setMsg(reply,
                    "Permission denied.");
            out.writeEO(reply);
            return;
        }

        // TODO: Do we want to allow the owner to add themself as a member also?
        DefaultDomain members = lot.getMembers();
        if (!members.contains(player)) {
            SimpleResponseElementDAO.setMsg(reply,
                    "That Survivor is not a member of that lot.");
            out.writeEO(reply);
            return;
        }

        // TODO: What happens if there is no such player?
        members.removePlayer(player);

        SimpleResponseElementDAO.setMsg(reply,
                "Survivor removed from lot.");
        out.writeEO(reply);
    }

    private void landMemberAdd(JsonOutput out, JsonInput in) throws IOException {
        EO request = in.readEo();
        EO reply = SimpleResponseElementDAO.create();

        UUID owner = LandMemberAddRequestElementDAO.getSrc(request);
        String player = LandMemberAddRequestElementDAO.getPlayer(request);
        String lotName = LandMemberAddRequestElementDAO.getLot(request);

        // TODO: Add support for other worlds
        World world = Bukkit.getWorld("world");
        RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer();
        RegionManager regions = container.get(BukkitAdapter.adapt(world));

        ProtectedRegion lot = regions.getRegions().get(lotName);
        if (lot == null) {
            SimpleResponseElementDAO.setMsg(reply, "Could not find lot: " + lotName);
            out.writeEO(reply);
            return;
        }

        DefaultDomain owners = lot.getOwners();
        if (!owners.contains(owner)) {
            SimpleResponseElementDAO.setMsg(reply,
                    "Permission denied.");
            out.writeEO(reply);
            return;
        }

        // TODO: Do we want to allow the owner to add themself as a member also?
        DefaultDomain members = lot.getMembers();
        if (members.contains(player)) {
            SimpleResponseElementDAO.setMsg(reply,
                    "That Survivor is already a member of this lot.");
            out.writeEO(reply);
            return;
        }

        // TODO: What happens if there is no such player?
        members.addPlayer(player);

        SimpleResponseElementDAO.setMsg(reply,
                "Survivor added to lot.");
        out.writeEO(reply);
    }

    /**
     * Perform a land purchase operation.
     * <p>
     * This method verifies that the requested land area does not overlap an existing lot, unless that lot begins
     * with "ctl-". The purchase operation calculates the size for the new lot based on the simple formula of
     * M19/sqm. If the player has insufficient funds to complete the transaction, the land sale is blocked.
     */
    // TODO: Is it safe to run this processing outside of a tick?
    private void landBuy(JsonOutput out, JsonInput in) throws IOException {
        logger.info("PROCESSING LAND BUY REQUEST");

        EO request = in.readEo();
        EO reply = new EO();

        Player p = Bukkit.getPlayer(request.getValueString(MinecraftLinkConstants.SRC_KEY));

        int price = request.getValueInteger(MinecraftLinkConstants.PRICE);
        RegisteredServiceProvider<Economy> economyProvider =
                getServer().getServicesManager().getRegistration(net.milkbowl.vault.economy.Economy.class);

        // TODO: Verify math here, it may be truncating
        double marbles = price / 100;

        // TODO: Add support for other worlds
        World world = Bukkit.getWorld("world");
        RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer();
        RegionManager regions = container.get(BukkitAdapter.adapt(world));

        BlockVector3 min = BlockVector3.at(request.getValueInteger(MinecraftLinkConstants.POS_X1),
                Land.MIN_Y,
                request.getValueInteger(MinecraftLinkConstants.POS_Z1));
        BlockVector3 max = BlockVector3.at(request.getValueInteger(MinecraftLinkConstants.POS_X2),
                Land.MAX_Y,
                request.getValueInteger(MinecraftLinkConstants.POS_Z2));

        logger.info("Calculated min/max {} {}", min, max);

        // TODO: This leaks a lot # from the master sequence if there is a collision.

        String lotName = plugin.getRealmAbv() + "-" + plugin.getNewLotNumber();
        lotName = lotName.toUpperCase(Locale.getDefault());
        ProtectedRegion candidateLot = new ProtectedCuboidRegion(lotName, min, max);
        ApplicableRegionSet set = regions.getApplicableRegions(candidateLot);
        StringAccumulatorV2 sa = new StringAccumulatorV2(",");

        boolean foundCollision = false;
        for (ProtectedRegion reg : set) {
            // We ignore control zones, since they are for weather-mods, etc.
            if (reg.getId().toLowerCase(Locale.getDefault()).startsWith("ctl-")) continue;
            foundCollision = true;
            sa.add(reg.getId());
        }

        if (foundCollision) {
            reply.setValue(MinecraftLinkConstants.MSG_KEY,
                    "Could not create lot due to overlaps with existing lots: " + sa.asString());
            out.writeEO(reply);
            return;
        }

        DefaultDomain members = candidateLot.getOwners();
        members.addPlayer(p.getUniqueId());
        EconomyResponse er = economyProvider.getProvider().withdrawPlayer(p.getName(), marbles);
        logger.info("Economy response: {} {}", er.transactionSuccess(), er.errorMessage);

        if (!er.transactionSuccess()) {
            reply.setValue(MinecraftLinkConstants.MSG_KEY, "Insufficient funds.");
            out.writeEO(reply);
            return;
        }

        // TODO: Transaction logging and auditing.
        regions.addRegion(candidateLot);
        reply.setValue(MinecraftLinkConstants.MSG_KEY, "Lot created. [name: " + lotName + "]");
        out.writeEO(reply);
    }
}
