# -*- coding: utf-8 -*-
"""Importa o catalogo de missoes do client para dbo.quest.

    python tools/importar_quests.py            mostra o que faria
    python tools/importar_quests.py --aplicar  grava

O quest.lod e uma tabela de registros de tamanho fixo -- 6336 bytes cada, 726 deles,
comecando no offset 25348. O layout foi medido em 24/08 comparando o arquivo com uma
cadeia de missoes feita ao vivo no servidor oficial: os campos batem com o que o
09 03 mandou no fio (alvo 9300311 = Lucas, 9300161 = Oshirus, 70054 = Dente do lagarto
do pantano x5).

Cuidado com dois campos que enganam:
  [8]  parece nivel e nao e -- so vai de 1 a 9 nos 726 registros, e uma categoria.
  [52] e o nivel de verdade: 1 a 200, com 111 valores distintos.
"""
import os
import struct
import sys

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "server"))
import lod_crypto                                                    # noqa: E402
import pyodbc                                                        # noqa: E402

LOD = r"C:\Games\Karos\ResUnpak\Interface\IE_Lod\quest.lod_pt"

# Os 4 primeiros bytes do fluxo decifrado sao a CONTAGEM de registros; a tabela comeca
# logo depois. Na primeira versao eu achei o inicio varrendo por ids maiores que 10 milhoes
# e comecei no offset 25348, perdendo 4 missoes caladamente -- as tres do Kielias (id
# 9000001..3) e a de teste do desenvolvedor. Ler o cabecalho e o jeito certo.
INI, TAM = 4, 6336

# offset dentro do registro -> coluna
OFF_ID      = 0
OFF_CAT     = 8       # 1..9, categoria (NAO e nivel)
OFF_CAP     = 12
OFF_X       = 28      # float
OFF_Y       = 32      # float
OFF_DOADOR  = 44      # npc_id de quem entrega a missao
OFF_PREREQ  = 48      # quest_id anterior na cadeia, 0 se e a primeira
OFF_NIVEL   = 52      # nivel minimo
OFF_NIVMAX  = 56
OFF_TIPO    = 84      # 1=matar  3=coletar  8=falar  9=?
OFF_ALVO    = 88      # mob, item ou npc, conforme o tipo
OFF_QTD     = 92
OFF_ALVO2   = 100     # segundo objetivo, 0 quando nao ha
OFF_REGIAO  = 132

# Bloco de recompensa, medido em 24/08 contra a entrega real da missao 19003136:
# a EXP paga foi 2030 e o item recebido foi o 46403, os dois batendo com o arquivo.
OFF_EXP     = 1148    # EXP paga. bigint, nao int: 28 das 726 passam de 2^31, e o valor
                      # 0xFFFFFFFF e sentinela de "sem recompensa", nao um numero.
OFF_PREMIO  = 1196    # inicio do vetor de premios
PASSO_PREMIO = 24     # cada slot ocupa 24 bytes; +0 item, +8 quantidade
N_PREMIO    = 7       # SETE slots, um por classe -- o Artilheiro recebeu o slot 6

TIPO = {1: "matar", 3: "coletar", 8: "falar", 9: "desconhecido"}

DDL = ("""
IF OBJECT_ID('dbo.quest') IS NULL
CREATE TABLE dbo.quest (
    quest_id      int          NOT NULL PRIMARY KEY,
    nivel         int          NOT NULL,
    nivel_max     int          NULL,
    categoria     int          NULL,
    doador_npc    int          NULL,
    pre_requisito int          NULL,
    tipo          int          NOT NULL,
    tipo_nome     nvarchar(20) NULL,
    alvo          int          NULL,
    quantidade    int          NULL,
    alvo2         int          NULL,
    exp_paga      bigint       NULL,
    suspeita      nvarchar(60) NULL,
    regiao        int          NULL,
    x             real         NULL,
    y             real         NULL
)
""", """
IF OBJECT_ID('dbo.quest_premio') IS NULL
CREATE TABLE dbo.quest_premio (
    quest_id   int NOT NULL,
    classe     int NOT NULL,
    item_id    int NOT NULL,
    quantidade int NOT NULL,
    CONSTRAINT pk_quest_premio PRIMARY KEY (quest_id, classe)
)
""")


def registros(dados):
    n = struct.unpack_from("<I", dados, 0)[0]
    if not (0 < n < 100000):
        raise ValueError("contagem de registros implausivel no cabecalho: %d" % n)
    for k in range(n):
        o = INI + k * TAM
        if o + 140 > len(dados):
            break
        u32 = lambda off: struct.unpack_from("<I", dados, o + off)[0]   # noqa: E731
        f32 = lambda off: struct.unpack_from("<f", dados, o + off)[0]   # noqa: E731
        qid = u32(OFF_ID)
        if not qid:
            continue
        tp = u32(OFF_TIPO)
        yield dict(quest_id=qid, nivel=u32(OFF_NIVEL), nivel_max=u32(OFF_NIVMAX),
                   categoria=u32(OFF_CAT), doador_npc=u32(OFF_DOADOR),
                   pre_requisito=u32(OFF_PREREQ), tipo=tp, tipo_nome=TIPO.get(tp),
                   alvo=u32(OFF_ALVO), quantidade=u32(OFF_QTD), alvo2=u32(OFF_ALVO2),
                   regiao=u32(OFF_REGIAO), x=f32(OFF_X), y=f32(OFF_Y),
                   exp_paga=(lambda v: None if v == 0xFFFFFFFF else v)(u32(OFF_EXP)),
                   premios=[(s, u32(OFF_PREMIO + s * PASSO_PREMIO),
                             u32(OFF_PREMIO + s * PASSO_PREMIO + 8))
                            for s in range(N_PREMIO)
                            if u32(OFF_PREMIO + s * PASSO_PREMIO)],
                   suspeita=None)


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    if not os.path.exists(LOD):
        print("nao achei %s" % LOD)
        return 1
    dados = lod_crypto.decrypt_lod_stream(open(LOD, "rb").read())
    linhas = list(registros(dados))
    print("%d missoes lidas de %s" % (len(linhas), os.path.basename(LOD)))

    c = pyodbc.connect("DRIVER={ODBC Driver 18 for SQL Server};SERVER=localhost;"
                       "DATABASE=Rosh;Trusted_Connection=yes;TrustServerCertificate=yes")
    import collections
    print("   por tipo   : %s" % dict(collections.Counter(l["tipo_nome"] for l in linhas)))
    print("   nivel      : %d a %d" % (min(l["nivel"] for l in linhas),
                                       max(l["nivel"] for l in linhas)))
    print("   cadeias    : %d missoes tem pre-requisito"
          % sum(1 for l in linhas if l["pre_requisito"]))
    print("   regioes    : %d" % len({l["regiao"] for l in linhas}))
    print("   com premio : %d missoes, %d linhas de premio"
          % (sum(1 for l in linhas if l["premios"]),
             sum(len(l["premios"]) for l in linhas)))
    print("   pagam EXP  : %d" % sum(1 for l in linhas if l["exp_paga"]))
    # Marcar "de teste" por valor de EXP nao funciona: no nivel 90 uma missao legitima ja
    # paga 25 milhoes. Quem denuncia e o ALVO -- o client nomeia os objetos de depuracao.
    alvos = {l["alvo"] for l in linhas}
    nomes = {}
    if alvos:
        lista = ",".join(str(a) for a in alvos if a)
        for tab, col in (("item", "item_id"), ("mob", "mob_id"), ("npc", "npc_id")):
            u2 = c.cursor()
            u2.execute("SELECT %s, nome FROM dbo.%s WHERE %s IN (%s)" % (col, tab, col, lista))
            for i, nm in u2.fetchall():
                nomes.setdefault(i, nm)
    for l in linhas:
        nm = (nomes.get(l["alvo"]) or "").lower()
        # Substring pura da falso positivo: "Testemunho de enterro" contem "teste".
        # Exigir a palavra inteira, e mais o sinal de EXP fora de escala, que e o que
        # separa o objeto de depuracao de um NPC chamado "Mestre do teste".
        palavra = "teste" in nm.replace(".", " ").replace(",", " ").split()
        exagero = (l["exp_paga"] or 0) > 100000000
        if palavra and exagero:
            l["suspeita"] = "depuracao: %s" % nomes.get(l["alvo"])
    susp = [l for l in linhas if l["suspeita"]]
    if susp:
        print("   SUSPEITAS  : %d com alvo de depuracao:" % len(susp))
        for l in susp[:8]:
            print("      %d  lv%-4d exp=%-12s %s" % (l["quest_id"], l["nivel"],
                                                     l["exp_paga"], l["suspeita"]))

    if not aplicar:
        print()
        print("nada gravado. rode com --aplicar")
        return 0

    u = c.cursor()
    for comando in DDL:
        u.execute(comando)
    c.commit()
    # a tabela pode ser de uma importacao anterior, sem as colunas novas: completa em vez
    # de recriar, pra nao perder nada que ja dependa dela.
    for col, tipo in (("exp_paga", "bigint"), ("suspeita", "nvarchar(60)"), ("nivel_max", "int"), ("alvo2", "int"),
                      ("categoria", "int"), ("regiao", "int"), ("x", "real"), ("y", "real")):
        u.execute("IF COL_LENGTH('dbo.quest', ?) IS NULL "
                  "EXEC('ALTER TABLE dbo.quest ADD ' + ? + ' ' + ?)", col, col, tipo)
    c.commit()
    u.execute("DELETE FROM dbo.quest")
    cols = ("quest_id", "nivel", "nivel_max", "categoria", "doador_npc", "pre_requisito",
            "tipo", "tipo_nome", "alvo", "quantidade", "alvo2", "exp_paga",
            "suspeita", "regiao", "x", "y")
    sql = "INSERT INTO dbo.quest (%s) VALUES (%s)" % (",".join(cols),
                                                      ",".join("?" * len(cols)))
    u.fast_executemany = True
    u.executemany(sql, [tuple(l[k] for k in cols) for l in linhas])
    c.commit()
    u.execute("DELETE FROM dbo.quest_premio")
    u.executemany("INSERT INTO dbo.quest_premio (quest_id,classe,item_id,quantidade) "
                  "VALUES (?,?,?,?)",
                  [(l["quest_id"], s, it, qt) for l in linhas for s, it, qt in l["premios"]])
    c.commit()
    u.execute("SELECT COUNT(*) FROM dbo.quest")
    n1 = u.fetchone()[0]
    u.execute("SELECT COUNT(*) FROM dbo.quest_premio")
    print()
    print("gravadas %d linhas em dbo.quest e %d em dbo.quest_premio" % (n1, u.fetchone()[0]))
    return 0


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