"""
Servidor de teste pro HOP 2 (o "servidor de mundo/canal" de verdade, que na captura real e'
85.119.149.224:10199). Nosso login_server.py (hop 1) redireciona o client pra CA (127.0.0.1:20199
por padrao) reescrevendo o IP/porta embutido no CChannelWaitLoginPacket -- ver
redirect_hop2_bytes() em login_server.py.

Objetivo: mandar de volta um CLoginChannelListPacket de verdade (formato 100% decodificado em
channel_list_packet.py, confirmado por round-trip byte-a-byte contra a captura real) com os 5
SERVER IDs reais (42, 41, 28, 22, 1) -- NAO indices sequenciais 0-4, que ja provamos que regridem
o client pra "nada acontece". A hipotese que estamos testando: com esse pacote de verdade, os
campos de status/canal do item de lista de servidor (que ficavam com lixo de heap em +0x40..+0x54,
ver docs/findings.md) devem ficar populados corretamente, e o clique num servidor nao deve mais
crashar.

Uso:
    python channel_server.py [porta]   # default 20199
"""

from __future__ import annotations

import socket
import struct
import sys
import threading
import time as _time

_orig_print = print
def print(*args, **kwargs):  # noqa: A001
    ms = int(_time.time() * 1000) % 1000
    _orig_print(f"[{_time.strftime('%H:%M:%S')}.{ms:03d}]", *args, **kwargs)

from login_packet import encode_frame, read_frame, FrameError, parse_login_request
from channel_list_packet import ChannelEntry, ChannelListPacket
import lotacao

HOST = "0.0.0.0"
DEFAULT_PORT = 20199

WORLD_HOST = "144.217.65.115"  # TESTE 22/08: mesmo motivo do HOP2_HOST no login_server.py
WORLD_PORT = 20299


def build_channel_change_response(server_id: int, channel_idx: int, seq: int = 7,
                                  origem: str | None = None) -> bytes:
    """
    Mesma resposta ao pedido "02 07" (troca de canal) que o login_server.py implementa pro hop 1
    -- aqui e' a versao que roda no hop 2 (que e' de fato onde o client manda esse pedido no
    fluxo real, ja que o hop 2 costuma estar conectado antes do clique). Formato 100% confirmado
    em 21/08 contra uma captura real completa (ver docs/findings.md): IP com OCTETOS INVERTIDOS +
    porta BIG-ENDIAN nos bytes 14:20.
    """
    payload = bytearray(24)
    payload[0:2] = b"\x02\x07"
    payload[2:6] = b"\x00\x00\x00" + bytes([seq])
    payload[6] = 0
    struct.pack_into("<H", payload, 7, server_id)
    struct.pack_into("<H", payload, 9, channel_idx)
    # ESPELHA O CAMINHO DE CHEGADA, como o login_server ja fazia no salto anterior.
    #
    # Mandavamos sempre o IP publico. Quem joga na propria maquina do servidor entrava assim:
    #
    #     login   chegava de 127.0.0.1        -> sessao gravada como 127.0.0.1
    #     canal   chegava de 127.0.0.1        -> mas respondiamos "o mundo e 144.217.65.115"
    #     mundo   chegava de 144.217.65.115   -> IP diferente, conta nao identificada
    #
    # Enquanto so havia uma pessoa online isto passava, porque o mundo caia na regra da
    # "unica sessao recente". Com duas contas ligadas ele recusa -- e bem, porque nao ha
    # como saber qual e qual -- e o jogador fica preso em "Muda para o canal seleccionado".
    # Visto em 30/08 com um amigo ligado ao mesmo tempo.
    host = WORLD_HOST
    if origem and origem.startswith("127."):
        host = origem
    ip_reversed = bytes(reversed([int(x) for x in host.split(".")]))
    struct.pack_into("4s", payload, 14, ip_reversed)
    struct.pack_into(">H", payload, 18, WORLD_PORT)
    payload[20:24] = b"\x00\x00\x2a\x76"
    return encode_frame(bytes(payload))

# Nomes reais confirmados empiricamente em 22/08 (client mostra na tela "Escolhendo o mundo" ao
# receber cada server_id via chanwait). Padrao descoberto: string_id da tabela String.lod (ver
# server/lod_crypto.py) = 1000 + server_id -- confirmado 5/5, sem excecao. server_id 42 mostra
# "Kart" no client original; renomeado pra "Axion" (nome do nosso projeto/servidor).
#   server_id  1 -> "Azmara-Blackforest"  (string_id 1001)
#   server_id 22 -> "Shuradan-Ashara"     (string_id 1022)
#   server_id 28 -> "Bent-Gerot"          (string_id 1028)
#   server_id 41 -> "Victorum"            (string_id 1041)
#   server_id 42 -> "Axion" (era "Kart" no client original, string_id 1042)
REAL_SERVER_NAMES = {
    1: "Azmara-Blackforest",
    22: "Shuradan-Ashara",
    28: "Bent-Gerot",
    41: "Victorum",
    42: "Axion",
}

# SIMPLIFICADO (22/08 tarde) de proposito: so o server_id 42 (Axion) funciona de ponta a ponta
# (world_server.py). Mostrar os outros 4 servidores reais (Azmara-Blackforest/Shuradan-Ashara/
# Bent-Gerot/Victorum) deixava a tela de "Escolhendo o mundo" com um segundo item "NEW" vazio/lixo
# (2 canais pro mesmo server_id 42 com status_flag diferentes, so o primeiro tinha nome
# associado) e ainda deixava os outros 4 mundos clicaveis -- clicar neles crasha o client (nao tem
# nada implementado por tras). Ficando so com 1 servidor / 1 canal, a lista fica limpa e 100%
# funcional (nada pra clicar que quebre).
REAL_SERVER_IDS = [42]

# O TERCEIRO CAMPO E O `status_flag`. TESTADO A 0 EM 30/08 e revertido: nao muda nada na tela.
# O selo "NEW", as etiquetas de regiao e as quatro linhas vazias sao TODOS do client --
# procurou-se "Europa" e "Americas" como texto em todos os ficheiros do jogo e nao existem em
# lado nenhum (so em DLLs do dxvk), portanto sao imagens. E a lista nao e tabela estatica no
# Axn.exe: procurou-se os cinco ids de servidor (1, 22, 28, 41, 42) colados e com passo
# regular de 2 a 128 bytes, e pelas classes CGuiServerSelectList/CGuiServerList -- nada.
# A lista e montada em execucao. Ver a seccao 49 dos achados.
REAL_CHANNEL_ENTRIES = [
    (42, 1, 1, "85.119.149.225", 10101),  # Axion, unico canal (o channel_idx fica em 1 --
    # 99 crasha mesmo com entrada correspondente na lista, testado e descartado em 22/08)
]


def build_test_channel_list() -> ChannelListPacket:
    """A lista de canais. O `status_flag` sai da LOTACAO REAL, nao de uma constante."""
    estado = lotacao.estado()
    channels = [
        ChannelEntry(server_id=sid, channel_idx=idx, status_flag=estado, ip=ip, port=port)
        for (sid, idx, _flag, ip, port) in REAL_CHANNEL_ENTRIES
    ]
    return ChannelListPacket(server_ids=REAL_SERVER_IDS, channels=channels)


def build_handshake_frame() -> bytes:
    # mesmo formato do hop 1 (ver login_server.py) -- confirmado igual na captura real do hop 2
    # (packet #34, 15 bytes de payload: 0b 00 00 00 00 01 00 + desafio de 7 bytes + 00)
    payload = bytes([0x0b, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00]) + b"\x44\xb6\x38\x9b\x76\x01\xe5" + b"\x00"
    return encode_frame(payload)


def build_middle2_frame() -> bytes:
    # replica o "frame do meio" real do hop 2 (packet #37, 290 bytes de payload interno,
    # sig 02 00). So os primeiros ~16 bytes parecem campos de verdade -- resto zerado
    # (mesma estrategia que usamos no hop 1 pra evitar lixo de memoria aleatorio).
    head = bytes.fromhex("020000000002000000000000000100010063")
    payload = head + (b"\x00" * (290 - len(head)))
    return encode_frame(payload)


def build_keepalive_frame(seq: int) -> bytes:
    # CORRIGIDO (22/08): a captura real mostra o SERVIDOR mandando um frame de 19 bytes (mesmo
    # formato do handshake) periodicamente (~a cada 10s) enquanto o client fica parado numa tela
    # (ex: lista de canais, esperando o usuario clicar). Nunca implementamos isso -- suspeita
    # forte (confirmada com log com timestamp) de que o client reseta a conexao sozinho por falta
    # desse keepalive, mesmo antes do usuario terminar de clicar.
    payload = bytes([0x0b, 0x00, 0x00, 0x00, 0x00, seq & 0xFF, 0x00]) + b"\x11\x22\x33\x44\x55\x66\x77" + b"\x00"
    return encode_frame(payload)


def handle_client(conn: socket.socket, addr):
    print(f"[hop2] conexao de {addr}")
    try:
        body = read_frame(conn)
        print(f"[hop2] frame recebido de {addr}: {len(body)} bytes: {body[:64]!r}")

        # DESCOBERTA (21/08): o client manda pro hop 2 o MESMO pacote de login (CLoginReqPacket)
        # que mandou pro hop 1 -- confirmado byte a byte identico contra a captura real (packet
        # #17 == packet #32). Nao existe token separado hop1->hop2; cada servidor reautentica
        # com as MESMAS credenciais. Por isso usamos o mesmo parser aqui.
        try:
            req = parse_login_request(body)
            print(f"[hop2] LOGIN (reautenticacao) de {addr}: {req}")
        except FrameError as e:
            print(f"[hop2] frame recebido nao e um login request valido: {e}")

        # CORRIGIDO (21/08): a captura real mostra o handshake sendo mandado sozinho primeiro
        # (o client eco de volta), e SO DEPOIS middle2+channel_list como 2 sends separados --
        # mandar tudo junto num so sendall() parece disparar a barra de "carregando mundo" cedo
        # demais no client (achado do usuario, confirmado comparando contra o servidor real).
        handshake = build_handshake_frame()
        conn.sendall(handshake)
        print(f"[hop2] handshake ({len(handshake)}) enviado pra {addr}")

        conn.settimeout(10)
        try:
            echo = read_frame(conn)
            print(f"[hop2] eco do handshake recebido de {addr}: {len(echo)} bytes")
        except (FrameError, socket.timeout) as e:
            print(f"[hop2] nao recebeu eco do handshake: {e}")

        middle2 = build_middle2_frame()
        conn.sendall(middle2)
        chanlist_payload = build_test_channel_list().encode_payload()
        chanlist_frame = encode_frame(chanlist_payload)
        conn.sendall(chanlist_frame)
        print(f"[hop2] middle2 ({len(middle2)}) + channel_list REAL ids={REAL_SERVER_IDS} "
              f"({len(chanlist_frame)}) enviados separados pra {addr}")

        # CORRIGIDO (22/08): timeout curto (3s) em vez de 30s, pra poder mandar keepalive
        # periodico enquanto espera o client (ver build_keepalive_frame). So desiste de vez
        # depois de MAX_IDLE_ROUNDS timeouts seguidos sem nenhum dado do client.
        conn.settimeout(3)
        keepalive_seq = 10
        idle_rounds = 0
        MAX_IDLE_ROUNDS = 40  # 40 * 3s = 120s de tolerancia total
        while True:
            try:
                body = read_frame(conn)
                idle_rounds = 0
                print(f"[hop2] frame adicional de {addr}: {len(body)} bytes: {body[:64]!r}...")
                if len(body) >= 11 and body[0:2] == b"\x02\x07":
                    server_id, channel_idx = struct.unpack_from("<HH", body, 7)
                    resp = build_channel_change_response(
                        server_id, channel_idx, origem=addr[0] if addr else None)
                    conn.sendall(resp)
                    print(f"[hop2] respondido pedido de troca de canal (02 07, servidor={server_id} "
                          f"canal={channel_idx}) com endereco do mundo {WORLD_HOST}:{WORLD_PORT} "
                          f"({len(resp)} bytes) pra {addr}")
                elif len(body) >= 16 and body[0:2] == b"\x02\x06":
                    # NOVO (22/08): descoberto ao vivo -- depois de clicar OK na tela "Selecao do
                    # canal", o CLIENT manda esse pacote de 16 bytes (assinatura 02 06, mas
                    # CLIENTE->SERVIDOR dessa vez -- nao eh o mesmo sentido do chanwait 02 06 que o
                    # hop1 manda). Antes nao tratavamos isso, o client ficava mandando keepalive
                    # por ~20s e desistia (WinError 10054) -- isso era o "disconnect" reportado.
                    # Hipotese: e o pedido de "entrar no canal" (equivalente ao 02 07, so que
                    # disparado por essa tela especificamente). So temos 1 canal configurado
                    # (server_id=42/canal=1), entao respondemos direto com o redirecionamento pro
                    # world_server, igual fariamos pro 02 07.
                    resp = build_channel_change_response(
                        42, 1, origem=addr[0] if addr else None)
                    conn.sendall(resp)
                    print(f"[hop2] recebido 02 06 do CLIENT (pedido de entrar no canal?) -- "
                          f"respondido com endereco do mundo {WORLD_HOST}:{WORLD_PORT} "
                          f"({len(resp)} bytes) pra {addr}")
            except FrameError as e:
                print(f"[hop2] conexao fechada pelo client (ou erro de framing): {e}")
                break
            except socket.timeout:
                idle_rounds += 1
                if idle_rounds >= MAX_IDLE_ROUNDS:
                    print(f"[hop2] {MAX_IDLE_ROUNDS * 3}s sem nada do client -- desistindo")
                    break
                keepalive_seq += 1
                ka = build_keepalive_frame(keepalive_seq)
                try:
                    conn.sendall(ka)
                    print(f"[hop2] keepalive (seq={keepalive_seq}) mandado pra {addr} "
                          f"(idle_round {idle_rounds}/{MAX_IDLE_ROUNDS})")
                except OSError as e:
                    print(f"[hop2] falhou mandando keepalive: {e}")
                    break
    except FrameError as e:
        print(f"[hop2] erro de framing com {addr}: {e}")
    except Exception as e:
        print(f"[hop2] erro com {addr}: {e}")
    finally:
        conn.close()
        print(f"[hop2] conexao {addr} fechada")


def main():
    port = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PORT
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((HOST, port))
    srv.listen(5)
    print(f"channel_server (hop2) escutando em {HOST}:{port}")
    while True:
        conn, addr = srv.accept()
        t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
        t.start()


if __name__ == "__main__":
    main()
