"""
Utilidades de autenticación para servidores 3.4.3 (TrinityCore) con cuentas
Battle.net.

Dos algoritmos SRP6 distintos conviven:

  * ``battlenet_accounts``  -> SRP6 **v2**: PBKDF2-HMAC-SHA512, N de 2048 bits,
    g = 2.  El "usuario" SRP es HEX(SHA256(UPPER(email))).  Es el que usa el
    cliente 3.4.3 para conectarse (login por email).

  * ``account`` (cuenta de juego) -> SRP6 **Grunt/SHA1**: el clásico de
    AzerothCore/3.3.5a.  Cada cuenta bnet tiene 1..N cuentas de juego con
    username ``<bnetId>#<index>``.

Referencias del propio source 3.4.3:
  src/common/Cryptography/Authentication/SRP6.cpp  (CalculateX / CalculateVerifier)
  src/server/game/Accounts/BattlenetAccountMgr.cpp (CreateBattlenetAccount)
"""

import hashlib
import os

# ---------------------------------------------------------------------------
# SRP6 v2 (battlenet_accounts) — PBKDF2-HMAC-SHA512
# ---------------------------------------------------------------------------

# BnetSRP6v2Base::N (2048 bits) y g, tal cual en SRP6.cpp
_BNET_N = int(
    "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050"
    "A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50"
    "E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B8"
    "55F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773B"
    "CA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748"
    "544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6"
    "AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB6"
    "94B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73",
    16,
)
_BNET_g = 2
_BNET_SALT_LEN = 32
_BNET_ITERATIONS = 15000          # GetXIterations() para v2
_BNET_SRP_VERSION = 2
_TWO_POW_512 = 1 << 512

# TrinityCore almacena el verifier con BigNumber::ToByteVector(), que por
# defecto es little-endian.  Si al comparar contra una cuenta creada por el
# propio servidor (`.bnetaccount create`) el login del CLIENTE fallara, prueba a
# poner esto en False (big-endian).  El login del PORTAL funciona en cualquier
# caso porque registra y verifica con el mismo criterio.
_BNET_VERIFIER_LITTLE_ENDIAN = True


def bnet_srp_username(email):
    """SRP username de bnet = HEX(SHA256(UPPER(email))) en mayúsculas."""
    email_up = email.strip().upper()
    return hashlib.sha256(email_up.encode("utf-8")).hexdigest().upper()


def _bnet_calculate_x(email, password, salt):
    """Replica BnetSRP6v2Base::CalculateX (SRP6.cpp)."""
    srp_user = bnet_srp_username(email)
    tmp = (srp_user + ":" + password).encode("utf-8")
    x_bytes = hashlib.pbkdf2_hmac("sha512", tmp, salt, _BNET_ITERATIONS, dklen=64)
    x = int.from_bytes(x_bytes, "big")
    if x_bytes[0] & 0x80:            # si el bit alto está puesto, resta 2^512
        x -= _TWO_POW_512
    return x % (_BNET_N - 1)


def _int_to_verifier_bytes(value):
    """Emula BigNumber::ToByteVector(): longitud mínima, endianness configurable."""
    length = max(1, (value.bit_length() + 7) // 8)
    order = "little" if _BNET_VERIFIER_LITTLE_ENDIAN else "big"
    return value.to_bytes(length, order)


def _verifier_bytes_to_int(data):
    order = "little" if _BNET_VERIFIER_LITTLE_ENDIAN else "big"
    return int.from_bytes(data, order)


def bnet_make_registration(email, password):
    """
    Genera (salt, verifier, srp_version) para una nueva cuenta Battle.net.

    salt: 32 bytes aleatorios (se guarda tal cual).
    verifier: g^x mod N en longitud mínima (como TrinityCore::ToByteVector).
    """
    salt = os.urandom(_BNET_SALT_LEN)
    x = _bnet_calculate_x(email, password, salt)
    v = pow(_BNET_g, x, _BNET_N)
    return salt, _int_to_verifier_bytes(v), _BNET_SRP_VERSION


def bnet_verify(email, password, salt, stored_verifier):
    """Comprueba la contraseña de una cuenta Battle.net (login por email)."""
    if salt is None or stored_verifier is None:
        return False
    if isinstance(salt, memoryview):
        salt = bytes(salt)
    if isinstance(stored_verifier, memoryview):
        stored_verifier = bytes(stored_verifier)
    x = _bnet_calculate_x(email, password, salt)
    v = pow(_BNET_g, x, _BNET_N)
    # Comparación numérica (robusta frente a padding).
    return v == _verifier_bytes_to_int(stored_verifier)


# ---------------------------------------------------------------------------
# SRP6 Grunt/SHA1 (cuenta de juego `account`) — el clásico de 3.3.5a
# ---------------------------------------------------------------------------

_GRUNT_g = 7
_GRUNT_N = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16)
_GRUNT_SALT_LEN = 32


def _sha1(data):
    return hashlib.sha1(data).digest()


def game_calculate_verifier(username, password, salt):
    """
    SRP6 Grunt (SHA1) para la tabla `account`.  ``username`` y ``password`` se
    pasan en mayúsculas (TrinityCore hace Utf8ToUpperOnlyLatin en ambos).
    Devuelve el verifier en bytes little-endian (32).
    """
    if isinstance(salt, memoryview):
        salt = bytes(salt)
    h1 = _sha1((username.upper() + ":" + password.upper()).encode("utf-8"))
    h2 = _sha1(salt + h1)
    h2_int = int.from_bytes(h2, "little")
    verifier = pow(_GRUNT_g, h2_int, _GRUNT_N)
    return verifier.to_bytes(32, "little")


def game_make_registration(username, password):
    """Genera (salt, verifier) Grunt para una cuenta de juego nueva."""
    salt = os.urandom(_GRUNT_SALT_LEN)
    verifier = game_calculate_verifier(username, password[:16], salt)
    return salt, verifier


def game_verify(username, password, salt, stored_verifier):
    """Comprueba la contraseña de una cuenta de juego (SRP6 Grunt)."""
    if salt is None or stored_verifier is None:
        return False
    if isinstance(stored_verifier, memoryview):
        stored_verifier = bytes(stored_verifier)
    calc = game_calculate_verifier(username, password[:16], salt)
    return int.from_bytes(calc, "little") == int.from_bytes(stored_verifier, "little")


# ---------------------------------------------------------------------------
# Helpers de acceso a la BD auth (bnet <-> cuentas de juego)
# ---------------------------------------------------------------------------

def normalize_email(email):
    """El email en battlenet_accounts se almacena en MAYÚSCULAS."""
    return email.strip().upper()


def get_bnet_account_by_email(cursor, email):
    """Devuelve dict {id, email, srp_version, salt, verifier} o None."""
    cursor.execute(
        "SELECT id, email, srp_version, salt, verifier "
        "FROM battlenet_accounts WHERE email = %s",
        [normalize_email(email)],
    )
    row = cursor.fetchone()
    if not row:
        return None
    return {
        "id": row[0],
        "email": row[1],
        "srp_version": row[2],
        "salt": row[3],
        "verifier": row[4],
    }


def get_game_accounts_for_bnet(cursor, bnet_id):
    """Lista de cuentas de juego de una cuenta bnet: [{id, username, index}]."""
    cursor.execute(
        "SELECT id, username, battlenet_index "
        "FROM account WHERE battlenet_account = %s ORDER BY battlenet_index",
        [bnet_id],
    )
    return [{"id": r[0], "username": r[1], "index": r[2]} for r in cursor.fetchall()]


def get_next_battlenet_index(cursor, bnet_id):
    """Siguiente battlenet_index libre para una cuenta bnet (1-based)."""
    cursor.execute(
        "SELECT MAX(battlenet_index) FROM account WHERE battlenet_account = %s",
        [bnet_id],
    )
    row = cursor.fetchone()
    return (row[0] or 0) + 1


def make_game_account_username(bnet_id, index=1):
    """Nombre de la cuenta de juego: ``<bnetId>#<index>`` (como TrinityCore)."""
    return f"{bnet_id}#{index}"
