# -*- coding: utf-8 -*-
"""Diz se uma captura e do OFICIAL ou do NOSSO servidor, pelo conteudo.

Existe porque em 28/08 se compararam tres vezes "o nosso contra o oficial" e eram as duas do
oficial: o gravador do Axion desistia ao fim de 5 minutos e nunca se agarrava, mas o ficheiro
antigo continuava la com um nome plausivel. Nome de ficheiro e pasta nao sao prova.

A PRIMEIRA VERSAO DESTA FERRAMENTA ESTAVA ERRADA e marcou capturas do oficial como nossas.
Usava "manda 08 01 E 08 06" como assinatura, no raciocinio de que so o nosso passou a mandar
os dois em 28/08. Mas o oficial tambem manda 08 01 -- em dano e subida de nivel; o que se
mediu nesse dia foi apenas que a REGENERACAO dele vai no 08 06. Presenca dos dois nao prova
nada. Ficou aqui como aviso: uma assinatura tem de ser algo que o outro lado seja INCAPAZ de
produzir, nao algo que ele costume nao fazer.

As provas que se usam agora, por ordem de forca:

  1. OPCODES QUE NOS CORTAMOS (decisiva). world_server.OPCODES_A_CORTAR remove cinco opcodes
     do nosso fluxo -- o nosso servidor e literalmente incapaz de os emitir. Um so que apareca
     e prova de que a captura e do oficial.
  2. NOMES DA NOSSA BASE (decisiva ao contrario). Se um nome de dbo.personagem aparece, e
     nossa. A ausencia nao prova nada: se a gravacao comecar a meio da sessao, o nome nunca
     passa no fio.
  3. VARIEDADE E POPULACAO (forte). Medido em 28/08: o nosso mundo deu 30 opcodes distintos e
     7-10 entidades; oito capturas do oficial deram 39-94 opcodes e 34-1846 entidades.
"""
import collections
import io
import os
import re
import sys

CAB = re.compile(r"^\[(\d{1,2}):(\d\d):(\d\d)\.(\d+)\] (SEND|RECV) sock=(\S+) len=(\d+)")
HEX = re.compile(r"^[0-9A-Fa-f ]+$")

# Os cinco que world_server.cortar_do_stream() remove. Se aparecem, nao fomos nos.
CORTADOS = {(0x06, 0x28), (0x0d, 0x0f), (0x0e, 0x0c), (0x06, 0x25), (0x0a, 0x0c)}
# Familia 03 que so o oficial povoa (03 06 sozinho deu 6322 pacotes numa captura).
SO_OFICIAL = {(0x03, 0x06), (0x03, 0x0a), (0x03, 0x0b), (0x03, 0x00)}


def varrer(caminho, limite_bytes=None):
    rec, env = collections.Counter(), collections.Counter()
    ents, textos = set(), bytearray()
    t0 = t1 = None
    atual, buf, lidos = None, [], 0

    def fechar():
        if atual is None:
            return
        try:
            b = bytes.fromhex("".join(buf))
        except ValueError:
            return
        if atual == "RECV":
            textos.extend(b[:4000])
        i = 0
        while i + 4 <= len(b):
            n = int.from_bytes(b[i:i + 4], "big")
            if n <= 0 or n > 65535 or i + 4 + n > len(b):
                break
            p = b[i + 4:i + 4 + n]
            if len(p) >= 2:
                (rec if atual == "RECV" else env)[(p[0], p[1])] += 1
            if len(p) >= 12 and p[0] == 4 and p[1] in (1, 2):
                ents.add(int.from_bytes(p[8:12], "big"))
            i += 4 + n

    with io.open(caminho, encoding="latin-1", errors="ignore") as f:
        for ln in f:
            lidos += len(ln)
            if limite_bytes and lidos > limite_bytes:
                break
            m = CAB.match(ln)
            if m:
                fechar()
                atual, buf = m.group(5), []
                t = "%s:%s:%s" % (m.group(1), m.group(2), m.group(3))
                if t0 is None:
                    t0 = t
                t1 = t
            elif atual and HEX.match(ln.strip()):
                buf.append(ln.strip().replace(" ", ""))
    fechar()
    return rec, env, ents, bytes(textos), t0, t1


def nossos_nomes():
    try:
        sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "server"))
        import db
        con = db._conectar()
        if con is None:
            return set()
        cur = con.cursor()
        cur.execute("SELECT nome FROM dbo.personagem")
        return {r[0] for r in cur.fetchall()}
    except Exception:                                                 # noqa: BLE001
        return set()


def julgar(rec, ents, textos, nomes):
    provas = []
    achados = sorted(CORTADOS & set(rec))
    if achados:
        provas.append(("OFICIAL", "decisiva",
                       "tem %s -- opcodes que cortamos do nosso fluxo, nao os sabemos emitir"
                       % " ".join("%02x%02x" % k for k in achados)))
    batem = [n for n in nomes if n and n.encode() in textos]
    if batem:
        provas.append(("NOSSO", "decisiva",
                       "nome da nossa base no fio: %s" % ", ".join(sorted(batem))))
    so = sorted(SO_OFICIAL & set(rec))
    if so:
        provas.append(("OFICIAL", "forte",
                       "familia 03 do oficial: %s"
                       % " ".join("%02x%02x(%d)" % (a, b, rec[(a, b)]) for a, b in so)))
    # A CONTAGEM DE ENTIDADES DEIXOU DE SERVIR, e fica aqui o porque.
    #
    # Esta regra dizia "34 entidades ou mais e do oficial", calibrada em 28/08 quando o nosso
    # RAIO_VISAO era 430 e mostravamos 7 a 10. Nessa mesma noite o raio passou a 480 com 120
    # de margem -- medido no oficial -- e o nosso mundo passou a mostrar 64. A regra comecou a
    # acusar as nossas proprias capturas de serem do oficial, e uma delas saiu "CONTRADITORIO"
    # contra a prova decisiva do nome do personagem.
    #
    # Uma heuristica calibrada contra um valor que o projecto ainda vai mudar tem prazo de
    # validade. Fica so a contagem de OPCODES, que nao depende do raio de visao: o nosso
    # repertorio e pequeno porque ha sistemas por implementar, nao porque se ve menos longe.
    if len(rec) >= 45:
        provas.append(("OFICIAL", "forte",
                       "%d opcodes distintos -- o nosso repertorio nao chega la"
                       % len(rec)))
    elif len(rec) <= 32:
        provas.append(("NOSSO", "fraca",
                       "so %d opcodes distintos, do tamanho do nosso repertorio"
                       % len(rec)))
    peso = {"decisiva": 100, "forte": 10, "fraca": 1}
    n = sum(peso[f] for lado, f, _ in provas if lado == "NOSSO")
    o = sum(peso[f] for lado, f, _ in provas if lado == "OFICIAL")
    if n and o:
        return "CONTRADITORIO", provas
    return ("NOSSO" if n > o else "OFICIAL" if o > n else "INDECISO"), provas


def main() -> int:
    alvos = [a for a in sys.argv[1:] if os.path.isfile(a)]
    for d in [a for a in sys.argv[1:] if os.path.isdir(a)]:
        alvos += [os.path.join(d, f) for f in sorted(os.listdir(d)) if f.endswith(".log")]
    if not alvos:
        print(__doc__)
        return 2
    nomes = nossos_nomes()
    print("personagens da nossa base: %s" % (", ".join(sorted(nomes)) or "(banco indisponivel)"))
    print()
    for cam in alvos:
        mb = os.path.getsize(cam) / 1e6
        lim = 40_000_000 if mb > 60 else None
        rec, env, ents, textos, t0, t1 = varrer(cam, lim)
        lado, provas = julgar(rec, ents, textos, nomes)
        print("%-30s %6.1f MB%s" % (os.path.basename(cam), mb, "  (so o inicio)" if lim else ""))
        print("   >>> %s <<<   %s -> %s   %d opcodes, %d pacotes, %d entidades"
              % (lado, t0, t1, len(rec), sum(rec.values()), len(ents)))
        for _, forca, txt in provas:
            print("      [%s] %s" % (forca, txt))
        print()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
