from datetime import datetime
from typing import Dict, List, Optional

from sqlalchemy import Column, Table, UniqueConstraint, and_
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.types import TIMESTAMP, Boolean, Integer, String

from core.data.schema import BaseData, metadata

card: Table = Table(
    "ongeki_user_card",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("cardId", Integer),
    Column("digitalStock", Integer),
    Column("analogStock", Integer),
    Column("level", Integer),
    Column("maxLevel", Integer),
    Column("exp", Integer),
    Column("printCount", Integer),
    Column("useCount", Integer),
    Column("isNew", Boolean),
    Column("kaikaDate", String(25)),
    Column("choKaikaDate", String(25)),
    Column("skillId", Integer),
    Column("isAcquired", Boolean),
    Column("created", String(25)),
    UniqueConstraint("user", "cardId", name="ongeki_user_card_uk"),
    mysql_charset="utf8mb4",
)

deck = Table(
    "ongeki_user_deck",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("deckId", Integer),
    Column("cardId1", Integer),
    Column("cardId2", Integer),
    Column("cardId3", Integer),
    UniqueConstraint("user", "deckId", name="ongeki_user_deck_uk"),
    mysql_charset="utf8mb4",
)

character: Table = Table(
    "ongeki_user_character",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("characterId", Integer),
    Column("costumeId", Integer),
    Column("attachmentId", Integer),
    Column("playCount", Integer),
    Column("intimateLevel", Integer),
    Column("intimateCount", Integer),
    Column("intimateCountRewarded", Integer),
    Column("intimateCountDate", String(25)),
    Column("isNew", Boolean),
    UniqueConstraint("user", "characterId", name="ongeki_user_character_uk"),
    mysql_charset="utf8mb4",
)

boss = Table(
    "ongeki_user_boss",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("musicId", Integer),
    Column("damage", Integer),
    Column("isClear", Boolean),
    Column("eventId", Integer),
    UniqueConstraint("user", "musicId", "eventId", name="ongeki_user_boss_uk"),
    mysql_charset="utf8mb4",
)

story = Table(
    "ongeki_user_story",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("storyId", Integer),
    Column("jewelCount", Integer),
    Column("lastChapterId", Integer),
    Column("lastPlayMusicId", Integer),
    Column("lastPlayMusicCategory", Integer),
    Column("lastPlayMusicLevel", Integer),
    UniqueConstraint("user", "storyId", name="ongeki_user_story_uk"),
    mysql_charset="utf8mb4",
)

chapter = Table(
    "ongeki_user_chapter",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("chapterId", Integer),
    Column("jewelCount", Integer),
    Column("isStoryWatched", Boolean),
    Column("isClear", Boolean),
    Column("lastPlayMusicId", Integer),
    Column("lastPlayMusicCategory", Integer),
    Column("lastPlayMusicLevel", Integer),
    Column("skipTiming1", Integer),
    Column("skipTiming2", Integer),
    UniqueConstraint("user", "chapterId", name="ongeki_user_chapter_uk"),
    mysql_charset="utf8mb4",
)

memorychapter = Table(
    "ongeki_user_memorychapter",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("chapterId", Integer),
    Column("gaugeId", Integer),
    Column("gaugeNum", Integer),
    Column("jewelCount", Integer),
    Column("isStoryWatched", Boolean),
    Column("isBossWatched", Boolean),
    Column("isDialogWatched", Boolean),
    Column("isEndingWatched", Boolean),
    Column("isClear", Boolean),
    Column("lastPlayMusicId", Integer),
    Column("lastPlayMusicLevel", Integer),
    Column("lastPlayMusicCategory", Integer),
    UniqueConstraint("user", "chapterId", name="ongeki_user_memorychapter_uk"),
    mysql_charset="utf8mb4",
)

item: Table = Table(
    "ongeki_user_item",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("itemKind", Integer),
    Column("itemId", Integer),
    Column("stock", Integer),
    Column("isValid", Boolean),
    UniqueConstraint("user", "itemKind", "itemId", name="ongeki_user_item_uk"),
    mysql_charset="utf8mb4",
)

music_item = Table(
    "ongeki_user_music_item",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("musicId", Integer),
    Column("status", Integer),
    UniqueConstraint("user", "musicId", name="ongeki_user_music_item_uk"),
    mysql_charset="utf8mb4",
)

login_bonus = Table(
    "ongeki_user_login_bonus",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("bonusId", Integer),
    Column("bonusCount", Integer),
    Column("lastUpdateDate", String(25)),
    UniqueConstraint("user", "bonusId", name="ongeki_user_login_bonus_uk"),
    mysql_charset="utf8mb4",
)

event_point = Table(
    "ongeki_user_event_point",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("version", Integer, nullable=False),
    Column("eventId", Integer, nullable=False),
    Column("point", Integer, nullable=False),
    Column("rank", Integer),
    Column("type", Integer, nullable=False),
    Column("date", String(25)),
    Column("isRankingRewarded", Boolean),
    UniqueConstraint("user", "eventId", name="ongeki_user_event_point_uk"),
    mysql_charset="utf8mb4",
)

mission_point = Table(
    "ongeki_user_mission_point",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("version", Integer),
    Column("eventId", Integer),
    Column("point", Integer),
    UniqueConstraint("user", "eventId", name="ongeki_user_mission_point_uk"),
    mysql_charset="utf8mb4",
)

scenerio = Table(
    "ongeki_user_scenerio",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("scenarioId", Integer),
    Column("playCount", Integer),
    UniqueConstraint("user", "scenarioId", name="ongeki_user_scenerio_uk"),
    mysql_charset="utf8mb4",
)

trade_item = Table(
    "ongeki_user_trade_item",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("chapterId", Integer),
    Column("tradeItemId", Integer),
    Column("tradeCount", Integer),
    UniqueConstraint(
        "user", "chapterId", "tradeItemId", name="ongeki_user_trade_item_uk"
    ),
    mysql_charset="utf8mb4",
)

event_music = Table(
    "ongeki_user_event_music",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("eventId", Integer),
    Column("type", Integer),
    Column("musicId", Integer),
    Column("level", Integer),
    Column("techScoreMax", Integer),
    Column("platinumScoreMax", Integer),
    Column("techRecordDate", String(25)),
    Column("isTechNewRecord", Boolean),
    UniqueConstraint(
        "user", "eventId", "type", "musicId", "level", name="ongeki_user_event_music"
    ),
    mysql_charset="utf8mb4",
)

tech_event = Table(
    "ongeki_user_tech_event",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("version", Integer, nullable=False),
    Column("eventId", Integer, nullable=False),
    Column("totalTechScore", Integer, nullable=False),
    Column("totalPlatinumScore", Integer, nullable=False),
    Column("techRecordDate", String(25)),
    Column("isRankingRewarded", Boolean),
    Column("isTotalTechNewRecord", Boolean),
    UniqueConstraint("user", "eventId", name="ongeki_user_tech_event_uk"),
    mysql_charset="utf8mb4",
)

tech_ranking = Table(
    "ongeki_tech_event_ranking",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
    Column("version", Integer, nullable=False),
    Column("date", String(25)),
    Column("eventId", Integer, nullable=False),
    Column("rank", Integer),
    Column("totalPlatinumScore", Integer, nullable=False),
    Column("totalTechScore", Integer, nullable=False),
    UniqueConstraint("user", "eventId", name="ongeki_tech_event_ranking_uk"),
    mysql_charset="utf8mb4",
)

gacha = Table(
    "ongeki_user_gacha",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("gachaId", Integer, nullable=False),
    Column("totalGachaCnt", Integer, server_default="0"),
    Column("ceilingGachaCnt", Integer, server_default="0"),
    Column("selectPoint", Integer, server_default="0"),
    Column("useSelectPoint", Integer, server_default="0"),
    Column("dailyGachaCnt", Integer, server_default="0"),
    Column("fiveGachaCnt", Integer, server_default="0"),
    Column("elevenGachaCnt", Integer, server_default="0"),
    Column("dailyGachaDate", TIMESTAMP, nullable=False, server_default=func.now()),
    UniqueConstraint("user", "gachaId", name="ongeki_user_gacha_uk"),
    mysql_charset="utf8mb4",
)

gacha_supply = Table(
    "ongeki_user_gacha_supply",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("cardId", Integer, nullable=False),
    UniqueConstraint("user", "cardId", name="ongeki_user_gacha_supply_uk"),
    mysql_charset="utf8mb4",
)


print_detail = Table(
    "ongeki_user_print_detail",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("cardId", Integer, nullable=False),
    Column("cardType", Integer, server_default="0"),
    Column("printDate", TIMESTAMP, nullable=False),
    Column("serialId", String(20), nullable=False),
    Column("placeId", Integer, nullable=False),
    Column("clientId", String(11), nullable=False),
    Column("printerSerialId", String(20), nullable=False),
    Column("isHolograph", Boolean, server_default="0"),
    Column("isAutographed", Boolean, server_default="0"),
    Column("printOption1", Boolean, server_default="1"),
    Column("printOption2", Boolean, server_default="1"),
    Column("printOption3", Boolean, server_default="1"),
    Column("printOption4", Boolean, server_default="1"),
    Column("printOption5", Boolean, server_default="1"),
    Column("printOption6", Boolean, server_default="1"),
    Column("printOption7", Boolean, server_default="1"),
    Column("printOption8", Boolean, server_default="1"),
    Column("printOption9", Boolean, server_default="1"),
    Column("printOption10", Boolean, server_default="0"),
    UniqueConstraint("serialId", name="ongeki_user_print_detail_uk"),
    mysql_charset="utf8mb4",
)

class OngekiItemData(BaseData):
    async def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]:
        card_data["user"] = aime_id

        sql = insert(card).values(**card_data)
        conflict = sql.on_duplicate_key_update(**card_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_card: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_cards(
        self, aime_id: int, limit: Optional[int] = None, offset: Optional[int] = None
    ) -> Optional[List[Row]]:
        sql = select(card).where(card.c.user == aime_id)

        if limit is not None or offset is not None:
            sql = sql.order_by(card.c.id)
        if limit is not None:
            sql = sql.limit(limit)
        if offset is not None:
            sql = sql.offset(offset)

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchall()

    async def put_character(self, aime_id: int, character_data: Dict) -> Optional[int]:
        character_data["user"] = aime_id

        sql = insert(character).values(**character_data)
        conflict = sql.on_duplicate_key_update(**character_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_character: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_characters(
        self, aime_id: int, limit: Optional[int] = None, offset: Optional[int] = None
    ) -> Optional[List[Row]]:
        sql = select(character).where(character.c.user == aime_id)

        if limit is not None or offset is not None:
            sql = sql.order_by(character.c.id)
        if limit is not None:
            sql = sql.limit(limit)
        if offset is not None:
            sql = sql.offset(offset)

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchall()

    async def put_deck(self, aime_id: int, deck_data: Dict) -> Optional[int]:
        deck_data["user"] = aime_id

        sql = insert(deck).values(**deck_data)
        conflict = sql.on_duplicate_key_update(**deck_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_deck: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_deck(self, aime_id: int, deck_id: int) -> Optional[Dict]:
        sql = select(deck).where(and_(deck.c.user == aime_id, deck.c.deckId == deck_id))

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchone()

    async def get_decks(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(deck).where(deck.c.user == aime_id)

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchall()

    async def put_boss(self, aime_id: int, boss_data: Dict) -> Optional[int]:
        boss_data["user"] = aime_id

        sql = insert(boss).values(**boss_data)
        conflict = sql.on_duplicate_key_update(**boss_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_boss: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]:
        story_data["user"] = aime_id

        sql = insert(story).values(**story_data)
        conflict = sql.on_duplicate_key_update(**story_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_story: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_stories(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(story).where(story.c.user == aime_id)

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchall()

    async def put_chapter(self, aime_id: int, chapter_data: Dict) -> Optional[int]:
        chapter_data["user"] = aime_id

        sql = insert(chapter).values(**chapter_data)
        conflict = sql.on_duplicate_key_update(**chapter_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_chapter: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_chapters(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(chapter).where(chapter.c.user == aime_id)

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchall()

    async def put_item(self, aime_id: int, item_data: Dict) -> Optional[int]:
        item_data["user"] = aime_id

        sql = insert(item).values(**item_data)
        conflict = sql.on_duplicate_key_update(**item_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_item: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_item(self, aime_id: int, item_id: int, item_kind: int) -> Optional[Dict]:
        sql = select(item).where(and_(item.c.user == aime_id, item.c.itemId == item_id))

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchone()

    async def get_items(
        self,
        aime_id: int,
        item_kind: Optional[int] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None, 
    ) -> Optional[List[Row]]:
        cond = item.c.user == aime_id

        if item_kind is not None:
            cond &= item.c.itemKind == item_kind

        sql = select(item).where(cond)

        if limit is not None or offset is not None:
            sql = sql.order_by(item.c.id)
        if limit is not None:
            sql = sql.limit(limit)
        if offset is not None:
            sql = sql.offset(offset)

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchall()

    async def put_music_item(self, aime_id: int, music_item_data: Dict) -> Optional[int]:
        music_item_data["user"] = aime_id

        sql = insert(music_item).values(**music_item_data)
        conflict = sql.on_duplicate_key_update(**music_item_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_music_item: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_music_items(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(music_item).where(music_item.c.user == aime_id)
        result = await self.execute(sql)

        if result is None:
            return None
        return result.fetchall()

    async def put_login_bonus(self, aime_id: int, login_bonus_data: Dict) -> Optional[int]:
        login_bonus_data["user"] = aime_id

        sql = insert(login_bonus).values(**login_bonus_data)
        conflict = sql.on_duplicate_key_update(**login_bonus_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_login_bonus: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_login_bonuses(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(login_bonus).where(login_bonus.c.user == aime_id)
        result = await self.execute(sql)

        if result is None:
            return None
        return result.fetchall()

    async def put_mission_point(self, aime_id: int, version: int, mission_point_data: Dict) -> Optional[int]:
        mission_point_data["version"] = version
        mission_point_data["user"] = aime_id

        sql = insert(mission_point).values(**mission_point_data)
        conflict = sql.on_duplicate_key_update(**mission_point_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_mission_point: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_mission_points(self, version: int, aime_id: int) -> Optional[List[Dict]]:
        sql = select(mission_point).where(and_(
            mission_point.c.user == aime_id,
            mission_point.c.version <= version)
        ).order_by(mission_point.c.version.desc())

        result = await self.execute(sql)

        if result is None:
            return None
        return result.fetchall()

    async def put_event_point(self, aime_id: int, version: int, event_point_data: Dict) -> Optional[int]:
        # We update only the newest (type: 1) entry, in official spec game watches for both latest(type:1) and previous (type:2) entries to give an additional info how many ranks has player moved up or down
        # This fully featured is on TODO list, at the moment we just update the tables as data comes and give out rank as request comes
        event_point_data["user"] = aime_id
        event_point_data["version"] = version
        event_point_data["type"] = 1
        event_point_time = datetime.now()
        event_point_data["date"] = datetime.strftime(event_point_time, "%Y-%m-%d %H:%M")

        sql = insert(event_point).values(**event_point_data)
        conflict = sql.on_duplicate_key_update(**event_point_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_event_point: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_event_points(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(event_point).where(event_point.c.user == aime_id)
        result = await self.execute(sql)

        if result is None:
            return None
        return result.fetchall()

    async def put_scenerio(self, aime_id: int, scenerio_data: Dict) -> Optional[int]:
        scenerio_data["user"] = aime_id

        sql = insert(scenerio).values(**scenerio_data)
        conflict = sql.on_duplicate_key_update(**scenerio_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_scenerio: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_scenerios(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(scenerio).where(scenerio.c.user == aime_id)
        result = await self.execute(sql)

        if result is None:
            return None
        return result.fetchall()

    async def put_trade_item(self, aime_id: int, trade_item_data: Dict) -> Optional[int]:
        trade_item_data["user"] = aime_id

        sql = insert(trade_item).values(**trade_item_data)
        conflict = sql.on_duplicate_key_update(**trade_item_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_trade_item: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_trade_items(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(trade_item).where(trade_item.c.user == aime_id)
        result = await self.execute(sql)

        if result is None:
            return None
        return result.fetchall()

    async def put_event_music(self, aime_id: int, event_music_data: Dict) -> Optional[int]:
        event_music_data["user"] = aime_id

        sql = insert(event_music).values(**event_music_data)
        conflict = sql.on_duplicate_key_update(**event_music_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_event_music: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_event_music(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(event_music).where(event_music.c.user == aime_id)
        result = await self.execute(sql)

        if result is None:
            return None
        return result.fetchall()

    async def put_tech_event(self, aime_id: int, version: int, tech_event_data: Dict) -> Optional[int]:
        tech_event_data["user"] = aime_id
        tech_event_data["version"] = version

        sql = insert(tech_event).values(**tech_event_data)
        conflict = sql.on_duplicate_key_update(**tech_event_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_tech_event: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def put_tech_event_ranking(self, aime_id: int, version: int, tech_event_data: Dict) -> Optional[int]:
        tech_event_data["user"] = aime_id
        tech_event_data["version"] = version
        tech_event_data.pop("isRankingRewarded")
        tech_event_data.pop("isTotalTechNewRecord")
        tech_event_data["date"] = tech_event_data.pop("techRecordDate")
        tech_event_data["rank"] = 0

        sql = insert(tech_ranking).values(**tech_event_data)
        conflict = sql.on_duplicate_key_update(**tech_event_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_tech_event_ranking: Failed to update ranking! aime_id {aime_id}")
            return None
        return result.lastrowid

    async def get_tech_event(self, version: int, aime_id: int) -> Optional[List[Dict]]:
        sql = select(tech_event).where(and_(
            tech_event.c.user == aime_id,
            tech_event.c.version <= version)
        ).order_by(tech_event.c.version.desc())

        result = await self.execute(sql)

        if result is None:
            return None
        return result.fetchall()

    async def get_bosses(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(boss).where(boss.c.user == aime_id)
        result = await self.execute(sql)

        if result is None:
            return None
        return result.fetchall()

    async def put_memorychapter(
        self, aime_id: int, memorychapter_data: Dict
    ) -> Optional[int]:
        memorychapter_data["user"] = aime_id

        sql = insert(memorychapter).values(**memorychapter_data)
        conflict = sql.on_duplicate_key_update(**memorychapter_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_memorychapter: Failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def get_memorychapters(self, aime_id: int) -> Optional[List[Dict]]:
        sql = select(memorychapter).where(memorychapter.c.user == aime_id)

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchall()

    async def get_user_gacha(self, aime_id: int, gacha_id: int) -> Optional[Row]:
        sql = gacha.select(and_(gacha.c.user == aime_id, gacha.c.gachaId == gacha_id))

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchone()

    async def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]:
        sql = gacha.select(gacha.c.user == aime_id)

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchall()

    async def get_user_gacha_supplies(self, aime_id: int) -> Optional[List[Row]]:
        sql = gacha_supply.select(gacha_supply.c.user == aime_id)

        result = await self.execute(sql)
        if result is None:
            return None
        return result.fetchall()

    async def put_user_gacha(self, aime_id: int, gacha_id: int, **data) -> Optional[int]:
        sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **data)

        conflict = sql.on_duplicate_key_update(user=aime_id, gachaId=gacha_id, **data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
            return None
        return result.lastrowid

    async def put_user_print_detail(
        self, aime_id: int, serial_id: str, user_print_data: Dict
    ) -> Optional[int]:
        sql = insert(print_detail).values(
            user=aime_id, serialId=serial_id, **user_print_data
        )

        conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data)
        result = await self.execute(conflict)

        if result is None:
            self.logger.warning(
                f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
            )
            return None
        return result.lastrowid


    async def get_ranking_event_ranks(self, version: int, aime_id: int) -> Optional[List[Dict]]:
        # Calculates player rank on GameRequest from server, and sends it back, official spec would rank players in maintenance period, on TODO list
        sql = select(event_point.c.id, event_point.c.user, event_point.c.eventId, event_point.c.type, func.row_number().over(partition_by=event_point.c.eventId, order_by=event_point.c.point.desc()).label('rank'), event_point.c.date, event_point.c.point).where(event_point.c.version <= version).order_by(event_point.c.version.desc())
        result = await self.execute(sql)
        if result is None:
            self.logger.error(f"failed to rank aime_id: {aime_id} ranking event positions")
            return None
        return result.fetchall()

    async def get_tech_event_ranking(self, version: int, aime_id: int) -> Optional[List[Dict]]:
        sql = select(tech_ranking.c.id, tech_ranking.c.user, tech_ranking.c.date, tech_ranking.c.eventId, func.row_number().over(partition_by=tech_ranking.c.eventId, order_by=[tech_ranking.c.totalTechScore.desc(),tech_ranking.c.totalPlatinumScore.desc()]).label('rank'), tech_ranking.c.totalTechScore, tech_ranking.c.totalPlatinumScore).where(tech_ranking.c.version == version)
        result = await self.execute(sql)
        if result is None:
            self.logger.warning(f"aime_id: {aime_id} has no tech ranking ranks")
            return None
        return result.fetchall()
