# -*- coding: utf-8 -*-
"""Ferramenta web de administracao: dar itens a um personagem, com as opcoes escolhidas.

    python admin/web.py            abre em http://127.0.0.1:8088
    python admin/web.py 9000       noutra porta

    python admin/web.py --publico <senha>    escuta na rede, com senha

POR OMISSAO SO ESCUTA EM 127.0.0.1, E ISSO NAO E DETALHE. Esta pagina cria itens do nada --
quem lhe chega tem o servidor todo. Como o Claude Code corre NA MAQUINA DO SERVIDOR e o
administrador costuma estar noutra, o `--publico` existe para o caso real; mas exige senha,
e sem senha recusa-se a subir na rede. Nao ha modo publico sem autenticacao de proposito.

A senha vai por HTTP Basic, ou seja EM CLARO na ligacao. Contra alguem que esteja a ler o
trafego isso nao vale nada; vale contra quem apenas encontre a porta aberta, que e o risco
de facto. Para uma ligacao a serio, o tunel SSH continua a ser o caminho:

    ssh -L 8088:127.0.0.1:8088 <utilizador>@<maquina>

e depois abrir http://127.0.0.1:8088 no proprio PC.

DOIS TIPOS DE OPCAO, e a pagina mostra os dois separados:

  FIXAS       vem de Data/Items/ItemBonus.ini, sao do ITEM e iguais em toda copia dele.
              E o que o client ja desenha na caixa de texto sozinho. Nao se editam aqui:
              editar era mudar o item para toda a gente, e o client continuaria a mostrar
              o valor da tabela dele.
  DA INSTANCIA  ficam em dbo.personagem_item_bonus, presas a ESTA copia do item. Sao as
              que o jogo sorteia quando um item cai, e as que esta pagina deixa escolher.

AVISO SOBRE O QUE O JOGADOR VE. A caixa de texto do item e desenhada pelo CLIENT a partir
das tabelas dele, e o nosso servidor ainda nao sabe mandar opcoes de instancia no registo
do item. Ou seja: uma opcao posta aqui CONTA no combate e na ficha, mas nao aparece escrita
na caixa de texto. O caminho para ela aparecer e o `06 06` levar os pares (codigo, valor),
que ainda nao esta decifrado -- ver docs/opcoes-de-item.md.
"""
import base64
import hmac
import html
import http.server
import os
import sys
import urllib.parse

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

ENDERECO_LOCAL = "127.0.0.1"
ENDERECO_REDE = "0.0.0.0"
PORTA_PADRAO = 8088
SENHA = None            # posta por --publico; None = so aceita ligacoes locais
UTILIZADOR = "admin"
MAX_RESULTADOS = 60

# Tipos que o servidor sabe mesmo aplicar. E a mesma lista de db.TIPO_DE_BONUS, aqui com o
# nome que o client mostra -- oferecer os 98 casos do switch seria oferecer 90 que nao fazem
# nada, e o administrador nao teria como saber quais.
TIPOS_UTEIS = [
    (1, "Forca"), (2, "Vitalidade"), (3, "Destreza"), (4, "Sabedoria"),
    (5, "Inteligencia"), (6, "Forca de Ataque Fisico"), (8, "Poder magico. ataques"),
    (71, "Forca de ataque (sobe fisico E magico, use %)"),
    (10, "Classificacao de ataque"), (11, "Chance de Critico"),
    (12, "Multiplicador de critico"), (17, "Protecao fisica"), (19, "Magico. protecao"),
    (21, "Chance de evasao"), (22, "Chance de bloqueio"),
    (46, "Saude"), (47, "Mana"), (108, "Poder de ataque de habilidade"),
    (13, "Velocidade de ataque (use %)"),
    (30, "Regeneracao de HP"), (31, "Regeneracao do MP"),
    (59, "Experiencia (use %)"),
]
NOME_DO_TIPO = dict(TIPOS_UTEIS)


def valores_por_tipo():
    """{(tipo, unidade): [valores que o client sabe desenhar]}.

    O client nao le um numero: le um INDICE do uniqueOptionType, e o que ele escreve sai do
    skillData desse indice. Fora desses valores a opcao conta no combate e fica calada na
    caixa de texto.

    LE DO CATALOGO, e nao dos digitos do codigo. A versao anterior fazia `grupo % 100` para o
    tipo e tirava o valor do `grau`, e as duas contas erram: o indice 145 (codigo 70404) e
    "proteccao magica +12" e nao "sabedoria +4", e o jogador viu isso na tela do jogo em
    31/08. Ver db.catalogo_de_opcoes.
    """
    saida = {}
    for _idx, (tipo, valor, unidade) in db.catalogo_de_opcoes().items():
        saida.setdefault((int(tipo), unidade), set()).add(float(valor))
    return {k: sorted(v) for k, v in saida.items()}


def rotulo_do_tipo(tipo, nome, valores):
    """O nome do atributo mais os valores que aparecem escritos, por unidade."""
    partes = []
    for unidade, marca in (("abs", ""), ("%", "%")):
        v = valores.get((tipo, unidade))
        if not v:
            continue
        nums = ", ".join(("%g%s" % (x, marca)) for x in v[:6])
        partes.append(nums + (" ..." if len(v) > 6 else ""))
    if not partes:
        return "%s -- so no combate, nao aparece no item" % nome
    return "%s -- aparece com %s" % (nome, "  |  ".join(partes))


# ---------------------------------------------------------------------------
# leitura
# ---------------------------------------------------------------------------
def personagens():
    con = db._conectar()
    if con is None:
        return []
    cur = con.cursor()
    cur.execute("SELECT p.entity_id, p.nome, p.nivel, p.classe, c.usuario "
                "FROM dbo.personagem p JOIN dbo.conta c ON c.conta_id = p.conta_id "
                "ORDER BY c.usuario, p.nome")
    return [{"entity_id": int(a), "nome": b, "nivel": int(c), "classe": int(d), "conta": e}
            for a, b, c, d, e in cur.fetchall()]


def procurar_itens(termo, limite=MAX_RESULTADOS):
    """Por numero ou por pedaco do nome, sem distinguir maiusculas nem acentos."""
    termo = (termo or "").strip()
    if not termo:
        return []
    tabela = data_files.tabela("Item")
    if termo.isdigit():
        alvo = int(termo)
        exatos = [r for r in tabela if r.get("item_id") == alvo]
        if exatos:
            return exatos[:limite]
    chave = _simples(termo)
    saida = [r for r in tabela if chave in _simples(str(r.get("nome") or ""))]
    # nome mais curto primeiro: quem procura "asa" quer "Asas de anjo" antes de
    # "Caixa com asas de Anjo Caido (30 dias)"
    saida.sort(key=lambda r: (len(str(r.get("nome") or "")), r.get("item_id") or 0))
    return saida[:limite]


_ACENTOS = {"á": "a", "à": "a", "ã": "a", "â": "a", "é": "e", "ê": "e", "í": "i",
            "ó": "o", "õ": "o", "ô": "o", "ú": "u", "ü": "u", "ç": "c"}


def _simples(s):
    s = s.lower()
    for de, para in _ACENTOS.items():
        s = s.replace(de, para)
    return s


def bonus_fixos(item_id, refino):
    tabela = data_files.agrupar_composto("ItemBonus", ("item_id", "refino"))
    linhas = tabela.get((int(item_id), int(refino))) or tabela.get((int(item_id), 0)) or []
    return [(l.get("tipo"), l.get("nome"), float(l.get("valor") or 0),
             str(l.get("unidade") or "abs")) for l in linhas]


def inventario(entity_id):
    con = db._conectar()
    if con is None:
        return []
    cur = con.cursor()
    cur.execute("SELECT personagem_item_id, container, slot, item_id, quantidade, bruto "
                "FROM dbo.personagem_item WHERE entity_id = ? "
                "ORDER BY container, slot", int(entity_id))
    linhas = cur.fetchall()
    catalogo = data_files.indice("Item", "item_id")
    ids = [int(l[0]) for l in linhas]
    instancia = db.bonus_das_instancias(ids)
    saida = []
    for pid, cont, slot, item_id, qtd, bruto in linhas:
        b = bytes(bruto) if bruto else b""
        ref = b[db.OFF_REFINO_BRUTO] if len(b) > db.OFF_REFINO_BRUTO else 0
        ficha = catalogo.get(int(item_id)) or {}
        saida.append({
            "id": int(pid), "container": int(cont), "slot": int(slot),
            "item_id": int(item_id), "quantidade": int(qtd), "refino": int(ref),
            "nome": str(ficha.get("nome") or "?"),
            "fixos": bonus_fixos(item_id, ref),
            "instancia": instancia.get(int(pid), []),
        })
    return saida


# ---------------------------------------------------------------------------
# pagina
# ---------------------------------------------------------------------------
def e(s):
    return html.escape(str(s), quote=True)


ESTILO = """
:root{--fundo:#f6f7f9;--carta:#fff;--traco:#dfe3e8;--texto:#1c2530;--fraco:#68727e;
       --acento:#2f6fd0;--fixo:#8a6d1f;--fixo-fundo:#fdf6e3;--inst:#1f6b4a;--inst-fundo:#eaf6f0;}
@media (prefers-color-scheme:dark){:root{--fundo:#14181d;--carta:#1c2229;--traco:#2c343d;
       --texto:#e6eaef;--fraco:#96a1ad;--acento:#6fa5f0;--fixo:#e0bd63;--fixo-fundo:#2a2517;
       --inst:#77d3a6;--inst-fundo:#152a21;}}
*{box-sizing:border-box}
body{margin:0;padding:24px;background:var(--fundo);color:var(--texto);
     font:14px/1.5 "Segoe UI",system-ui,sans-serif}
h1{font-size:19px;margin:0 0 4px}
h2{font-size:15px;margin:0 0 12px}
.sub{color:var(--fraco);margin:0 0 20px;font-size:13px}
.grade{display:grid;grid-template-columns:repeat(auto-fit,minmax(430px,1fr));gap:18px;
       align-items:start;max-width:1500px}
.carta{background:var(--carta);border:1px solid var(--traco);border-radius:10px;padding:18px}
label{display:block;font-size:12px;color:var(--fraco);margin:12px 0 4px}
input,select{width:100%;padding:8px 10px;border:1px solid var(--traco);border-radius:6px;
             background:var(--carta);color:var(--texto);font:inherit}
button{padding:8px 16px;border:0;border-radius:6px;background:var(--acento);color:#fff;
       font:inherit;font-weight:600;cursor:pointer}
button.fraco{background:transparent;color:var(--fraco);border:1px solid var(--traco);
             font-weight:400;padding:4px 10px;font-size:12px}
table{width:100%;border-collapse:collapse;font-size:13px}
th{text-align:left;font-weight:600;color:var(--fraco);font-size:11px;text-transform:uppercase;
   letter-spacing:.4px;padding:6px 8px;border-bottom:1px solid var(--traco)}
td{padding:6px 8px;border-bottom:1px solid var(--traco);vertical-align:top}
tr:last-child td{border-bottom:0}
.num{font-variant-numeric:tabular-nums}
.eti{display:inline-block;padding:1px 7px;border-radius:99px;font-size:11px;margin:1px 3px 1px 0}
.fixo{background:var(--fixo-fundo);color:var(--fixo)}
.inst{background:var(--inst-fundo);color:var(--inst)}
.aviso{background:var(--fixo-fundo);color:var(--fixo);border-radius:6px;padding:10px 12px;
       font-size:12px;margin:0 0 16px}
.linha{display:grid;grid-template-columns:1fr 90px 70px 34px;gap:6px;margin-bottom:6px}
.linha input,.linha select{margin:0}
.ok{background:var(--inst-fundo);color:var(--inst);padding:10px 12px;border-radius:6px;
    margin:0 0 16px;font-size:13px}
.erro{background:#fdeaea;color:#a02020;padding:10px 12px;border-radius:6px;margin:0 0 16px}
@media (prefers-color-scheme:dark){.erro{background:#2e1a1a;color:#f0a0a0}}
a{color:var(--acento)}
.vazio{color:var(--fraco);font-style:italic;padding:10px 0}
"""

SCRIPT = """
function addLinha(){
  const c=document.getElementById('opcoes');
  const i=c.children.length;
  const d=document.createElement('div');
  d.className='linha';
  d.innerHTML=`<select name="tipo${i}">TIPOS</select>`
    +`<input name="valor${i}" type="number" step="0.01" placeholder="valor">`
    +`<select name="unidade${i}"><option value="abs">abs</option><option value="%">%</option></select>`
    +`<input name="vezes${i}" type="number" min="1" max="10" value="1" title="quantas vezes repetir esta opcao" style="width:56px">`
    +`<button type="button" class="fraco" onclick="this.parentNode.remove()">x</button>`;
  c.appendChild(d);
}
"""


def pagina(corpo, mensagem=None, erro=None):
    graus = valores_por_tipo()
    opcoes_tipo = "".join('<option value="%d">%s</option>'
                          % (t, e(rotulo_do_tipo(t, n, graus))) for t, n in TIPOS_UTEIS)
    js = SCRIPT.replace("TIPOS", opcoes_tipo.replace("`", ""))
    aviso = ""
    if mensagem:
        aviso += '<p class="ok">%s</p>' % e(mensagem)
    if erro:
        aviso += '<p class="erro">%s</p>' % e(erro)
    return ("""<!doctype html><html lang="pt"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Itens - administracao</title><style>%s</style></head><body>
<h1>Dar itens</h1>
<p class="sub">Rosh / Axion &mdash; so escuta em 127.0.0.1. Toda escrita e imediata no banco.</p>
%s%s<script>%s</script></body></html>""" % (ESTILO, aviso, corpo, js))


def form_dar(pers, sel, termo, achados, item_sel, refino):
    o = []
    o.append('<div class="carta"><h2>1. Personagem e item</h2><form method="get">')
    o.append('<label>Personagem%s</label>'
             % ('' if sel else ' &mdash; <b style="color:#c04040">escolha um primeiro</b>'))
    o.append('<select name="ent" onchange="this.form.submit()">')
    o.append('<option value="">-- escolher --</option>')
    for p in pers:
        o.append('<option value="%d"%s>%s &mdash; nivel %d (conta %s)</option>'
                 % (p["entity_id"], " selected" if p["entity_id"] == sel else "",
                    e(p["nome"]), p["nivel"], e(p["conta"])))
    o.append('</select>')
    o.append('<label>Procurar item &mdash; numero ou pedaco do nome</label>')
    o.append('<input name="q" value="%s" placeholder="asas de anjo, 999900058, bengala..." '
             'autofocus>' % e(termo or ""))
    o.append('<div style="margin-top:12px"><button>Procurar</button></div></form></div>')

    if achados:
        o.append('<div class="carta"><h2>2. Resultados (%d)</h2><table>' % len(achados))
        o.append('<tr><th>id</th><th>nome</th><th>nivel</th><th>slot</th><th></th></tr>')
        for r in achados:
            o.append('<tr><td class="num">%s</td><td>%s</td><td class="num">%s</td>'
                     '<td class="num">%s</td>'
                     '<td><a href="?ent=%s&q=%s&item=%s#escolhido">escolher</a></td></tr>'
                     % (e(r.get("item_id")), e(r.get("nome")), e(r.get("nivel_req") or 0),
                        e(r.get("slot") or 0), sel or "",
                        urllib.parse.quote(termo or ""), e(r.get("item_id"))))
        o.append('</table></div>')

    # O cartao aparece assim que ha ITEM, mesmo sem personagem escolhido.
    #
    # Antes exigia os dois, e sem personagem o clique em "escolher" nao produzia nada
    # visivel: a pagina recarregava igual e ficava a parecer avariada. Silencio e o pior
    # modo de recusar -- agora entra o cartao a dizer o que falta.
    if item_sel:
        fixos = bonus_fixos(item_sel["item_id"], refino)
        o.append('<div class="carta" id="escolhido"><h2>3. %s</h2>'
                 % e(item_sel.get("nome")))
        o.append('<p class="sub">item %s &mdash; nivel %s &mdash; slot %s</p>'
                 % (e(item_sel["item_id"]), e(item_sel.get("nivel_req") or 0),
                    e(item_sel.get("slot") or 0)))
        if fixos:
            o.append('<p style="font-size:12px;color:var(--fraco);margin:0 0 6px">'
                     'Caracteristicas fixas do item (o client ja as mostra):</p><p>')
            for _t, nome, valor, un in fixos:
                o.append('<span class="eti fixo">%s +%.2f%s</span>'
                         % (e(nome), valor, "%" if un == "%" else ""))
            o.append('</p>')
        else:
            o.append('<p class="sub">Este item nao tem caracteristicas fixas.</p>')
        if not sel:
            o.append('<p class="erro" style="margin:12px 0 0">'
                     '<b>Falta escolher o personagem.</b> Escolha-o na lista ali em cima '
                     'e volte a clicar em "escolher" neste item.</p></div>')
            return "".join(o)
        o.append('<form method="post" action="/dar">')
        o.append('<input type="hidden" name="ent" value="%d">' % sel)
        o.append('<input type="hidden" name="item" value="%d">' % item_sel["item_id"])
        o.append('<input type="hidden" name="q" value="%s">' % e(termo or ""))
        o.append('<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">')
        o.append('<div><label>Quantidade</label>'
                 '<input name="qtd" type="number" value="1" min="1"></div>')
        o.append('<div><label>Refino</label>'
                 '<input name="refino" type="number" value="%d" min="0" max="%d"></div>'
                 % (refino, db.REFINO_MAX))
        o.append('</div>')
        o.append('<label>Opcoes desta copia do item &mdash; somam por cima das '
                 'fixas</label>')
        o.append('<p class="aviso" style="margin:0 0 10px">'
                 '<b>Contam no combate e na ficha, mas nao aparecem na caixa de texto.</b> '
                 'A caixa e desenhada pelo CLIENT a partir das tabelas dele, e o nosso '
                 'servidor ainda nao sabe mandar opcoes de instancia no registo do item. '
                 'E o jogo original nao permite qualquer opcao em qualquer peca &mdash; '
                 'as tabelas de sorteio sao por slot, e a regra completa ainda nao esta '
                 'decifrada, entao aqui nao ha restricao nenhuma: o que puser, vale.<br>'
                 '<b>Para APARECER escrita no item</b>, a opcao tem de estar no catalogo do '
                 'client -- os valores possiveis vao ao lado do nome de cada atributo. Fora '
                 'deles conta no combate e fica calada.<br>'
                 '<b>Sao DEZ ranhuras</b> (medidas em 31/08): as cinco primeiras aparecem a '
                 'branco e sao as opcoes normais; as cinco seguintes aparecem a AMARELO e '
                 'sao as do refino. Da decima primeira em diante o client ignora.<br>'
                 '<b>Repetir a mesma opcao soma</b>, e e como se passa do maximo do '
                 'catalogo: "Velocidade de movimento 10% x5" da +50%.</p>')
        o.append('<div id="opcoes"></div>')
        o.append('<button type="button" class="fraco" onclick="addLinha()">'
                 '+ acrescentar opcao</button>')
        o.append('<div style="margin-top:16px"><button>Dar o item</button></div>')
        o.append('</form></div>')
    return "".join(o)


def form_inventario(sel, itens):
    if not sel:
        return ""
    o = ['<div class="carta"><h2>Inventario</h2>']
    if not itens:
        o.append('<p class="vazio">Sem itens.</p></div>')
        return "".join(o)
    o.append('<table><tr><th>onde</th><th>item</th><th>qtd</th>'
             '<th>caracteristicas</th><th></th></tr>')
    for it in itens:
        onde = ("equipado" if it["container"] == db.CONTAINER_EQUIPADO
                else "mochila") + " %d" % it["slot"]
        etiquetas = []
        for _t, nome, valor, un in it["fixos"]:
            etiquetas.append('<span class="eti fixo">%s +%.2f%s</span>'
                             % (e(nome), valor, "%" if un == "%" else ""))
        for tipo, valor, un in it["instancia"]:
            visivel = db.indice_de_opcao(tipo, valor, un) is not None
            etiquetas.append('<span class="eti inst"%s>%s %+.2f%s%s</span>'
                             % ('' if visivel else ' style="opacity:.55"',
                                e(NOME_DO_TIPO.get(tipo, "tipo %d" % tipo)), valor,
                                "%" if un == "%" else "",
                                "" if visivel else " (so no combate)"))
        o.append('<tr><td>%s</td><td>%s%s<br><span style="color:var(--fraco);font-size:11px">'
                 '%d</span></td><td class="num">%d</td><td>%s</td>'
                 '<td><form method="post" action="/apagar" style="margin:0">'
                 '<input type="hidden" name="id" value="%d">'
                 '<input type="hidden" name="ent" value="%d">'
                 '<button class="fraco">apagar</button></form></td></tr>'
                 % (e(onde), e(it["nome"]),
                    (" <b>+%d</b>" % it["refino"]) if it["refino"] else "",
                    it["item_id"], it["quantidade"],
                    "".join(etiquetas) or '<span style="color:var(--fraco)">&mdash;</span>',
                    it["id"], sel))
    o.append('</table><p class="sub" style="margin:12px 0 0">'
             '<span class="eti fixo">amarelo</span> fixa do item, da tabela do client &mdash; '
             '<span class="eti inst">verde</span> desta copia, guardada no banco</p></div>')
    return "".join(o)


# ---------------------------------------------------------------------------
# servidor
# ---------------------------------------------------------------------------
class Mao(http.server.BaseHTTPRequestHandler):
    server_version = "AdminRosh/1.0"

    def log_message(self, fmt, *args):
        print("[web] %s %s" % (self.client_address[0], fmt % args))

    def _autorizado(self):
        """Sem senha configurada, so passa quem vem da propria maquina."""
        if SENHA is None:
            return self.client_address[0] in ("127.0.0.1", "::1")
        cab = self.headers.get("Authorization") or ""
        if not cab.startswith("Basic "):
            return False
        try:
            dados = base64.b64decode(cab[6:]).decode("utf-8", "replace")
        except Exception:
            return False
        utilizador, _, senha = dados.partition(":")
        # comparacao de tempo constante: sem isto o tempo de resposta diz quantos
        # caracteres da senha estao certos
        return (hmac.compare_digest(utilizador, UTILIZADOR)
                and hmac.compare_digest(senha, SENHA))

    def _pedir_senha(self):
        self.send_response(401)
        self.send_header("WWW-Authenticate", 'Basic realm="Administracao Rosh"')
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()
        self.wfile.write("Precisa de senha.".encode("utf-8"))

    def _responder(self, corpo, codigo=200):
        dados = corpo.encode("utf-8")
        self.send_response(codigo)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(dados)))
        self.end_headers()
        self.wfile.write(dados)

    def _render(self, sel, termo, item_id, refino=0, mensagem=None, erro=None):
        data_files.carregar()
        pers = personagens()
        achados = procurar_itens(termo) if termo else []
        item_sel = None
        if item_id:
            item_sel = data_files.indice("Item", "item_id").get(int(item_id))
        corpo = ('<div class="grade">'
                 + form_dar(pers, sel, termo, achados, item_sel, refino)
                 + form_inventario(sel, inventario(sel) if sel else [])
                 + '</div>')
        self._responder(pagina(corpo, mensagem, erro))

    def do_GET(self):
        if not self._autorizado():
            self._pedir_senha()
            return
        u = urllib.parse.urlparse(self.path)
        if u.path not in ("/", "/index.html"):
            self._responder(pagina('<div class="carta">Nao existe.</div>'), 404)
            return
        q = urllib.parse.parse_qs(u.query)
        sel = int(q.get("ent", [0])[0] or 0)
        self._render(sel, q.get("q", [""])[0], q.get("item", [0])[0] or 0)

    def _corpo(self):
        n = int(self.headers.get("Content-Length") or 0)
        return urllib.parse.parse_qs(self.rfile.read(n).decode("utf-8"))

    def do_POST(self):
        if not self._autorizado():
            self._pedir_senha()
            return
        f = self._corpo()
        ent = int(f.get("ent", [0])[0] or 0)
        if self.path == "/apagar":
            ok = db.apagar_item(int(f.get("id", [0])[0] or 0))
            self._render(ent, "", 0,
                         mensagem="Item apagado." if ok else None,
                         erro=None if ok else "Nao consegui apagar.")
            return
        if self.path != "/dar":
            self._responder(pagina('<div class="carta">Nao existe.</div>'), 404)
            return

        item = int(f.get("item", [0])[0] or 0)
        qtd = max(1, int(f.get("qtd", [1])[0] or 1))
        refino = max(0, min(db.REFINO_MAX, int(f.get("refino", [0])[0] or 0)))
        termo = f.get("q", [""])[0]
        # As linhas de opcao sao numeradas pelo javascript; le-se enquanto houver.
        opcoes, i = [], 0
        while ("tipo%d" % i) in f:
            try:
                valor = float(f.get("valor%d" % i, ["0"])[0] or 0)
            except ValueError:
                valor = 0.0
            # REPETIR A MESMA OPCAO E LEGITIMO, e e o unico jeito de passar do valor
            # maximo do catalogo: a velocidade de movimento so vai ate +10%, mas cinco
            # linhas de +10% somam +50% e o client desenha as cinco. Ver
            # db.N_OPCOES_NO_REGISTO para as dez ranhuras (cinco normais, cinco de refino).
            try:
                vezes = max(1, min(10, int(f.get("vezes%d" % i, ["1"])[0] or 1)))
            except ValueError:
                vezes = 1
            if valor:
                for _ in range(vezes):
                    opcoes.append((int(f["tipo%d" % i][0]),
                                   valor,
                                   f.get("unidade%d" % i, ["abs"])[0]))
            i += 1

        r = db.dar_item(ent, item, qtd, refino, opcoes)
        if r["ok"]:
            extra = (" com %d opcao(oes) propria(s)" % len(opcoes)) if opcoes else ""
            msg = ("Item %d posto na mochila, slot %s, quantidade %d, refino +%d%s. "
                   "O jogador tem de reentrar para o ver."
                   % (item, r["slot"], qtd, refino, extra))
            self._render(ent, termo, item, refino, mensagem=msg)
        else:
            self._render(ent, termo, item, refino, erro="Falhou: %s" % r["erro"])


def main() -> int:
    global SENHA
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    publico = "--publico" in sys.argv
    if publico:
        i = sys.argv.index("--publico")
        SENHA = sys.argv[i + 1] if i + 1 < len(sys.argv) else ""
        if len(SENHA) < 8:
            print("--publico exige uma senha de 8 caracteres ou mais.")
            print("   python admin/web.py --publico <senha> [porta]")
            return 2
        args = [a for a in args if a != SENHA]
    porta = int(args[0]) if args else PORTA_PADRAO
    endereco = ENDERECO_REDE if publico else ENDERECO_LOCAL

    data_files.carregar()
    srv = http.server.ThreadingHTTPServer((endereco, porta), Mao)
    if publico:
        print("administracao de itens na REDE, porta %d" % porta)
        print("   utilizador: %s   senha: a que passou em --publico" % UTILIZADOR)
        print("   a senha viaja em claro (HTTP Basic). Feche quando acabar.")
    else:
        print("administracao de itens em http://127.0.0.1:%d/" % porta)
        print("   so local. Para chegar de outra maquina: --publico <senha>")
    try:
        srv.serve_forever()
    except KeyboardInterrupt:
        print()
        print("fechado")
    return 0


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