import base64
import hashlib
from typing import Optional, TypedDict

import bcrypt
from flask import current_app, session
from nh3 import nh3

from src.internals.cache.redis import get_conn
from src.internals.database.database import cached_query, get_cursor, query_db, query_one_db
from src.internals.serializers.account import deserialize_account, serialize_account
from src.lib.artist import get_artist
from src.lib.favorites import add_favorite_artist
from src.lib.security import is_login_rate_limited
from src.types.account import ServiceKey
from src.types.account.account import Account


def load_account(account_id: Optional[str] = None, reload: bool = False):
    if not account_id and "account_id" in session:
        account_id = session["account_id"]
    elif not account_id and "account_id" not in session:
        return None

    key = f"account:{account_id}"
    params = dict(account_id=account_id)
    query = """
        SELECT
            id,
            username,
            created_at,
            role
        FROM
            account
        WHERE
            id = %(account_id)s
    """
    account_dict = cached_query(query, key, params, serialize_account, deserialize_account, reload, True)
    return Account.init_from_dict(account_dict)


class TDSessionKeyImportID(TypedDict):
    key_id: int
    import_id: str


def get_saved_key_import_ids(key_id, reload=False) -> list[TDSessionKeyImportID]:
    key = f"saved_key_import_ids:{key_id}"
    params = dict(key_id=key_id)
    query = """
        SELECT
            key_id,
            import_id
        FROM
            saved_session_key_import_ids
        WHERE
            key_id = %(key_id)s
    """
    return cached_query(query, key, params, reload=reload)


def get_saved_keys(account_id: int, reload: bool = False):
    # lets not cache this for now
    # key = f"saved_keys:{account_id}"
    query = """
        SELECT id, service, discord_channel_ids, added, dead
        FROM saved_session_keys_with_hashes
        WHERE contributor_id = %s
        ORDER BY added DESC
    """
    # results = cached_query(query, key, (account_id,), reload=reload)
    results = query_db(query, (account_id,))
    return [ServiceKey.init_from_dict(service_key) for service_key in results]


def revoke_saved_keys(key_ids: list[int], account_id: int):
    cursor = get_cursor()
    query_args = dict(key_ids=key_ids, account_id=account_id)
    query1 = """
        DELETE
        FROM saved_session_key_import_ids skid
        USING saved_session_keys_with_hashes sk
        WHERE
            skid.key_id = sk.id
            AND sk.id = ANY (%(key_ids)s)
            AND sk.contributor_id = %(account_id)s
    """
    cursor.execute(query1, query_args)
    query2 = """
        DELETE
        FROM saved_session_keys_with_hashes
        WHERE
            id = ANY (%(key_ids)s)
            AND contributor_id = %(account_id)s
    """
    cursor.execute(query2, query_args)
    redis = get_conn()
    key = f"saved_keys:{account_id}"
    redis.delete(key)


class TDLoginInfo(TypedDict):
    id: str
    password_hash: str


def get_login_info_for_username(username) -> TDLoginInfo | None:
    query = """
        SELECT
            id,
            password_hash
        FROM
            account
        WHERE
            username = %s
    """
    return query_one_db(query, (username,))


def get_password_hash_for_user_id(user_id: int) -> Optional[str]:
    query = "SELECT password_hash FROM account WHERE id = %s"
    return (query_one_db(query, (user_id,)) or {}).get("password_hash")


def set_password_hash_for_user_id(user_id: int, password_hash: str):
    query = "UPDATE account SET password_hash = %s WHERE id = %s"
    cursor = get_cursor()
    cursor.execute(query, (password_hash, user_id))


def is_logged_in():
    if "account_id" in session:
        return True
    return False


def create_account(username: str, password: str, favorites: Optional[list[dict]] = None) -> bool:
    password_hash = bcrypt.hashpw(get_base_password_hash(password), bcrypt.gensalt()).decode()

    cursor = get_cursor()
    query = """
        INSERT INTO account (username, password_hash)
        VALUES (%s, %s)
        ON CONFLICT (username) DO NOTHING
        RETURNING id
    """
    cursor.execute(query, (nh3.clean(username, tags=set()), password_hash))
    account = cursor.fetchone()
    if not account:
        return False
    if account["id"] == 1:
        cursor = get_cursor()
        query = """
            UPDATE account
            SET role = 'administrator'
            WHERE id = 1
        """
        cursor.execute(query)

    if favorites is not None and isinstance(favorites, list):
        for favorite in favorites:
            artist = get_artist(favorite["service"], favorite["artist_id"])
            if artist is None:
                continue
            add_favorite_artist(account["id"], favorite["service"], favorite["artist_id"])

    return True


def change_password(user_id: int, current_password: str, new_password: str) -> bool:
    current_hash = get_password_hash_for_user_id(user_id)

    if not current_hash:
        # hopefully not possible
        raise Exception("Non-existent user ID provided")

    if not bcrypt.checkpw(get_base_password_hash(current_password), current_hash.encode()):
        return False

    new_password_hash = bcrypt.hashpw(get_base_password_hash(new_password), bcrypt.gensalt()).decode()
    set_password_hash_for_user_id(user_id, new_password_hash)
    return True


def attempt_login(username: str, password: str) -> tuple[Account, None] | tuple[None, str]:
    if not username or not password:
        return (None, "Username and password must be provided.")

    account_info = get_login_info_for_username(username)

    if account_info is None:
        return (None, "Username or password is incorrect.")

    if current_app.config.get("ENABLE_LOGIN_RATE_LIMITING") and is_login_rate_limited(str(account_info["id"])):
        return (None, "You're doing that too much. Try again in a little bit.")

    if bcrypt.checkpw(get_base_password_hash(password), account_info["password_hash"].encode("utf-8")):
        if account := load_account(account_info["id"], True):
            session["account_id"] = account.id

            return (account, None)
        else:
            raise Exception("Error loading account")

    return (None, "Username or password is incorrect")


def get_base_password_hash(password: str):
    return base64.b64encode(hashlib.sha256(password.encode("utf-8")).digest())
