"""
Parser/encoder pro formato de pacote CLoginReqPacket do Karos Online (client "alpha"/Rebirth).

Layout confirmado via engenharia reversa do AMo.exe (Ghidra), batido byte a byte contra uma
captura real (ver D:\\RoshEmulator\\docs\\findings.md, secao "CONFIRMADO 100%").

Framing no socket: 4 bytes big-endian = tamanho do que vem depois (o "body").
Dentro do body:
    offset 0x00 (2 bytes): assinatura, deve ser 02 00 pra esse tipo de pacote
    offset 0x02..0x114   : bloco de cabecalho (~275 bytes) -- client->server manda isso sem uso
                            (era memoria reaproveitada, lixo); server->client usa esses offsets
                            pra portas de sub-servidor (ainda nao confirmado 100%)
    offset 0x115 em diante: 5 strings \0-terminadas: ID, PW (texto puro), hash1 (32 hex),
                            locale (ex "pt"), hash2 (32 hex)
"""

from __future__ import annotations

import struct
from dataclasses import dataclass

SIGNATURE = b"\x02\x00"
STRINGS_OFFSET = 0x115
HEADER_MIN_LEN = 0x16  # Parse exige body length > 0x15


class FrameError(Exception):
    pass


def encode_frame(body: bytes) -> bytes:
    return struct.pack(">I", len(body)) + body


MAX_FRAME = 16384
"""Teto para o tamanho de frame que o client manda.

O valor vem dos parsers do proprio projeto, que ja usavam 16384 como limite de sanidade.
Medido no trafego oficial em 24/08: o maior frame legitimo tem 1812 bytes, entao o teto e
generoso por quase dez vezes. Frame de tamanho ZERO e valido -- aparece 140 vezes na
captura -- e por isso nao e recusado aqui.
"""


def read_frame(sock) -> bytes:
    """Le um frame completo (header de 4 bytes BE + body) de um socket bloqueante.

    O tamanho vem do client e NAO e confiavel. Sem o teto abaixo, um client mandando
    FF FF FF FF fazia o servidor tentar acumular 4 GB num bytearray; como ha uma thread
    por jogador, meia duzia de conexoes assim derrubava a maquina. Quatro bytes.
    """
    header = _recv_exact(sock, 4)
    (body_len,) = struct.unpack(">I", header)
    if body_len > MAX_FRAME:
        raise FrameError("tamanho de frame invalido: %d (maximo %d)" % (body_len, MAX_FRAME))
    return _recv_exact(sock, body_len)


def _recv_exact(sock, n: int) -> bytes:
    buf = bytearray()
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise FrameError(f"conexao fechada esperando {n} bytes (recebeu {len(buf)})")
        buf.extend(chunk)
    return bytes(buf)


@dataclass
class LoginRequest:
    id: str
    pw: str
    hash1: str
    locale: str
    hash2: str

    def __repr__(self) -> str:
        """A senha NUNCA vai pro log.

        Ate 23/08 os tres servidores faziam `print(f"LOGIN de {addr}: {req}")` e o dataclass
        imprimia tudo, senha inclusive -- em `_login_stdout.log`, em disco, em claro. O client
        manda em texto puro e isso a gente nao controla; gravar, controla.
        """
        return (f"LoginRequest(id={self.id!r}, pw=<oculta {len(self.pw)} chars>, "
                f"hash1={self.hash1!r}, locale={self.locale!r}, hash2={self.hash2!r})")


def parse_login_request(body: bytes) -> LoginRequest:
    if len(body) <= HEADER_MIN_LEN:
        raise FrameError(f"body curto demais ({len(body)} bytes)")
    if body[:2] != SIGNATURE:
        raise FrameError(f"assinatura invalida: {body[:2]!r} (esperado {SIGNATURE!r})")
    if len(body) <= STRINGS_OFFSET:
        raise FrameError(f"body nao tem os {STRINGS_OFFSET} bytes de cabecalho esperados")

    cursor = STRINGS_OFFSET
    fields = []
    for _ in range(5):
        end = body.index(b"\x00", cursor)
        fields.append(body[cursor:end].decode("latin-1"))
        cursor = end + 1

    return LoginRequest(
        id=fields[0],
        pw=fields[1],
        hash1=fields[2],
        locale=fields[3],
        hash2=fields[4],
    )


def build_login_request(req: LoginRequest, header_len: int = STRINGS_OFFSET) -> bytes:
    """Monta um body de CLoginReqPacket (pro caso de precisarmos simular o client)."""
    header = SIGNATURE + b"\x00" * (header_len - len(SIGNATURE))
    strings = b"\x00".join(
        s.encode("latin-1") for s in (req.id, req.pw, req.hash1, req.locale, req.hash2)
    ) + b"\x00"
    return header + strings


if __name__ == "__main__":
    with open(r"D:\RoshEmulator\captures\login_packet_01_raw.bin", "rb") as f:
        raw = f.read()
    (declared_len,) = struct.unpack(">I", raw[:4])
    body = raw[4:]
    assert declared_len == len(body), f"{declared_len} != {len(body)}"
    req = parse_login_request(body)
    print(req)
