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

from core.data.schema import BaseData, metadata
from ..handlers.helpers import PlayType

profile = Table(
    "wacca_profile",
    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("username", String(8), nullable=False),
    Column("xp", Integer, server_default="0"),
    Column("wp", Integer, server_default="0"),
    Column("wp_total", Integer, server_default="0"),
    Column("wp_spent", Integer, server_default="0"),
    Column("dan_type", Integer, server_default="0"),
    Column("dan_level", Integer, server_default="0"),
    Column("title_0", Integer, server_default="0"),
    Column("title_1", Integer, server_default="0"),
    Column("title_2", Integer, server_default="0"),
    Column("rating", Integer, server_default="0"),
    Column("vip_expire_time", TIMESTAMP),
    Column("always_vip", Boolean, server_default="0"),
    Column("login_count", Integer, server_default="0"),
    Column("login_count_consec", Integer, server_default="0"),
    Column("login_count_days", Integer, server_default="0"),
    Column("login_count_days_consec", Integer, server_default="0"),
    Column("login_count_today", Integer, server_default="0"),
    Column("playcount_single", Integer, server_default="0"),
    Column("playcount_multi_vs", Integer, server_default="0"),
    Column("playcount_multi_coop", Integer, server_default="0"),
    Column("playcount_stageup", Integer, server_default="0"),
    Column("playcount_time_free", Integer, server_default="0"),
    Column("friend_view_1", Integer),
    Column("friend_view_2", Integer),
    Column("friend_view_3", Integer),
    Column("last_game_ver", String(50)),
    Column("last_song_id", Integer, server_default="0"),
    Column("last_song_difficulty", Integer, server_default="0"),
    Column("last_folder_order", Integer, server_default="0"),
    Column("last_folder_id", Integer, server_default="0"),
    Column("last_song_order", Integer, server_default="0"),
    Column("last_login_date", TIMESTAMP, server_default=func.now()),
    Column("gate_tutorial_flags", JSON),
    UniqueConstraint("user", "version", name="wacca_profile_uk"),
    mysql_charset="utf8mb4",
)

option = Table(
    "wacca_option",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("opt_id", Integer, nullable=False),
    Column("value", Integer, nullable=False),
    UniqueConstraint("user", "opt_id", name="wacca_option_uk"),
)

bingo = Table(
    "wacca_bingo",
    metadata,
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        primary_key=True,
        nullable=False,
    ),
    Column("page_number", Integer, nullable=False),
    Column("page_progress", JSON, nullable=False),
    UniqueConstraint("user", "page_number", name="wacca_bingo_uk"),
    mysql_charset="utf8mb4",
)

friend = Table(
    "wacca_friend",
    metadata,
    Column(
        "profile_sender",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column(
        "profile_reciever",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("is_accepted", Boolean, server_default="0"),
    PrimaryKeyConstraint("profile_sender", "profile_reciever", name="arcade_owner_pk"),
    mysql_charset="utf8mb4",
)

favorite = Table(
    "wacca_favorite_song",
    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, nullable=False),
    UniqueConstraint("user", "song_id", name="wacca_favorite_song_uk"),
    mysql_charset="utf8mb4",
)

gate = Table(
    "wacca_gate",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("gate_id", Integer, nullable=False),
    Column("page", Integer, nullable=False, server_default="0"),
    Column("progress", Integer, nullable=False, server_default="0"),
    Column("loops", Integer, nullable=False, server_default="0"),
    Column("last_used", TIMESTAMP, nullable=False, server_default=func.now()),
    Column("mission_flag", Integer, nullable=False, server_default="0"),
    Column("total_points", Integer, nullable=False, server_default="0"),
    UniqueConstraint("user", "gate_id", name="wacca_gate_uk"),
)


class WaccaProfileData(BaseData):
    async def create_profile(
        self, aime_id: int, username: str, version: int
    ) -> Optional[int]:
        """
        Given a game version, aime id, and username, create a profile and return it's ID
        """
        sql = insert(profile).values(user=aime_id, username=username, version=version)

        conflict = sql.on_duplicate_key_update(username=sql.inserted.username)

        result = await self.execute(conflict)
        if result is None:
            self.logger.error(
                f"{__name__} Failed to insert wacca profile! aime id: {aime_id} username: {username}"
            )
            return None
        return result.lastrowid

    async def update_profile_playtype(
        self, profile_id: int, play_type: int, game_version: str
    ) -> None:
        sql = profile.update(profile.c.id == profile_id).values(
            playcount_single=profile.c.playcount_single + 1
            if play_type == PlayType.PlayTypeSingle.value
            else profile.c.playcount_single,
            playcount_multi_vs=profile.c.playcount_multi_vs + 1
            if play_type == PlayType.PlayTypeVs.value
            else profile.c.playcount_multi_vs,
            playcount_multi_coop=profile.c.playcount_multi_coop + 1
            if play_type == PlayType.PlayTypeCoop.value
            else profile.c.playcount_multi_coop,
            playcount_stageup=profile.c.playcount_stageup + 1
            if play_type == PlayType.PlayTypeStageup.value
            else profile.c.playcount_stageup,
            playcount_time_free=profile.c.playcount_time_free + 1
            if play_type == PlayType.PlayTypeTimeFree.value
            else profile.c.playcount_time_free,
            last_game_ver=game_version,
        )

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"update_profile: failed to update profile! profile: {profile_id}"
            )
        return None

    async def update_profile_lastplayed(
        self,
        profile_id: int,
        last_song_id: int,
        last_song_difficulty: int,
        last_folder_order: int,
        last_folder_id: int,
        last_song_order: int,
    ) -> None:
        sql = profile.update(profile.c.id == profile_id).values(
            last_song_id=last_song_id,
            last_song_difficulty=last_song_difficulty,
            last_folder_order=last_folder_order,
            last_folder_id=last_folder_id,
            last_song_order=last_song_order,
        )
        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"update_profile_lastplayed: failed to update profile! profile: {profile_id}"
            )
        return None

    async def update_profile_dan(
        self, profile_id: int, dan_level: int, dan_type: int
    ) -> Optional[int]:
        sql = profile.update(profile.c.id == profile_id).values(
            dan_level=dan_level, dan_type=dan_type
        )

        result = await self.execute(sql)
        if result is None:
            self.logger.warning(
                f"update_profile_dan: Failed to update! profile {profile_id}"
            )
            return None
        return result.lastrowid

    async def get_profile(self, profile_id: int = 0, aime_id: int = None) -> Optional[Row]:
        """
        Given a game version and either a profile or aime id, return the profile
        """
        if aime_id is not None:
            sql = profile.select(profile.c.user == aime_id)
        elif profile_id > 0:
            sql = profile.select(profile.c.id == profile_id)
        else:
            self.logger.error(
                f"get_profile: Bad arguments!! profile_id {profile_id} aime_id {aime_id}"
            )
            return None

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

    async def get_options(self, user_id: int, option_id: int = None) -> Optional[List[Row]]:
        """
        Get a specific user option for a profile, or all of them if none specified
        """
        sql = option.select(
            and_(
                option.c.user == user_id,
                option.c.opt_id == option_id if option_id is not None else True,
            )
        )

        result = await self.execute(sql)
        if result is None:
            return None
        if option_id is not None:
            return result.fetchone()
        else:
            return result.fetchall()

    async def update_option(self, user_id: int, option_id: int, value: int) -> Optional[int]:
        sql = insert(option).values(user=user_id, opt_id=option_id, value=value)

        conflict = sql.on_duplicate_key_update(value=value)

        result = await self.execute(conflict)
        if result is None:
            self.logger.error(
                f"{__name__} failed to insert option! profile: {user_id}, option: {option_id}, value: {value}"
            )
            return None

        return result.lastrowid

    async def add_favorite_song(self, user_id: int, song_id: int) -> Optional[int]:
        sql = favorite.insert().values(user=user_id, song_id=song_id)

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

    async def remove_favorite_song(self, user_id: int, song_id: int) -> None:
        sql = favorite.delete(
            and_(favorite.c.user == user_id, favorite.c.song_id == song_id)
        )

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

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

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

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

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

    async def update_gate(
        self,
        user_id: int,
        gate_id: int,
        page: int,
        progress: int,
        loop: int,
        mission_flag: int,
        total_points: int,
    ) -> Optional[int]:
        sql = insert(gate).values(
            user=user_id,
            gate_id=gate_id,
            page=page,
            progress=progress,
            loops=loop,
            mission_flag=mission_flag,
            total_points=total_points,
        )

        conflict = sql.on_duplicate_key_update(
            page=sql.inserted.page,
            progress=sql.inserted.progress,
            loops=sql.inserted.loops,
            mission_flag=sql.inserted.mission_flag,
            total_points=sql.inserted.total_points,
        )

        result = await self.execute(conflict)
        if result is None:
            self.logger.error(
                f"{__name__} failed to update gate! user: {user_id}, gate_id: {gate_id}"
            )
            return None
        return result.lastrowid

    async def get_friends(self, user_id: int) -> Optional[List[Row]]:
        sql = friend.select(friend.c.profile_sender == user_id)

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

    async def profile_to_aime_user(self, profile_id: int) -> Optional[int]:
        sql = select(profile.c.user).where(profile.c.id == profile_id)

        result = await self.execute(sql)
        if result is None:
            self.logger.info(
                f"profile_to_aime_user: No user found for profile {profile_id}"
            )
            return None

        this_profile = result.fetchone()
        if this_profile is None:
            self.logger.info(
                f"profile_to_aime_user: No user found for profile {profile_id}"
            )
            return None

        return this_profile["user"]

    async def session_login(
        self, profile_id: int, is_new_day: bool, is_consec_day: bool
    ) -> None:
        # TODO: Reset consec days counter
        sql = profile.update(profile.c.id == profile_id).values(
            login_count=profile.c.login_count + 1,
            login_count_consec=profile.c.login_count_consec + 1,
            login_count_days=profile.c.login_count_days + 1
            if is_new_day
            else profile.c.login_count_days,
            login_count_days_consec=profile.c.login_count_days_consec + 1
            if is_new_day and is_consec_day
            else profile.c.login_count_days_consec,
            login_count_today=1 if is_new_day else profile.c.login_count_today + 1,
            last_login_date=func.now(),
        )

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"session_login: failed to update profile! profile: {profile_id}"
            )
        return None

    async def session_logout(self, profile_id: int) -> None:
        sql = profile.update(profile.c.id == id).values(login_count_consec=0)

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"{__name__} failed to update profile! profile: {profile_id}"
            )
        return None

    async def add_xp(self, profile_id: int, xp: int) -> None:
        sql = profile.update(profile.c.id == profile_id).values(xp=profile.c.xp + xp)

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"add_xp: Failed to update profile! profile_id {profile_id} xp {xp}"
            )
        return None

    async def add_wp(self, profile_id: int, wp: int) -> None:
        sql = profile.update(profile.c.id == profile_id).values(
            wp=profile.c.wp + wp,
            wp_total=profile.c.wp_total + wp,
        )

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"add_wp: Failed to update profile! profile_id {profile_id} wp {wp}"
            )
        return None

    async def spend_wp(self, profile_id: int, wp: int) -> None:
        sql = profile.update(profile.c.id == profile_id).values(
            wp=profile.c.wp - wp,
            wp_spent=profile.c.wp_spent + wp,
        )

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"spend_wp: Failed to update profile! profile_id {profile_id} wp {wp}"
            )
        return None

    async def activate_vip(self, profile_id: int, expire_time) -> None:
        sql = profile.update(profile.c.id == profile_id).values(
            vip_expire_time=expire_time
        )

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"activate_vip: Failed to update profile! profile_id {profile_id} expire_time {expire_time}"
            )
        return None

    async def update_user_rating(self, profile_id: int, new_rating: int) -> None:
        sql = profile.update(profile.c.id == profile_id).values(rating=new_rating)

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"update_user_rating: Failed to update profile! profile_id {profile_id} new_rating {new_rating}"
            )
        return None

    async def update_bingo(self, aime_id: int, page: int, progress: int) -> Optional[int]:
        sql = insert(bingo).values(
            user=aime_id, page_number=page, page_progress=progress
        )

        conflict = sql.on_duplicate_key_update(page_number=page, page_progress=progress)

        result = await self.execute(conflict)
        if result is None:
            self.logger.error(f"put_bingo: failed to update! aime_id: {aime_id}")
            return None
        return result.lastrowid

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

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

    async def get_bingo_page(self, aime_id: int, page: Dict) -> Optional[List[Row]]:
        sql = select(bingo).where(
            and_(bingo.c.user == aime_id, bingo.c.page_number == page)
        )

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

    async def update_vip_time(self, profile_id: int, time_left) -> None:
        sql = profile.update(profile.c.id == profile_id).values(
            vip_expire_time=time_left
        )

        result = await self.execute(sql)
        if result is None:
            self.logger.error(f"Failed to update VIP time for profile {profile_id}")

    async def update_tutorial_flags(self, profile_id: int, flags: Dict) -> None:
        sql = profile.update(profile.c.id == profile_id).values(
            gate_tutorial_flags=flags
        )

        result = await self.execute(sql)
        if result is None:
            self.logger.error(
                f"Failed to update tutorial flags for profile {profile_id}"
            )
