import sqlalchemy
from sqlalchemy import Table, Column, Integer, String, DateTime, JSON, ForeignKey, Index
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import select, update, insert
import json
import os

START_COIN = 10

import os
import databases
from datetime import datetime, timedelta

DB_NAME = "player.db"
DB_PATH = os.path.join(os.getcwd(), DB_NAME)
DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}"

OLD_DB_NAME = "player_d.db"
OLD_DB_PATH = os.path.join(os.getcwd(), OLD_DB_NAME)
OLD_DATABASE_URL = f"sqlite+aiosqlite:///{OLD_DB_PATH}"

old_database = databases.Database(OLD_DATABASE_URL)
old_metadata = sqlalchemy.MetaData()

player_database = databases.Database(DATABASE_URL)
player_metadata = sqlalchemy.MetaData()

#----------------------- Old Table definitions -----------------------#

user = Table(
    "user",
    old_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("username", String(20), unique=True, nullable=False),
    Column("password_hash", String(255), nullable=False),
    Column("device_id", String(512)),
    Column("data", String, nullable=True),
    Column("save_id", String, nullable=True),
    Column("crc", Integer, nullable=True),
    Column("timestamp", DateTime, default=None),
    Column("coin_mp", Integer, default=1)
)

daily_reward = Table(
    "daily_reward",
    old_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("device_id", String(512)),
    Column("timestamp", DateTime, default=datetime.utcnow),
    Column("my_stage", JSON),
    Column("my_avatar", JSON),
    Column("item", JSON),
    Column("day", Integer),
    Column("coin", Integer),
    Column("lvl", Integer),
    Column("title", Integer),
    Column("avatar", Integer),
)

result = Table(
    "result",
    old_metadata,
    Column("rid", Integer, primary_key=True, autoincrement=True),
    Column("vid", String(512), nullable=False),
    Column("tid", String(512), nullable=False),
    Column("sid", Integer, nullable=False),
    Column("stts", String(64)),
    Column("id", Integer),
    Column("mode", Integer),
    Column("avatar", Integer),
    Column("score", Integer),
    Column("high_score", String(128)),
    Column("play_rslt", String(128)),
    Column("item", Integer),
    Column("os", String(16)),
    Column("os_ver", String(16)),
    Column("ver", String(16)),
    Column("mike", Integer),
)

whitelist = Table(
    "whitelist",
    old_metadata,
    Column("id", String(512), primary_key=True)
)
blacklist = Table(
    "blacklist",
    old_metadata,
    Column("id", String(512), primary_key=True),
    Column("reason", String(256))
)

bind = Table(
    "bind",
    old_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")),
    Column("bind_acc", String(256), unique=True, nullable=False),
    Column("bind_code", String(16), nullable=False),
    Column("is_verified", Integer, default=0),
    Column("auth_token", String(256), nullable=True),
    Column("bind_date", DateTime, default=datetime.utcnow)
)

logs = Table(
    "logs",
    old_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")),
    Column("filename", String(256)),
    Column("filesize", Integer),
    Column("timestamp", DateTime, default=datetime.utcnow)
)

device_list = Table(
    "device_list",
    old_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")),
    Column("device_id", String(512), unique=True, nullable=False),
    Column("last_login", DateTime, default=datetime.utcnow)
)

#----------------------- End of old Table definitions -----------------------#

#----------------------- New Table definitions -----------------------#

accounts = Table(
    "accounts",
    player_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("username", String(20), unique=True, nullable=False),
    Column("password_hash", String(255), nullable=False),
    Column("save_crc", String(24), nullable=True),
    Column("save_timestamp", DateTime, nullable=True),
    Column("save_id", String(24), nullable=True),
    Column("coin_mp", Integer, default=0),
    Column("title", Integer, default=1),
    Column("avatar", Integer, default=1),
    Column("mobile_delta", Integer, default=0),
    Column("arcade_delta", Integer, default=0),
    Column("total_delta", Integer, default=0),
    Column("created_at", DateTime, default=datetime.utcnow),
    Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)

devices = Table(
    "devices",
    player_metadata,
    Column("device_id", String(64), primary_key=True),
    Column("user_id", Integer, ForeignKey("accounts.id")),
    Column("my_stage", JSON, default=[]),
    Column("my_avatar", JSON, default=[]),
    Column("item", JSON, default=[]),
    Column("daily_day", Integer, default=0),
    Column("daily_timestamp", DateTime, default=datetime.min),
    Column("coin", Integer, default=START_COIN),
    Column("lvl", Integer, default=1),
    Column("title", Integer, default=1),
    Column("avatar", Integer, default=1),
    Column("created_at", DateTime, default=datetime.utcnow),
    Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow),
    Column("last_login_at", DateTime, default=None),
     Column("bind_token", String(64), unique=True),
)

results = Table(
    "results",
    player_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("device_id", String(64), ForeignKey("devices.device_id")),
    Column("user_id", Integer, ForeignKey("accounts.id")),
    Column("stts", JSON, nullable=False),
    Column("song_id", Integer, nullable=False),
    Column("mode", Integer, nullable=False),
    Column("avatar", Integer, nullable=False),
    Column("score", Integer, nullable=False),
    Column("high_score", JSON, nullable=False),
    Column("play_rslt", JSON, nullable=False),
    Column("item", Integer, nullable=False),
    Column("os", String(8), nullable=False),
    Column("os_ver", String(16), nullable=False),
    Column("ver", String(8), nullable=False),
    Column("created_at", DateTime, default=datetime.utcnow)
)

Index(
    "idx_results_song_mode_score",
    results.c.song_id,
    results.c.mode,
    results.c.score.desc(),
)

webs = Table(
    "webs",
    player_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("user_id", Integer, ForeignKey("accounts.id")),
    Column("permission", Integer, default=1),
    Column("web_token", String(128), unique=True, nullable=False),
    Column("last_save_export", Integer, nullable=True),
    Column("created_at", DateTime, default=datetime.utcnow),
    Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)

batch_tokens = Table(
    "batch_tokens",
    player_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("batch_token", String(128), unique=True, nullable=False),
    Column("expire_at", DateTime, nullable=False),
    Column("uses_left", Integer, default=1),
    Column("auth_id", String(64), nullable=False),
    Column("created_at", DateTime, default=datetime.utcnow),
    Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)

whitelists = Table(
    "whitelists",
    player_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("device_id", String(64), ForeignKey("devices.device_id")),
)

blacklists = Table(
    "blacklists",
    player_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("ban_terms", String(128), unique=True, nullable=False),
    Column("reason", String(255), nullable=True)
)

binds = Table(
    "binds",
    player_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("user_id", Integer, ForeignKey("accounts.id")),
    Column("bind_account", String(128), unique=True, nullable=False),
    Column("bind_code", String(6), nullable=False),
    Column("is_verified", Integer, default=0),
    Column("bind_date", DateTime, default=datetime.utcnow)
)

logs = Table(
    "logs",
    player_metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("user_id", Integer, ForeignKey("accounts.id")),
    Column("filename", String(255), nullable=False),
    Column("filesize", Integer, nullable=False),
    Column("timestamp", DateTime, default=datetime.utcnow)
)

#----------------------- End of new Table definitions -----------------------#

async def init_db():
    if not os.path.exists(DB_PATH):
        print("[DB] Creating new database:", DB_PATH)

    if not os.path.exists(OLD_DB_PATH):
        print("[DB] Old db must exist as player_d.db")
    
    old_engine = create_async_engine(OLD_DATABASE_URL, echo=False)
    async with old_engine.begin() as conn:
        await conn.run_sync(old_metadata.create_all)
    await old_engine.dispose()
    
    engine = create_async_engine(DATABASE_URL, echo=False)
    
    async with engine.begin() as conn:
        await conn.run_sync(player_metadata.create_all)
    
    await engine.dispose()
    print("[DB] Database initialized successfully.")

async def save_user_data(user_id, user_data):
    save_dir = "save"
    save_file = os.path.join(save_dir, f"{user_id}.dat")

    if not os.path.exists(save_dir):
        os.makedirs(save_dir)
    
    with open(save_file, "w") as file:
        file.write(user_data)

async def convert_db():
    await old_database.connect()
    await player_database.connect()
    
    # Conversion logic goes here
    print("[DB] Starting database conversion...")

    # Convert user -> accounts
    print("[DB] Converting users to accounts...")
    query = select(old_metadata.tables["user"])
    all_users = await old_database.fetch_all(query)
    all_users = [dict(u) for u in all_users]
    for user in all_users:
        user_device_id = user['device_id']

        user_data = user.get('data') if 'data' in user else None
        if user_data:
            user_id = user['id']
            await save_user_data(user_id, user_data)


        user_old_device = await old_database.fetch_one(
            select(old_metadata.tables["daily_reward"]).where(old_metadata.tables["daily_reward"].c.device_id == user_device_id)
        )
        user_old_device = dict(user_old_device) if user_old_device else None

        insert_query = insert(accounts).values(
            username=user['username'],
            password_hash=user['password_hash'],
            save_crc=user['crc'],
            save_timestamp=user['timestamp'],
            save_id=user['save_id'],
            coin_mp=user['coin_mp'],
            title=user_old_device['title'] if user_old_device else 1,
            avatar=user_old_device['avatar'] if user_old_device else 1,
            mobile_delta=0,
            arcade_delta=0,
            total_delta=0,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow()
        )
        await player_database.execute(insert_query)

    # Compute score deltas
    print("[DB] Computing score deltas for accounts...")
    all_new_users = await player_database.fetch_all(select(accounts))
    all_new_users = [dict(u) for u in all_new_users]
    for new_user in all_new_users:
        username = new_user['username']
        old_user_object = await old_database.fetch_one(
            select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.username == username)
        )
        old_user_id = old_user_object['id']
        old_user_results_mobile = await old_database.fetch_all(
            select(old_metadata.tables["result"]).where(
                (old_metadata.tables["result"].c.sid == old_user_id) &
                (old_metadata.tables["result"].c.mode.in_([1, 2, 3]))
            )
        )
        old_user_mobile_sum = sum([int(r['score']) for r in old_user_results_mobile])
        old_user_results_arcade = await old_database.fetch_all(
            select(old_metadata.tables["result"]).where(
                (old_metadata.tables["result"].c.sid == old_user_id) &
                (old_metadata.tables["result"].c.mode.in_([11, 12, 13]))
            )
        )
        old_user_arcade_sum = sum([int(r['score']) for r in old_user_results_arcade])
        old_user_results_total = old_user_mobile_sum + old_user_arcade_sum

        update_query = (
            update(accounts).where(accounts.c.id == new_user['id']).values(
                mobile_delta=old_user_mobile_sum,
                arcade_delta=old_user_arcade_sum,
                total_delta=old_user_results_total
            )
        )
        await player_database.execute(update_query)

    # Convert daily_reward -> devices
    print("[DB] Converting daily rewards to devices...")
    all_devices = await old_database.fetch_all(select(old_metadata.tables["daily_reward"]))
    all_devices = [dict(d) for d in all_devices]
    for device in all_devices:
        connected_old_user = await old_database.fetch_one(
            select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.device_id == device['device_id'])
        )
        new_user_id = None
        old_user_username = connected_old_user['username'] if connected_old_user else None
        if old_user_username:
            connected_user = await player_database.fetch_one(
                select(accounts).where(accounts.c.username == old_user_username)
            )
            new_user_id = connected_user['id'] if connected_user else None

        insert_query = insert(devices).values(
            device_id=device['device_id'],
            user_id=new_user_id,
            my_stage=device['my_stage'],
            my_avatar=device['my_avatar'],
            item=device['item'],
            daily_day=device['day'],
            daily_timestamp=device['timestamp'],
            coin=device['coin'],
            lvl=device['lvl'],
            title=device['title'],
            avatar=device['avatar'],
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            last_login_at=datetime.utcnow()
        )
        await player_database.execute(insert_query)

    # convert result -> results
    print("[DB] Converting results to results...")
    all_results = await old_database.fetch_all(select(old_metadata.tables["result"]))
    all_results = [dict(r) for r in all_results]
    for result in all_results:
        if result['sid'] is not None and result['sid'] != 0:
            # Skip guests, they are no longer ranked
            old_user_object = await old_database.fetch_one(
                select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.id == result['sid'])
            )
            old_user_username = old_user_object['username'] if old_user_object else None
            if old_user_username:
                new_user_object = await player_database.fetch_one(
                    select(accounts).where(accounts.c.username == old_user_username)
                )
                new_user_id = new_user_object['id']
                insert_query = insert(results).values(
                    device_id=result['vid'],
                    user_id=new_user_id,
                    stts=convert_array(result['stts']),
                    song_id=result['id'],
                    mode=result['mode'],
                    avatar=result['avatar'],
                    score=result['score'],
                    high_score=convert_array(result['high_score']),
                    play_rslt=convert_array(result['play_rslt']),
                    item=result['item'],
                    os=result['os'],
                    os_ver=result['os_ver'],
                    ver=result['ver'],
                    created_at=datetime.utcnow(),
                )
                await player_database.execute(insert_query)

    # convert bwlist
    print("[DB] Converting blacklists and whitelists...")
    old_blacklists = await old_database.fetch_all(select(old_metadata.tables["blacklist"]))
    old_blacklists = [dict(b) for b in old_blacklists]

    for bl in old_blacklists:
        insert_query = insert(blacklists).values(
            ban_terms=bl['id'],
            reason=bl['reason']
        )
        await player_database.execute(insert_query)

    old_whitelists = await old_database.fetch_all(select(old_metadata.tables["whitelist"]))
    old_whitelists = [dict(w) for w in old_whitelists]
    for wl in old_whitelists:
        insert_query = insert(whitelists).values(
            device_id=wl['id']
        )
        await player_database.execute(insert_query)

    # convert binds (optional)

    if 'bind' in old_metadata.tables:
        print("[DB] Converting binds...")
        old_binds = await old_database.fetch_all(select(old_metadata.tables["bind"]))
        old_binds = [dict(b) for b in old_binds]
        for b in old_binds:
            insert_query = insert(binds).values(
                user_id=b['user_id'],
                bind_account=b['bind_acc'],
                bind_code=b['bind_code'],
                is_verified=b['is_verified'],
                auth_token=b['auth_token'],
                bind_date=b['bind_date']
            )
            await player_database.execute(insert_query)

    # convert logs (optional)

    if 'logs' in old_metadata.tables:
        print("[DB] Converting logs...")
        old_logs = await old_database.fetch_all(select(old_metadata.tables["logs"]))
        old_logs = [dict(l) for l in old_logs]
        for l in old_logs:
            insert_query = insert(logs).values(
                user_id=l['user_id'],
                filename=l['filename'],
                filesize=l['filesize'],
                timestamp=l['timestamp']
            )
            await player_database.execute(insert_query)

    # tables not converted: batch_tokens (new field added), webs (auto created)
    
    await old_database.disconnect()
    await player_database.disconnect()
    print("[DB] Database conversion completed successfully.")

def convert_array(old_array):
    old_array = "[" + old_array + "]"
    old_array = json.loads(old_array)
    return old_array
        


if __name__ == "__main__":
    import asyncio
    asyncio.run(init_db())
    asyncio.run(convert_db())