package xyz.alexcrea.cuanvil.command

import io.delilaheve.CustomAnvil
import net.md_5.bungee.api.chat.ClickEvent
import net.md_5.bungee.api.chat.HoverEvent
import net.md_5.bungee.api.chat.TextComponent
import net.md_5.bungee.api.chat.hover.content.Text
import org.bukkit.Bukkit
import org.bukkit.ChatColor
import org.bukkit.Material
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import org.bukkit.enchantments.Enchantment
import org.bukkit.entity.HumanEntity
import org.bukkit.entity.Player
import org.bukkit.event.inventory.InventoryType
import org.bukkit.event.inventory.PrepareAnvilEvent
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.Damageable
import org.bukkit.inventory.meta.EnchantmentStorageMeta
import org.bukkit.inventory.view.AnvilView
import org.bukkit.plugin.Plugin
import org.bukkit.plugin.RegisteredListener
import xyz.alexcrea.cuanvil.dependency.DependencyManager
import xyz.alexcrea.cuanvil.dependency.packet.NoPacketManager
import xyz.alexcrea.cuanvil.dependency.packet.ProtocoLibWrapper
import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry
import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener
import xyz.alexcrea.cuanvil.util.MetricsUtil
import java.util.*
import java.util.stream.Collectors

class DiagnosticExecutor : CASubCommand {

    companion object {
        const val NO_DIAG_PERM = "You do not have permission to diagnostic this server"

        fun fetchNMSType(): String {
            val packetManager = DependencyManager.packetManager
            val packetManagerClass = packetManager.javaClass

            val className = packetManagerClass.name
            val result = if (className.contains("PaperPacket")) {
                "Paper"
            } else {
                when (packetManagerClass) {
                    ProtocoLibWrapper::class.java -> "Protocolib"
                    NoPacketManager::class.java -> "None"
                    else -> "Version Specific"
                }
            }


            return "$result ${if (packetManager.canSetInstantBuild) '✅' else '❌'}"
        }
    }

    enum class DiagParams(val value: String) {
        OS_PRIVACY("os_privacy"),
        PLUGIN_PRIVACY("plugin_privacy"),
        NO_MERGE_TEST("no_merge_test"),
        FULL_ENCHANTMENT_DATA("full_enchantment_data"),
        INCLUDE_LAST_ERROR("include_last_error"),
    }

    private fun fetchParameters(args: Array<out String>): EnumSet<DiagParams> {
        val result = EnumSet.noneOf(DiagParams::class.java)
        val argSet = HashSet<String>()

        for (string in args) {
            argSet.add(string.lowercase())
        }

        for (param in DiagParams.entries) {
            if (argSet.contains(param.value))
                result.add(param)
        }

        return result
    }

    override fun tabCompleter(
        sender: CommandSender,
        args: Array<out String>,
        list: MutableList<String>
    ) {
        if (!allowed(sender)) return

        val map = fetchParameters(args)
        for (param in DiagParams.entries) {
            if (!map.contains(param))
                list.add(param.value)
        }

    }

    override fun executeCommand(
        sender: CommandSender,
        cmd: Command,
        cmdstr: String,
        args: Array<out String>
    ): Boolean {
        if (!allowed(sender)) {
            sender.sendMessage(NO_DIAG_PERM)
            return false
        }

        val stb = StringBuilder("```\n")
        val params = fetchParameters(args)
        var hasError = false
        try {
            diagnostic(sender, stb, params)
        } catch (e: Throwable) {
            stb.append("\n\nError happened trying to get diagnostic data:\n")
                .append(e.message).append("\n")
                .append(e.stackTrace.joinToString("\n"))
            hasError = true
            e.printStackTrace()
        }

        stb.append("\n```")

        if (sender is HumanEntity) {
            if (hasError)
                sender.spigot()
                    .sendMessage(TextComponent(ChatColor.RED.toString() + "There was an error running the diagnostic"))
            val message = TextComponent(ChatColor.GREEN.toString() + "Click to copy diagnostic data")

            message.clickEvent = ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, stb.toString())
            message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text("§7Click to copy"))

            sender.spigot().sendMessage(message)
        } else {
            sender.sendMessage(stb.toString())
        }

        return true
    }

    override fun allowed(sender: CommandSender): Boolean {
        return sender.hasPermission(CustomAnvil.diagnosticPermission)
    }

    fun diagnostic(sender: CommandSender, stb: StringBuilder, params: Set<DiagParams>) {
        stb.append("Server Info\n")
        val version = CustomAnvil.instance.description.version
        stb.append("\nPlugin Version: ").append(version)
        if (version.contains("dev")) stb.append(" (alpha)")

        stb.append("\nLatest Update: ").append(CustomAnvil.latestVer)
        stb.append("\nServer Version: ").append(Bukkit.getVersion()).append(" (").append(Bukkit.getName()).append(')')
        stb.append("\nPlugin Enabled: ").append(if (CustomAnvil.instance.isEnabled) "Yes" else "No")
        stb.append("\nNMS type: ").append(fetchNMSType())
        if (!params.contains(DiagParams.OS_PRIVACY)) {
            stb.append("\nJava Version: ").append(System.getProperty("java.version"))
            stb.append("\nOS: ").append(System.getProperty("os.name")).append(" ")
                .append(System.getProperty("os.version"))
                .append(System.getProperty("os.arch"))
        }

        stb.append("\nHad detect error: ").append(if (MetricsUtil.lastError != null) "Yes" else "No")

        if (!params.contains(DiagParams.PLUGIN_PRIVACY)) {
            pluginListDiag(sender, stb)
        }
        prepareAnvilListeners(stb)

        if (!params.contains(DiagParams.NO_MERGE_TEST)) {
            if (sender is Player) testMerge(sender, stb)
        }

        stb.append("\n\nEnchantments data:")
        partialEnchantmentData(stb)
        if (params.contains(DiagParams.FULL_ENCHANTMENT_DATA)) {
            fullEnchantmentData(stb)
        }

        if (params.contains(DiagParams.INCLUDE_LAST_ERROR)) {
            includeLastError(stb)
        }
    }

    private fun testMerge(player: Player, stb: StringBuilder) {
        val sword = ItemStack(Material.DIAMOND_SWORD)
        val damagedSword = sword.clone()
        val enchantedSword = sword.clone()
        val enchantedBook = ItemStack(Material.ENCHANTED_BOOK)
        val unitForRepair = ItemStack(Material.DIAMOND)

        var meta = damagedSword.itemMeta
        (meta as Damageable).damage = 5
        damagedSword.itemMeta = meta

        meta = enchantedSword.itemMeta
        meta!!.addEnchant(Enchantment.SHARPNESS, 1, true)
        enchantedSword.itemMeta = meta

        meta = enchantedBook.itemMeta
        (meta as EnchantmentStorageMeta).addStoredEnchant(Enchantment.SHARPNESS, 1, true)
        enchantedBook.itemMeta = meta

        stb.append("\n\nItem to Item repair:")
        simulateAnvil(player, stb, damagedSword, damagedSword, sword)

        stb.append("\n\nUnit repair:")
        simulateAnvil(player, stb, damagedSword, unitForRepair, sword)

        stb.append("\n\nEnchanting an item:")
        simulateAnvil(player, stb, sword, enchantedBook, enchantedSword)
    }

    private val Plugin.pluginNameDisplay: String
        get() {
            return this.name + " v" + this.description.version
        }

    override fun description(): String {
        return "Basic diagnostic of this plugin"
    }

    private fun pluginListDiag(sender: CommandSender, stb: StringBuilder) {
        val enabledPlugins: MutableList<Plugin?> = ArrayList<Plugin?>()
        val disabledPlugins: MutableList<Plugin?> = ArrayList<Plugin?>()
        for (plugin in Bukkit.getPluginManager().plugins) {
            if (plugin.isEnabled) {
                enabledPlugins.add(plugin)
            } else {
                disabledPlugins.add(plugin)
            }
        }

        stb.append("\nEnabled Plugins: ").append(
            enabledPlugins.stream()
                .map { plugin -> plugin!!.pluginNameDisplay }
                .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None")
        )

        stb.append("\nDisabled Plugins: ").append(
            disabledPlugins.stream()
                .map { plugin -> plugin!!.pluginNameDisplay }
                .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None")
        )
    }

    fun prepareAnvilListeners(stb: StringBuilder) {
        val eventListeners: MutableSet<Plugin?> = Arrays
            .stream(
                PrepareAnvilEvent
                    .getHandlerList()
                    .getRegisteredListeners()
            )
            .map { obj: RegisteredListener? -> obj!!.plugin }
            .collect(Collectors.toSet())

        eventListeners.remove(CustomAnvil.instance)
        stb.append("\nPrepare Anvil Listeners: ").append(
            if (eventListeners.isEmpty()) "None" else eventListeners.stream()
                .map { plugin -> plugin!!.pluginNameDisplay }
                .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None")
        )
    }

    fun simulateAnvil(player: Player, stb: StringBuilder, left: ItemStack?, right: ItemStack?, result: ItemStack?) {
        var invView: AnvilView
        var event: PrepareAnvilEvent
        try {
            val fakeInv = Bukkit.createInventory(player, InventoryType.ANVIL)
            invView = player.openInventory(fakeInv)!! as AnvilView
            event = DependencyManager.createFakeEvent(invView, result)
        } catch (_: Throwable) {
            // Help
            val menuTypeClazz = Class.forName("org.bukkit.inventory.MenuType")
            val anvilTypeField = menuTypeClazz.getField("ANVIL")
            val anvilType = anvilTypeField.get(null)
            val createMethod = anvilType.javaClass.getMethod("create", HumanEntity::class.java)
            invView = createMethod.invoke(anvilType, player) as AnvilView

            player.openInventory(invView)

            val constructor = PrepareAnvilEvent::class.java.getConstructor(AnvilView::class.java, ItemStack::class.java)
            event = constructor.newInstance(invView, result)
        }

        invView.setItem(0, left)
        invView.setItem(1, right)

        val xp = invView.repairCost
        val maxXp = invView.maximumRepairCost
        val mergeResult = invView.getItem(2)
        stb.append("\n${if (result == mergeResult) "E" else "Une"}xpected Result")

        PrepareAnvilListener().anvilCombineCheck(event)
        // Now we check if item and xp same
        stb.append("\nXP/Max XP: ")
            .append(if (invView.repairCost == xp) "Correct" else "Incorrect")
            .append("/")
            .append(if (invView.maximumRepairCost == maxXp) "Correct" else "Incorrect")
            .append(" (${invView.repairCost} $xp|${invView.maximumRepairCost} $maxXp)")
            .append("\nMerge result: ")
            .append(if (invView.getItem(2) == mergeResult) "Correct" else "Incorrect")

        PrepareAnvilListener.IS_EMPTY_TEST = true
        Bukkit.getPluginManager().callEvent(event)
        stb.append("\nNull result test: ")
            .append(if (event.result == null) "Correct" else "Incorrect")

        invView.setItem(0, null)
        invView.setItem(1, null)
        invView.setItem(2, null)
        player.closeInventory()
    }

    private fun fullEnchantmentData(stb: StringBuilder) {
        for (enchantment in CAEnchantmentRegistry.getInstance().values()) {
            stb.append("\n- ").append(enchantment.key.toString())
                .append(" ").append(enchantment.name)
                .append(" ").append(enchantment.defaultMaxLevel())
        }
    }

    private fun partialEnchantmentData(stb: StringBuilder) {
        val map = HashMap<String, Int>()
        for (enchant in CAEnchantmentRegistry.getInstance().values()) {
            map[enchant.key.namespace] = map.getOrDefault(enchant.key.namespace, 0) + 1
        }

        stb.append(
            "\nNamespaces: ${
                map.entries.stream()
                    .map { (key, value) -> "$key ($value)" }
                    .reduce { a, b -> "$a, $b" }.get()
            }"
        )

    }

    private fun includeLastError(stb: StringBuilder) {
        val e = MetricsUtil.lastError ?: return

        stb.append("\n\nLast stack trace: ${e.stackTraceToString()}")


    }

}