from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
from sqlalchemy.engine.base import Connection
from sqlalchemy.schema import ForeignKey
from sqlalchemy.engine import Row
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.sql.expression import exists
from core.data.schema import BaseData, metadata

course = Table(
    "chuni_score_course",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("courseId", Integer),
    Column("classId", Integer),
    Column("playCount", Integer),
    Column("scoreMax", Integer),
    Column("isFullCombo", Boolean),
    Column("isAllJustice", Boolean),
    Column("isSuccess", Integer),
    Column("scoreRank", Integer),
    Column("eventId", Integer),
    Column("lastPlayDate", String(25)),
    Column("param1", Integer),
    Column("param2", Integer),
    Column("param3", Integer),
    Column("param4", Integer),
    Column("isClear", Integer),
    Column("theoryCount", Integer),
    Column("orderId", Integer),
    Column("playerRating", Integer),
    UniqueConstraint("user", "courseId", name="chuni_score_course_uk"),
    mysql_charset="utf8mb4",
)

best_score = Table(
    "chuni_score_best",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("musicId", Integer),
    Column("level", Integer),
    Column("playCount", Integer),
    Column("scoreMax", Integer),
    Column("resRequestCount", Integer),
    Column("resAcceptCount", Integer),
    Column("resSuccessCount", Integer),
    Column("missCount", Integer),
    Column("maxComboCount", Integer),
    Column("isFullCombo", Boolean),
    Column("isAllJustice", Boolean),
    Column("isSuccess", Integer),
    Column("fullChain", Integer),
    Column("maxChain", Integer),
    Column("scoreRank", Integer),
    Column("isLock", Boolean),
    Column("ext1", Integer),
    Column("theoryCount", Integer),
    UniqueConstraint("user", "musicId", "level", name="chuni_score_best_uk"),
    mysql_charset="utf8mb4",
)

playlog = Table(
    "chuni_score_playlog",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("orderId", Integer),
    Column("sortNumber", Integer),
    Column("placeId", Integer),
    Column("playDate", String(20)),
    Column("userPlayDate", String(20)),
    Column("musicId", Integer),
    Column("level", Integer),
    Column("customId", Integer),
    Column("playedUserId1", Integer),
    Column("playedUserId2", Integer),
    Column("playedUserId3", Integer),
    Column("playedUserName1", String(20)),
    Column("playedUserName2", String(20)),
    Column("playedUserName3", String(20)),
    Column("playedMusicLevel1", Integer),
    Column("playedMusicLevel2", Integer),
    Column("playedMusicLevel3", Integer),
    Column("playedCustom1", Integer),
    Column("playedCustom2", Integer),
    Column("playedCustom3", Integer),
    Column("track", Integer),
    Column("score", Integer),
    Column("rank", Integer),
    Column("maxCombo", Integer),
    Column("maxChain", Integer),
    Column("rateTap", Integer),
    Column("rateHold", Integer),
    Column("rateSlide", Integer),
    Column("rateAir", Integer),
    Column("rateFlick", Integer),
    Column("judgeGuilty", Integer),
    Column("judgeAttack", Integer),
    Column("judgeJustice", Integer),
    Column("judgeCritical", Integer),
    Column("eventId", Integer),
    Column("playerRating", Integer),
    Column("isNewRecord", Boolean),
    Column("isFullCombo", Boolean),
    Column("fullChainKind", Integer),
    Column("isAllJustice", Boolean),
    Column("isContinue", Boolean),
    Column("isFreeToPlay", Boolean),
    Column("characterId", Integer),
    Column("skillId", Integer),
    Column("playKind", Integer),
    Column("isClear", Integer),
    Column("skillLevel", Integer),
    Column("skillEffect", Integer),
    Column("placeName", String(255)),
    Column("isMaimai", Boolean),
    Column("commonId", Integer),
    Column("charaIllustId", Integer),
    Column("romVersion", String(255)),
    Column("judgeHeaven", Integer),
    Column("regionId", Integer),
    Column("machineType", Integer),
    Column("ticketId", Integer),
    mysql_charset="utf8mb4"
)


class ChuniScoreData(BaseData):
    async def get_courses(self, aime_id: int) -> Optional[Row]:
        sql = select(course).where(course.c.user == aime_id)

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

    async def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]:
        course_data["user"] = aime_id
        course_data = self.fix_bools(course_data)

        sql = insert(course).values(**course_data)
        conflict = sql.on_duplicate_key_update(**course_data)

        result = await self.execute(conflict)
        if result is None:
            return None
        return result.lastrowid

    async def get_scores(self, aime_id: int) -> Optional[Row]:
        sql = select(best_score).where(best_score.c.user == aime_id)

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

    async def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]:
        score_data["user"] = aime_id
        score_data = self.fix_bools(score_data)

        sql = insert(best_score).values(**score_data)
        conflict = sql.on_duplicate_key_update(**score_data)

        result = await self.execute(conflict)
        if result is None:
            return None
        return result.lastrowid

    async def get_playlogs(self, aime_id: int) -> Optional[Row]:
        sql = select(playlog).where(playlog.c.user == aime_id)

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

    async def get_playlogs_limited(self, aime_id: int, index: int, count: int) -> Optional[Row]:
        sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.id.desc()).limit(count).offset(index * count)

        result = await self.execute(sql)
        if result is None:
            self.logger.warning(f" aime_id {aime_id} has no playlog ")
            return None
        return result.fetchall()

    async def get_user_playlogs_count(self, aime_id: int) -> Optional[Row]:
        sql = select(func.count()).where(playlog.c.user == aime_id)
        result = await self.execute(sql)
        if result is None:
            self.logger.warning(f" aime_id {aime_id} has no playlog ")
            return None
        return result.scalar()

    async def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]:
        # Calculate the ROM version that should be inserted into the DB, based on the version of the ggame being inserted
        # We only need from Version 10 (Plost) and back, as newer versions include romVersion in their upsert
        # This matters both for gameRankings, as well as a future DB update to keep version data separate
        romVer = {
            10: "1.50.0",
            9: "1.45.0",
            8: "1.40.0",
            7: "1.35.0",
            6: "1.30.0",
            5: "1.25.0",
            4: "1.20.0",
            3: "1.15.0",
            2: "1.10.0",
            1: "1.05.0",
            0: "1.00.0"
        }

        playlog_data["user"] = aime_id
        playlog_data = self.fix_bools(playlog_data)
        if "romVersion" not in playlog_data:
            playlog_data["romVersion"] = romVer.get(version, "1.00.0")

        sql = insert(playlog).values(**playlog_data)
        conflict = sql.on_duplicate_key_update(**playlog_data)

        result = await self.execute(conflict)
        if result is None:
            return None
        return result.lastrowid

    async def get_rankings(self, version: int) -> Optional[List[Dict]]:
        # Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved
        # This prevents tracks that are not accessible in your version from counting towards the 10 results
        romVer = {
            15: "2.20%",
            14: "2.15%",
            13: "2.10%",
            12: "2.05%",
            11: "2.00%",
            10: "1.50%",
            9: "1.45%",
            8: "1.40%",
            7: "1.35%",
            6: "1.30%",
            5: "1.25%",
            4: "1.20%",
            3: "1.15%",
            2: "1.10%",
            1: "1.05%",
            0: "1.00%"
        }
        sql = select([playlog.c.musicId.label('id'), func.count(playlog.c.musicId).label('point')]).where((playlog.c.level != 4) & (playlog.c.romVersion.like(romVer.get(version, "%")))).group_by(playlog.c.musicId).order_by(func.count(playlog.c.musicId).desc()).limit(10)
        result = await self.execute(sql)

        if result is None:
            return None

        rows = result.fetchall()
        return [dict(row) for row in rows]

    async def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]:
        sql = select(best_score).where(best_score.c.user == rival_id)

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