package aquadx.sega.aimedb

import ext.logger
import ext.toHex
import aquadx.net.Fedy
import aquadx.net.db.AquaUserServices
import aquadx.sega.allnet.AllNetProps
import aquadx.sega.general.model.Card
import aquadx.sega.general.service.CardService
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import org.springframework.stereotype.Component
import java.nio.charset.StandardCharsets.US_ASCII
import java.time.LocalDateTime

/**
 * @author samnyan (privateamusement@protonmail.com)
 */
@Component
@ChannelHandler.Sharable
class AimeDB(
    val cardService: CardService,
    val us: AquaUserServices,
    val allNetProps: AllNetProps,
    val fedy: Fedy,
): ChannelInboundHandlerAdapter() {
    val logger = logger()

    data class AimeBaseInfo(
        val magic: UInt, val version: UInt, val responseCode: UInt, val length: UInt,
        val status: UInt, val gameId: String, val storeId: UInt, val keychipId: String
    )

    fun ByteBuf.decodeHeader() = AimeBaseInfo(
        magic = readShortLE().toUInt(),         // 00  2b
        version = readShortLE().toUInt(),       // 02  2b
        responseCode = readShortLE().toUInt(),  // 04  2b
        length = readShortLE().toUInt(),        // 06  2b
        status = readShortLE().toUInt(),        // 08  2b
        gameId = readPaddedString(6u),          // 0a  6b
        storeId = readIntLE().toUInt(),         // 10  4b
        keychipId = readPaddedString(12u)       // 14 12b
    )

    fun ByteBuf.readPaddedString(maxLen: UInt) = readBytes(maxLen.toInt()).toString(US_ASCII).trimEnd('\u0000')

    data class Handler(val name: String, val fn: (ByteBuf) -> ByteBuf?)

    final val handlers = mapOf(
        0x01 to ::doFelicaLookup,
        0x04 to ::doLookup,
        0x05 to ::doRegister,
        0x09 to ::doLog,
        0x0b to ::doCampaign,
        0x0d to ::doTouch,
        0x0f to ::doLookupV2,
        0x11 to ::doFelicaLookupV2,
        0x13 to ::doUnknown19,
        0x64 to ::doHello,
        0x66 to ::doGoodbye
    ).map { (k, v) -> k to Handler(v.toString().substringBefore('(').substringAfterLast('.').substring(2), v) }.toMap()

    /**
     * Handle the incoming request
     */
    override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
        if (msg !is Map<*, *>) return
        val type = msg["type"] as Int
        val data = msg["data"] as ByteBuf
        try {
            val base = data.decodeHeader()
            val handler = handlers[type] ?: return logger.error("AimeDB: Unknown request type 0x${type.toString(16)}")

            logger.info("AimeDB /${handler.name} : $base")

            // Check keychip
            // We do not check for type 0x13 because of a bug in duolinguo.dll
            if (!us.validKeychip(base.keychipId) && type != 0x13) {
                if (allNetProps.keychipPermissiveForTesting) {
                    logger.warn("> Accepted invalid keychip ${base.keychipId} in permissive mode")
                } else {
                    logger.warn("> Rejected: Keychip not found")
                    return
                }
            }

            handler.fn(data)?.let { ctx.write(it) }
        } finally {
            data.release()
            ctx.flush()
            ctx.close()
        }
    }

    @Deprecated("Deprecated in Netty 5") // TODO: Move this to ChannelInboundHandler
    override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
        logger.error("AimeDB: Error", cause)
        ctx.close()
    }

    override fun channelInactive(ctx: ChannelHandlerContext) {
        super.channelInactive(ctx)
        logger.debug("AimeDB: Connection closed")
    }

    /**
     * Felica Lookup v1: Return the Felica IDm as-is
     */
    fun doFelicaLookup(msg: ByteBuf): ByteBuf {
        val idm = msg.slice(0x20, 0x28 - 0x20).getLong(0)
        val pmm = msg.slice(0x28, 0x30 - 0x28).getLong(0)
        logger.info("> Felica Lookup v1 (idm ${idm.toHex()}, pmm ${pmm.toHex()})")

        // Get the decimal represent of the hex value, same from minime
        val accessCode = idm.toString().replace("-", "").padStart(20, '0')

        logger.info("> Response: $accessCode")
        return Unpooled.copiedBuffer(ByteArray(0x30)).apply {
            setShortLE(0x04, 0x03)
            setShortLE(0x08, 1)
            setBytes(0x24, ByteBufUtil.decodeHexDump(accessCode))
        }
    }

    fun getCard(accessCode: String) = us.cardRepo.findByLuid(accessCode)?.maybeGhost()?.let { card ->
        // Update card access time and return the extId
        us.cardRepo.save(card.apply { accessTime = LocalDateTime.now() }).extId
    } ?: -1

    /**
     * Felica Lookup v2: Look up the card in the card repository, return the External ID
     */
    fun doFelicaLookupV2(msg: ByteBuf): ByteBuf {
        val idm = msg.slice(0x30, 0x38 - 0x30).getLong(0)
        logger.info("> Felica Lookup v2 (idm $idm)")

        // Get the decimal represent of the hex value, same from minime
        val accessCode = idm.toString().replace("-", "").padStart(20, '0')
        val aimeId = getCard(accessCode)

        logger.info("> Response: $accessCode, $aimeId")
        return Unpooled.copiedBuffer(ByteArray(0x0140)).apply {
            setShortLE(0x04, 0x12)
            setShortLE(0x08, 1)
            setLongLE(0x20, aimeId)
            setIntLE(0x24, -0x1) // 0xFFFFFFFF
            setIntLE(0x28, -0x1) // 0xFFFFFFFF
            setBytes(0x2c, ByteBufUtil.decodeHexDump(accessCode))
            setShortLE(0x37, 0x01)
        }
    }

    /**
     * Lookup v1: Find the LUID in the database and return the External ID
     */
    fun doLookup(msg: ByteBuf): ByteBuf {
        val luid = ByteBufUtil.hexDump(msg.slice(0x20, 0x2a - 0x20))
        logger.info("> Lookup v1 (luid $luid)")

        val aimeId = getCard(luid)

        logger.info("> Response: $aimeId")
        return Unpooled.copiedBuffer(ByteArray(0x0130)).apply {
            setShortLE(0x04, 0x06)
            setShortLE(0x08, 1)
            setLongLE(0x20, aimeId)
            setByte(0x24, 0)
        }
    }

    fun doLookupV2(msg: ByteBuf): ByteBuf {
        val luid = ByteBufUtil.hexDump(msg.slice(0x20, 0x2a - 0x20))
        logger.info("> Lookup v2 (luid $luid)")

        val aimeId = getCard(luid)

        logger.info("> Response: $aimeId")
        return Unpooled.copiedBuffer(ByteArray(0x0130)).apply {
            setShortLE(0x04, 0x10)
            setShortLE(0x08, 1)
            setLongLE(0x20, aimeId)
            setByte(0x24, 0)
        }
    }

    /**
     * Register: Register a new card by access code
     */
    fun doRegister(msg: ByteBuf): ByteBuf {
        val luid = ByteBufUtil.hexDump(msg.slice(0x20, 0x2a - 0x20))
        logger.info("> Register (luid $luid)")

        var status = 0
        var aimeId = 0L

        if (us.cardRepo.findByLuid(luid) == null) {
            val card: Card = cardService.registerByAccessCode(luid)

            status = 1
            aimeId = card.extId

            fedy.onCardCreated(luid, card.extId)
        }
        else logger.warn("> Duplicated Aime Card Register detected, access code: $luid")

        logger.info("> Response: $status, $aimeId")
        return Unpooled.copiedBuffer(ByteArray(0x30)).apply {
            setShortLE(0x04, 0x06)
            setShortLE(0x08, status)
            setLongLE(0x20, aimeId)
        }
    }

    /**
     * Log: Just log the request and return a status 1
     */
    fun doLog(msg: ByteBuf) = Unpooled.copiedBuffer(ByteArray(0x20)).apply {
        setShortLE(0x04, 0x0a)
        setShortLE(0x08, 1)
    }

    /**
     * Campaign: Just return a status 1
     */
    fun doCampaign(msg: ByteBuf) = Unpooled.copiedBuffer(ByteArray(0x0200)).apply {
        setShortLE(0x04, 0x0c)
        setShortLE(0x08, 1)
    }

    /**
     * Touch: Just return a status 1
     */
    fun doTouch(msg: ByteBuf): ByteBuf {
        val luid = msg.getUnsignedIntLE(0x20)
        logger.info("> Touch (luid $luid)")

        return Unpooled.copiedBuffer(ByteArray(0x50)).apply {
            setShortLE(0x04, 0x0e)
            setShortLE(0x08, 1)
            setShortLE(0x20, 0x6f)
            setShortLE(0x24, 0x01)
        }
    }

    /**
     * We don't know what this is, just return a status 1
     */
    fun doUnknown19(msg: ByteBuf) = Unpooled.copiedBuffer(ByteArray(0x40)).apply {
        setShortLE(0x04, 0x14)
        setShortLE(0x08, 1)
    }

    /**
     * Ping: Just return a status 1
     */
    fun doHello(msg: ByteBuf) = Unpooled.copiedBuffer(ByteArray(0x20)).apply {
        setShortLE(0x04, 0x65)
        setShortLE(0x08, 1)
    }
    
    fun doGoodbye(msg: ByteBuf) = null
}
