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.sql import func, select, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert

from core.data.schema import BaseData, metadata
from core.config import CoreConfig

# Cammel case column names technically don't follow the other games but
# it makes it way easier on me to not fuck with what the games has
profile = Table(
    "ongeki_profile_data",
    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("userName", String(8)),
    Column("level", Integer),
    Column("reincarnationNum", Integer),
    Column("exp", Integer),
    Column("point", Integer),
    Column("totalPoint", Integer),
    Column("playCount", Integer),
    Column("jewelCount", Integer),
    Column("totalJewelCount", Integer),
    Column("medalCount", Integer),
    Column("playerRating", Integer),
    Column("highestRating", Integer),
    Column("battlePoint", Integer),
    Column("nameplateId", Integer),
    Column("trophyId", Integer),
    Column("cardId", Integer),
    Column("characterId", Integer),
    Column("characterVoiceNo", Integer),
    Column("tabSetting", Integer),
    Column("tabSortSetting", Integer),
    Column("cardCategorySetting", Integer),
    Column("cardSortSetting", Integer),
    Column("playedTutorialBit", Integer),
    Column("firstTutorialCancelNum", Integer),
    Column("sumTechHighScore", BigInteger),
    Column("sumTechBasicHighScore", BigInteger),
    Column("sumTechAdvancedHighScore", BigInteger),
    Column("sumTechExpertHighScore", BigInteger),
    Column("sumTechMasterHighScore", BigInteger),
    Column("sumTechLunaticHighScore", BigInteger),
    Column("sumBattleHighScore", BigInteger),
    Column("sumBattleBasicHighScore", BigInteger),
    Column("sumBattleAdvancedHighScore", BigInteger),
    Column("sumBattleExpertHighScore", BigInteger),
    Column("sumBattleMasterHighScore", BigInteger),
    Column("sumBattleLunaticHighScore", BigInteger),
    Column("eventWatchedDate", String(255)),
    Column("cmEventWatchedDate", String(255)),
    Column("firstGameId", String(8)),
    Column("firstRomVersion", String(8)),
    Column("firstDataVersion", String(8)),
    Column("firstPlayDate", String(255)),
    Column("lastGameId", String(8)),
    Column("lastRomVersion", String(8)),
    Column("lastDataVersion", String(8)),
    Column("compatibleCmVersion", String(8)),
    Column("lastPlayDate", String(255)),
    Column("lastPlaceId", Integer),
    Column("lastPlaceName", String(255)),
    Column("lastRegionId", Integer),
    Column("lastRegionName", String(255)),
    Column("lastAllNetId", Integer),
    Column("lastClientId", String(16)),
    Column("lastUsedDeckId", Integer),
    Column("lastPlayMusicLevel", Integer),
    Column("banStatus", Integer, server_default="0"),
    Column("rivalScoreCategorySetting", Integer, server_default="0"),
    Column("overDamageBattlePoint", Integer, server_default="0"),
    Column("bestBattlePoint", Integer, server_default="0"),
    Column("lastEmoneyBrand", Integer, server_default="0"),
    Column("lastEmoneyCredit", Integer, server_default="0"),
    Column("isDialogWatchedSuggestMemory", Boolean, server_default="0"),
    UniqueConstraint("user", "version", name="ongeki_profile_profile_uk"),
    mysql_charset="utf8mb4",
)

# No point setting defaults since the game sends everything on profile creation anyway
option = Table(
    "ongeki_profile_option",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("optionSet", Integer),
    Column("speed", Integer),
    Column("mirror", Integer),
    Column("judgeTiming", Integer),
    Column("judgeAdjustment", Integer),
    Column("abort", Integer),
    Column("tapSound", Integer),
    Column("volGuide", Integer),
    Column("volAll", Integer),
    Column("volTap", Integer),
    Column("volCrTap", Integer),
    Column("volHold", Integer),
    Column("volSide", Integer),
    Column("volFlick", Integer),
    Column("volBell", Integer),
    Column("volEnemy", Integer),
    Column("volSkill", Integer),
    Column("volDamage", Integer),
    Column("colorField", Integer),
    Column("colorLaneBright", Integer),
    Column("colorLane", Integer),
    Column("colorSide", Integer),
    Column("effectDamage", Integer),
    Column("effectPos", Integer),
    Column("judgeDisp", Integer),
    Column("judgePos", Integer),
    Column("judgeBreak", Integer),
    Column("judgeHit", Integer),
    Column("platinumBreakDisp", Integer),
    Column("judgeCriticalBreak", Integer),
    Column("matching", Integer),
    Column("dispPlayerLv", Integer),
    Column("dispRating", Integer),
    Column("dispBP", Integer),
    Column("headphone", Integer),
    Column("stealthField", Integer),
    Column("colorWallBright", Integer),
    UniqueConstraint("user", name="ongeki_profile_option_uk"),
    mysql_charset="utf8mb4",
)

activity = Table(
    "ongeki_profile_activity",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("kind", Integer),
    Column("activityId", Integer),
    Column("sortNumber", Integer),
    Column("param1", Integer),
    Column("param2", Integer),
    Column("param3", Integer),
    Column("param4", Integer),
    UniqueConstraint("user", "kind", "activityId", name="ongeki_profile_activity_uk"),
    mysql_charset="utf8mb4",
)

recent_rating = Table(
    "ongeki_profile_recent_rating",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("recentRating", JSON),
    UniqueConstraint("user", name="ongeki_profile_recent_rating_uk"),
    mysql_charset="utf8mb4",
)

rating_log = Table(
    "ongeki_profile_rating_log",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("highestRating", Integer),
    Column("dataVersion", String(10)),
    UniqueConstraint("user", "dataVersion", name="ongeki_profile_rating_log_uk"),
    mysql_charset="utf8mb4",
)

region = Table(
    "ongeki_profile_region",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column(
        "user",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
        nullable=False,
    ),
    Column("regionId", Integer),
    Column("playCount", Integer),
    Column("created", String(25)),
    UniqueConstraint("user", "regionId", name="ongeki_profile_region_uk"),
    mysql_charset="utf8mb4",
)

training_room = Table(
    "ongeki_profile_training_room",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("roomId", Integer),
    Column("authKey", Integer),
    Column("cardId", Integer),
    Column("valueDate", String(25)),
    UniqueConstraint("user", "roomId", name="ongeki_profile_training_room_uk"),
    mysql_charset="utf8mb4",
)

kop = Table(
    "ongeki_profile_kop",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column("authKey", Integer),
    Column("kopId", Integer),
    Column("areaId", Integer),
    Column("totalTechScore", Integer),
    Column("totalPlatinumScore", Integer),
    Column("techRecordDate", String(25)),
    Column("isTotalTechNewRecord", Boolean),
    UniqueConstraint("user", "kopId", name="ongeki_profile_kop_uk"),
    mysql_charset="utf8mb4",
)

rival = Table(
    "ongeki_profile_rival",
    metadata,
    Column("id", Integer, primary_key=True, nullable=False),
    Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
    Column(
        "rivalUserId",
        ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
    ),
    UniqueConstraint("user", "rivalUserId", name="ongeki_profile_rival_uk"),
    mysql_charset="utf8mb4",
)

rating = Table(
    "ongeki_profile_rating",
    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("type", String(255), nullable=False),
    Column("index", Integer, nullable=False),
    Column("musicId", Integer),
    Column("difficultId", Integer),
    Column("romVersionCode", Integer),
    Column("score", Integer),
    UniqueConstraint("user", "version", "type", "index", name="ongeki_profile_rating_best_uk"),
    mysql_charset="utf8mb4",
)


class OngekiProfileData(BaseData):
    def __init__(self, cfg: CoreConfig, conn: Connection) -> None:
        super().__init__(cfg, conn)
        self.date_time_format_ext = (
            "%Y-%m-%d %H:%M:%S.%f"  # needs to be lopped off at [:-5]
        )
        self.date_time_format_short = "%Y-%m-%d"

    async def get_profile_name(self, aime_id: int, version: int) -> Optional[str]:
        sql = select(profile.c.userName).where(
            and_(profile.c.user == aime_id, profile.c.version == version)
        )

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

        row = result.fetchone()
        if row is None:
            return None

        return row["userName"]
    
    async def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]:
        sql = (
            select([profile, option])
            .join(option, profile.c.user == option.c.user)
            .filter(and_(profile.c.user == aime_id, profile.c.version == version))
        )

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

    async def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]:
        sql = select(profile).where(
            and_(
                profile.c.user == aime_id,
                profile.c.version == version,
            )
        )

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

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

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

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

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

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

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

    async def get_profile_activity(
        self, aime_id: int, kind: int = None
    ) -> Optional[List[Row]]:
        sql = select(activity).where(
            and_(
                activity.c.user == aime_id,
                (activity.c.kind == kind) if kind is not None else True,
            )
        )

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

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

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

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

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

    async def put_profile_data(self, aime_id: int, version: int, data: Dict) -> Optional[int]:
        data["user"] = aime_id
        data["version"] = version
        data.pop("accessCode")

        sql = insert(profile).values(**data)
        conflict = sql.on_duplicate_key_update(**data)
        result = await self.execute(conflict)

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

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

        sql = insert(option).values(**options_data)
        conflict = sql.on_duplicate_key_update(**options_data)
        result = await self.execute(conflict)

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

    async def put_profile_recent_rating(
        self, aime_id: int, recent_rating_data: List[Dict]
    ) -> Optional[int]:
        sql = insert(recent_rating).values(
            user=aime_id, recentRating=recent_rating_data
        )

        conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data)

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

    async def put_profile_bp_list(
        self, aime_id: int, bp_base_list: List[Dict]
    ) -> Optional[int]:
        pass

    async def put_profile_rating_log(
        self, aime_id: int, data_version: str, highest_rating: int
    ) -> Optional[int]:
        sql = insert(rating_log).values(
            user=aime_id, dataVersion=data_version, highestRating=highest_rating
        )

        conflict = sql.on_duplicate_key_update(highestRating=highest_rating)

        result = await self.execute(conflict)
        if result is None:
            self.logger.warning(
                f"put_profile_rating_log: failed to update rating log! aime_id {aime_id} data_version {data_version} highest_rating {highest_rating}"
            )
            return None
        return result.lastrowid

    async def put_profile_activity(
        self,
        aime_id: int,
        kind: int,
        activity_id: int,
        sort_num: int,
        p1: int,
        p2: int,
        p3: int,
        p4: int,
    ) -> Optional[int]:
        sql = insert(activity).values(
            user=aime_id,
            kind=kind,
            activityId=activity_id,
            sortNumber=sort_num,
            param1=p1,
            param2=p2,
            param3=p3,
            param4=p4,
        )

        conflict = sql.on_duplicate_key_update(
            sortNumber=sort_num, param1=p1, param2=p2, param3=p3, param4=p4
        )

        result = await self.execute(conflict)
        if result is None:
            self.logger.warning(
                f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}"
            )
            return None
        return result.lastrowid

    async def put_profile_region(self, aime_id: int, region: int, date: str) -> Optional[int]:
        sql = insert(activity).values(
            user=aime_id, region=region, playCount=1, created=date
        )

        conflict = sql.on_duplicate_key_update(
            playCount=activity.c.playCount + 1,
        )

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

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

        sql = insert(training_room).values(**room_detail)
        conflict = sql.on_duplicate_key_update(**room_detail)
        result = await self.execute(conflict)

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

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

        sql = insert(kop).values(**kop_data)
        conflict = sql.on_duplicate_key_update(**kop_data)
        result = await self.execute(conflict)

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

    async def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]:
        sql = insert(rival).values(user=aime_id, rivalUserId=rival_id)

        conflict = sql.on_duplicate_key_update(rivalUserId=rival_id)

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

    async def delete_rival(self, aime_id: int, rival_id: int) -> Optional[int]:
        sql = delete(rival).where(rival.c.user==aime_id, rival.c.rivalUserId==rival_id)
        result = await self.execute(sql)
        if result is None:
            self.logger.error(f"delete_rival: failed to delete! aime_id: {aime_id}, rival_id: {rival_id}")
        else:
            return result.rowcount
    
    async def put_profile_rating(
        self,
        aime_id: int,
        version: int,
        rating_type: str,
        rating_data: List[Dict],
    ):
        inserted_values = [
            {"user": aime_id, "version": version, "type": rating_type, "index": i, **x}
            for (i, x) in enumerate(rating_data)
        ]
        sql = insert(rating).values(inserted_values)
        update_dict = {x.name: x for x in sql.inserted if x.name != "id"}
        sql = sql.on_duplicate_key_update(**update_dict)
        result = await self.execute(sql)

        if result is None:
            self.logger.warn(
                f"put_profile_rating_{rating_type}: Could not insert rating entries, aime_id: {aime_id}",
            )
            return
        
        return result.lastrowid
