from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from typing import Optional, List, Dict, Any

from core.data.schema import BaseData, metadata
from core.data import cached

best_score = Table(
    "wacca_score_best",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("song_id", Integer),
    Column("chart_id", Integer),
    Column("score", Integer),
    Column("play_ct", Integer),
    Column("clear_ct", Integer),
    Column("missless_ct", Integer),
    Column("fullcombo_ct", Integer),
    Column("allmarv_ct", Integer),
    Column("grade_d_ct", Integer),
    Column("grade_c_ct", Integer),
    Column("grade_b_ct", Integer),
    Column("grade_a_ct", Integer),
    Column("grade_aa_ct", Integer),
    Column("grade_aaa_ct", Integer),
    Column("grade_s_ct", Integer),
    Column("grade_ss_ct", Integer),
    Column("grade_sss_ct", Integer),
    Column("grade_master_ct", Integer),
    Column("grade_sp_ct", Integer),
    Column("grade_ssp_ct", Integer),
    Column("grade_sssp_ct", Integer),
    Column("best_combo", Integer),
    Column("lowest_miss_ct", Integer),
    Column("rating", Integer),
    UniqueConstraint("user", "song_id", "chart_id", name="wacca_score_uk"),
    mysql_charset="utf8mb4",
)

playlog = Table(
    "wacca_score_playlog",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("song_id", Integer),
    Column("chart_id", Integer),
    Column("score", Integer),
    Column("clear", Integer),
    Column("grade", Integer),
    Column("max_combo", Integer),
    Column("marv_ct", Integer),
    Column("great_ct", Integer),
    Column("good_ct", Integer),
    Column("miss_ct", Integer),
    Column("fast_ct", Integer),
    Column("late_ct", Integer),
    Column("season", Integer),
    Column("date_scored", TIMESTAMP, server_default=func.now()),
    mysql_charset="utf8mb4",
)

stageup = Table(
    "wacca_score_stageup",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("version", Integer),
    Column("stage_id", Integer),
    Column("clear_status", Integer),
    Column("clear_song_ct", Integer),
    Column("song1_score", Integer),
    Column("song2_score", Integer),
    Column("song3_score", Integer),
    Column("play_ct", Integer, server_default="1"),
    UniqueConstraint("user", "stage_id", name="wacca_score_stageup_uk"),
    mysql_charset="utf8mb4",
)


class WaccaScoreData(BaseData):
    async def put_best_score(
        self,
        user_id: int,
        song_id: int,
        chart_id: int,
        score: int,
        clear: List[int],
        grade: List[int],
        best_combo: int,
        lowest_miss_ct: int,
    ) -> Optional[int]:
        """
        Update the user's best score for a chart
        """
        while len(grade) < 13:
            grade.append(0)

        sql = insert(best_score).values(
            user=user_id,
            song_id=song_id,
            chart_id=chart_id,
            score=score,
            play_ct=clear[0],
            clear_ct=clear[1],
            missless_ct=clear[2],
            fullcombo_ct=clear[3],
            allmarv_ct=clear[4],
            grade_d_ct=grade[0],
            grade_c_ct=grade[1],
            grade_b_ct=grade[2],
            grade_a_ct=grade[3],
            grade_aa_ct=grade[4],
            grade_aaa_ct=grade[5],
            grade_s_ct=grade[6],
            grade_ss_ct=grade[7],
            grade_sss_ct=grade[8],
            grade_master_ct=grade[9],
            grade_sp_ct=grade[10],
            grade_ssp_ct=grade[11],
            grade_sssp_ct=grade[12],
            best_combo=best_combo,
            lowest_miss_ct=lowest_miss_ct,
            rating=0,
        )

        conflict = sql.on_duplicate_key_update(
            score=score,
            play_ct=clear[0],
            clear_ct=clear[1],
            missless_ct=clear[2],
            fullcombo_ct=clear[3],
            allmarv_ct=clear[4],
            grade_d_ct=grade[0],
            grade_c_ct=grade[1],
            grade_b_ct=grade[2],
            grade_a_ct=grade[3],
            grade_aa_ct=grade[4],
            grade_aaa_ct=grade[5],
            grade_s_ct=grade[6],
            grade_ss_ct=grade[7],
            grade_sss_ct=grade[8],
            grade_master_ct=grade[9],
            grade_sp_ct=grade[10],
            grade_ssp_ct=grade[11],
            grade_sssp_ct=grade[12],
            best_combo=best_combo,
            lowest_miss_ct=lowest_miss_ct,
        )

        result = await self.execute(conflict)
        if result is None:
            self.logger.error(
                f"{__name__}: failed to insert best score! profile: {user_id}, song: {song_id}, chart: {chart_id}"
            )
            return None

        return result.lastrowid

    async def put_playlog(
        self,
        user_id: int,
        song_id: int,
        chart_id: int,
        this_score: int,
        clear: int,
        grade: int,
        max_combo: int,
        marv_ct: int,
        great_ct: int,
        good_ct: int,
        miss_ct: int,
        fast_ct: int,
        late_ct: int,
        season: int,
    ) -> Optional[int]:
        """
        Add an entry to the user's play log
        """
        sql = playlog.insert().values(
            user=user_id,
            song_id=song_id,
            chart_id=chart_id,
            score=this_score,
            clear=clear,
            grade=grade,
            max_combo=max_combo,
            marv_ct=marv_ct,
            great_ct=great_ct,
            good_ct=good_ct,
            miss_ct=miss_ct,
            fast_ct=fast_ct,
            late_ct=late_ct,
            season=season,
        )

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {chart_id}"
            )
            return None

        return result.lastrowid

    async def get_best_score(
        self, user_id: int, song_id: int, chart_id: int
    ) -> Optional[Row]:
        sql = best_score.select(
            and_(
                best_score.c.user == user_id,
                best_score.c.song_id == song_id,
                best_score.c.chart_id == chart_id,
            )
        )

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

    async def get_best_scores(self, user_id: int) -> Optional[List[Row]]:
        sql = best_score.select(best_score.c.user == user_id)

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

    async def update_song_rating(
        self, user_id: int, song_id: int, chart_id: int, new_rating: int
    ) -> None:
        sql = best_score.update(
            and_(
                best_score.c.user == user_id,
                best_score.c.song_id == song_id,
                best_score.c.chart_id == chart_id,
            )
        ).values(rating=new_rating)

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"update_song_rating: failed to update rating! user_id: {user_id} song_id: {song_id} chart_id {chart_id} new_rating {new_rating}"
            )
        return None

    async def put_stageup(
        self,
        user_id: int,
        version: int,
        stage_id: int,
        clear_status: int,
        clear_song_ct: int,
        score1: int,
        score2: int,
        score3: int,
    ) -> Optional[int]:
        sql = insert(stageup).values(
            user=user_id,
            version=version,
            stage_id=stage_id,
            clear_status=clear_status,
            clear_song_ct=clear_song_ct,
            song1_score=score1,
            song2_score=score2,
            song3_score=score3,
        )

        conflict = sql.on_duplicate_key_update(
            clear_status=clear_status,
            clear_song_ct=clear_song_ct,
            song1_score=score1,
            song2_score=score2,
            song3_score=score3,
            play_ct=stageup.c.play_ct + 1,
        )

        result = await self.execute(conflict)
        if result is None:
            self.logger.warning(
                f"put_stageup: failed to update! user_id: {user_id} version: {version} stage_id: {stage_id}"
            )
            return None
        return result.lastrowid

    async def get_stageup(self, user_id: int, version: int) -> Optional[List[Row]]:
        sql = select(stageup).where(
            and_(stageup.c.user == user_id, stageup.c.version == version)
        )

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

    async def get_stageup_stage(
        self, user_id: int, version: int, stage_id: int
    ) -> Optional[Row]:
        sql = select(stageup).where(
            and_(
                stageup.c.user == user_id,
                stageup.c.version == version,
                stageup.c.stage_id == stage_id,
            )
        )

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