import os
import time
import json
import re
import threading
import logging
import sys
import telebot
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# ─────────────────────────────────────────────────────────
# LOGGING SETUP (saves to bot.log file + console)
# ─────────────────────────────────────────────────────────
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler(
            os.path.join(os.path.dirname(os.path.abspath(__file__)), 'bot.log'),
            encoding='utf-8'
        ),
        logging.StreamHandler(sys.stdout)
    ]
)
log = logging.getLogger(__name__)

# ─────────────────────────────────────────────────────────
# CONFIGURATION
# ─────────────────────────────────────────────────────────
DIR_PATH = os.path.dirname(os.path.abspath(__file__))

with open(os.path.join(DIR_PATH, 'config.json'), 'r') as f:
    config = json.load(f)

BOT_TOKEN    = config['bot_token']
ALLOWED_USERS = config['allowed_users']
DEX_USERNAME = config['dex_username']
DEX_PASSWORD = config['dex_password']
BASE_URL     = 'https://dexshellx.com'

# ─────────────────────────────────────────────────────────
# STATE (thread-safe)
# ─────────────────────────────────────────────────────────
state_lock   = threading.Lock()
waiting_apks = {}       # chat_id -> {'path': ..., 'orig_name': ...}
processing   = set()    # chat_ids currently being processed

bot = telebot.TeleBot(BOT_TOKEN, threaded=True)

# ─────────────────────────────────────────────────────────
# HELPERS
# ─────────────────────────────────────────────────────────
def is_authorized(message):
    return message.chat.id in ALLOWED_USERS

def safe_send(chat_id, text):
    """Send a Telegram message, never crash the worker thread."""
    try:
        bot.send_message(chat_id, text)
    except Exception as e:
        log.error(f"[{chat_id}] send_message failed: {e}")

def get_session():
    session = requests.Session()
    retries = Retry(total=4, backoff_factor=1.5, status_forcelist=[429, 500, 502, 503, 504])
    adapter = HTTPAdapter(max_retries=retries)
    session.mount('https://', adapter)
    session.mount('http://', adapter)
    session.headers.update({
        'User-Agent'     : ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                            'AppleWebKit/537.36 (KHTML, like Gecko) '
                            'Chrome/125.0.0.0 Safari/537.36'),
        'Accept-Language': 'en-US,en;q=0.9',
        'Connection'     : 'keep-alive',
    })
    return session

def extract_process_id(url, html):
    """Try every possible pattern to extract the process ID."""
    patterns = [
        (url,  r'process/detail\?id=(\d+)'),
        (html, r'data-process-id=["\']?(\d+)["\']?'),
        (html, r'<title>Process\s+([\d,]+)', lambda x: x.replace(',', '')),
        (html, r'<h1>Process\s*<code>([\d,]+)</code>', lambda x: x.replace(',', '')),
        (html, r'process/detail\?id=(\d+)'),
        (html, r'name=["\']processId["\'][^>]+value=["\'](\d+)["\']'),
        (html, r'/dex/process/download\?id=(\d+)'),
    ]
    for args in patterns:
        src, pat = args[0], args[1]
        transform = args[2] if len(args) > 2 else None
        m = re.search(pat, src)
        if m:
            val = m.group(1)
            return transform(val) if transform else val

    # JSON response fallback
    try:
        j = json.loads(html)
        return str(j.get('id') or j.get('processId') or '')
    except Exception:
        pass

    return None

def heartbeat(chat_id, stop_event):
    """Sends a ping every 60 seconds while protection is running."""
    for _ in range(60):
        if stop_event.is_set():
            return
        time.sleep(1)
    while not stop_event.is_set():
        safe_send(chat_id, "⏳ Still working on your APK... Please wait.")
        for _ in range(60):
            if stop_event.is_set():
                break
            time.sleep(1)

# ─────────────────────────────────────────────────────────
# BOT HANDLERS
# ─────────────────────────────────────────────────────────
@bot.message_handler(commands=['start', 'help'])
def cmd_start(message):
    if not is_authorized(message):
        bot.reply_to(message, "🚫 You are not authorized to use this bot.")
        return
    bot.reply_to(message,
        "👋 Welcome! I protect your APK files.\n\n"
        "📤 How to use:\n"
        "1. Send your .apk file.\n"
        "2. Add a caption with filter text (e.g. your package name), OR send it separately after upload.\n"
        "3. Wait 2–5 minutes.\n"
        "4. Receive your protected APK! 🛡️"
    )

@bot.message_handler(commands=['cancel'])
def cmd_cancel(message):
    chat_id = message.chat.id
    if not is_authorized(message):
        return
    with state_lock:
        removed = waiting_apks.pop(chat_id, None)
    if removed and os.path.exists(removed['path']):
        os.remove(removed['path'])
    bot.reply_to(message, "🗑️ Cancelled. Send a new APK whenever you're ready.")

@bot.message_handler(content_types=['document'])
def handle_document(message):
    chat_id = message.chat.id
    if not is_authorized(message):
        bot.send_message(chat_id, "🚫 You are not authorized to use this bot.")
        return

    file_name = message.document.file_name or "upload.apk"
    if not file_name.lower().endswith('.apk'):
        bot.send_message(chat_id, "❌ Please send a valid .apk file.")
        return

    with state_lock:
        if chat_id in processing:
            bot.send_message(chat_id, "⚠️ Your previous APK is still being processed. Please wait.")
            return

    try:
        bot.send_message(chat_id, "📥 Downloading APK from Telegram...")
        file_info = bot.get_file(message.document.file_id)
        downloaded = bot.download_file(file_info.file_path)
    except Exception as e:
        log.error(f"[{chat_id}] Download from Telegram failed: {e}")
        bot.send_message(chat_id, "❌ Failed to download APK from Telegram. Please try again.")
        return

    local_path = os.path.join(DIR_PATH, f"temp_{chat_id}_{int(time.time())}.apk")
    with open(local_path, 'wb') as fp:
        fp.write(downloaded)

    caption = (message.caption or "").strip()

    if caption:
        bot.send_message(chat_id, "✅ APK & filters received! Starting protection...\n⏳ Please wait 2–5 minutes.")
        threading.Thread(target=process_apk, args=(chat_id, local_path, caption, file_name), daemon=True).start()
    else:
        with state_lock:
            # Remove old waiting APK if any
            old = waiting_apks.pop(chat_id, None)
            if old and os.path.exists(old['path']):
                os.remove(old['path'])
            waiting_apks[chat_id] = {'path': local_path, 'orig_name': file_name}
        bot.send_message(chat_id,
            "📥 APK received!\n\n"
            "Now please send your filter text (e.g. package name like `com.example.app`).\n"
            "Send /cancel to abort."
        )

@bot.message_handler(func=lambda message: True)
def handle_text(message):
    chat_id = message.chat.id
    if not is_authorized(message):
        return

    with state_lock:
        apk_data = waiting_apks.pop(chat_id, None)

    if apk_data:
        filters = message.text.strip()
        if not filters:
            with state_lock:
                waiting_apks[chat_id] = apk_data  # put it back
            bot.send_message(chat_id, "⚠️ Filter text cannot be empty. Please send your package name or filter text.")
            return
        bot.send_message(chat_id, "✅ Filters received! Starting protection...\n⏳ Please wait 2–5 minutes.")
        threading.Thread(
            target=process_apk,
            args=(chat_id, apk_data['path'], filters, apk_data['orig_name']),
            daemon=True
        ).start()
    else:
        bot.send_message(chat_id, "⚠️ No APK is waiting for filters. Please send an APK file first.")

# ─────────────────────────────────────────────────────────
# CORE PROTECTION WORKER
# ─────────────────────────────────────────────────────────
def process_apk(chat_id, apk_path, filters, orig_name):
    with state_lock:
        processing.add(chat_id)

    session = get_session()
    stop_heartbeat = threading.Event()
    hb_thread = threading.Thread(target=heartbeat, args=(chat_id, stop_heartbeat), daemon=True)

    try:
        log.info(f"[{chat_id}] Started processing: {orig_name}")
        safe_send(chat_id, "⚙️ Connecting to protection server...")

        # ── STEP 1: Homepage (get session cookie) ──────────
        try:
            r1 = session.get(BASE_URL + '/', timeout=30)
            r1.raise_for_status()
        except Exception as e:
            log.error(f"[{chat_id}] Homepage failed: {e}")
            safe_send(chat_id, "❌ Cannot reach protection server. Please try again later.")
            return

        # ── STEP 2: Login ──────────────────────────────────
        try:
            session.get(BASE_URL + '/dex/login', timeout=30)  # seed session
            r2 = session.post(
                BASE_URL + '/dex/login',
                data={'user': DEX_USERNAME, 'pass': DEX_PASSWORD, 'language': 'en_US'},
                headers={'Referer': BASE_URL + '/dex/login'},
                timeout=30,
                allow_redirects=True
            )
        except Exception as e:
            log.error(f"[{chat_id}] Login request failed: {e}")
            safe_send(chat_id, "❌ Could not connect to protection server. Please try again.")
            return

        # Login success = redirected to dashboard or away from /login
        login_success = ('dashboard' in r2.url) or ('dex/login' not in r2.url and r2.status_code == 200)
        if not login_success or ('Invalid' in r2.text and 'dashboard' not in r2.url):
            log.warning(f"[{chat_id}] Login failed. Final URL: {r2.url}")
            safe_send(chat_id, "❌ Login failed. Please check credentials in config.json.")
            return

        log.info(f"[{chat_id}] Login successful.")
        safe_send(chat_id, "✅ Login successful!\n\n📤 Uploading APK... This takes a few minutes.")
        hb_thread.start()

        # ── STEP 3: Upload APK ──────────────────────────────
        try:
            with open(apk_path, 'rb') as f:
                r3 = session.post(
                    BASE_URL + '/dex/protection/options',
                    files={'apkFile': (orig_name, f, 'application/vnd.android.package-archive')},
                    data={
                        'mode': 'standard', 'userModeValue': 'false',
                        'signatureAlias': 'android',
                        'stringFilters': filters, 'hideFilters': filters, 'classFilters': filters,
                        'optimize': 'on', 'stripLogging': 'on', 'crashHandler': 'on',
                        'webViewSupport': 'on', 'manifestMangling': 'on',
                        'assets': 'on', 'res': 'on', 'nameObf': 'on',
                        'root': 'on', 'strings': 'on',
                        'annotationEnc': 'on', 'stringEnc': 'on',
                        'hideAccess': 'on', 'classEnc': 'on',
                        'jniObf': 'on', 'nativeLib': 'on',
                        'antiEmulator': 'on', 'antiXposed': 'on'
                    },
                    headers={
                        'Origin'  : BASE_URL,
                        'Referer' : BASE_URL + '/dex/protection/options?mode=standard'
                    },
                    timeout=300
                )
        except Exception as e:
            log.error(f"[{chat_id}] Upload failed: {e}")
            safe_send(chat_id, "❌ APK upload failed. Please try again.")
            return

        log.info(f"[{chat_id}] Upload response: HTTP {r3.status_code}, URL: {r3.url}")

        # ── STEP 4: Extract Process ID ─────────────────────
        process_id = extract_process_id(r3.url, r3.text)

        if not process_id:
            debug_path = os.path.join(DIR_PATH, f'upload_debug_{chat_id}.html')
            with open(debug_path, 'w', encoding='utf-8', errors='replace') as df:
                df.write(r3.text)
            log.error(f"[{chat_id}] No process ID found. Debug saved to {debug_path}")
            safe_send(chat_id, "❌ Upload failed — server did not return a process ID.\nPlease try again or contact support.")
            return

        log.info(f"[{chat_id}] Process ID: {process_id}")
        safe_send(chat_id, "📋 APK uploaded! Protection is running on server, please wait...")

        # ── STEP 5: Poll for completion ────────────────────
        poll_url   = f"{BASE_URL}/dex/process/detail/data?id={process_id}"
        detail_url = f"{BASE_URL}/dex/process/detail?id={process_id}"
        max_wait = 900
        interval = 15
        waited   = 0
        status   = 'RUNNING'

        while waited < max_wait:
            time.sleep(interval)
            waited += interval

            try:
                r4 = session.get(
                    poll_url,
                    headers={'Referer': detail_url, 'Accept': 'application/json'},
                    timeout=30
                )
                try:
                    j = r4.json()
                    status = j.get('status', 'RUNNING').upper()
                    log.info(f"[{chat_id}] Poll [{waited}s]: {status} (JSON)")
                except Exception:
                    m = re.search(r'data-status=["\']([^"\']+)["\']', r4.text)
                    status = m.group(1).upper() if m else status
                    log.info(f"[{chat_id}] Poll [{waited}s]: {status} (HTML)")
            except Exception as e:
                log.warning(f"[{chat_id}] Poll error: {e}")
                continue

            if status in ('DONE', 'SUCCESS', 'FINISHED', 'COMPLETED'):
                break
            if status in ('ERROR', 'FAILED', 'FAILURE'):
                safe_send(chat_id, "❌ Protection failed on server. Please try again.")
                return

        if status not in ('DONE', 'SUCCESS', 'FINISHED', 'COMPLETED'):
            safe_send(chat_id, "⏰ Timeout — protection is taking too long. Please try again later.")
            return

        stop_heartbeat.set()
        safe_send(chat_id, f"🛡️ Protection complete! ({waited}s)\n⬇️ Downloading your protected APK...")

        # ── STEP 6: Find download link ─────────────────────
        try:
            r5 = session.get(detail_url, timeout=30)
        except Exception as e:
            log.error(f"[{chat_id}] Detail page fetch failed: {e}")
            safe_send(chat_id, "❌ Could not fetch the download link. Please try again.")
            return

        m = re.search(
            rf'href=["\']([^"\']*process/download\?id={process_id}[^"\']*type=apk[^"\']*)["\']',
            r5.text
        )
        if not m:
            log.error(f"[{chat_id}] Download link not found in detail page.")
            safe_send(chat_id, "❌ Could not find the download link. Please try again.")
            return

        download_url = BASE_URL + m.group(1).replace('&amp;', '&')
        log.info(f"[{chat_id}] Downloading from: {download_url[:80]}...")

        # ── STEP 7: Download and send APK ──────────────────
        try:
            r6 = session.get(
                download_url,
                stream=True,
                headers={'Referer': detail_url},
                timeout=300
            )
        except Exception as e:
            log.error(f"[{chat_id}] Download failed: {e}")
            safe_send(chat_id, "❌ Download failed. Please try again.")
            return

        content_type = r6.headers.get('Content-Type', '')
        if r6.status_code != 200 or not any(x in content_type for x in ['package-archive', 'octet-stream', 'zip']):
            log.error(f"[{chat_id}] Bad download: HTTP {r6.status_code}, Content-Type: {content_type}")
            safe_send(chat_id, "❌ Download failed. The server returned an invalid file. Please try again.")
            return

        safe_name = re.sub(r'[^a-zA-Z0-9._\-]', '_', orig_name)
        if not safe_name.lower().endswith('.apk'):
            safe_name += '.apk'
        out_path = os.path.join(DIR_PATH, f"out_{chat_id}_{safe_name}")

        with open(out_path, 'wb') as fp:
            for chunk in r6.iter_content(chunk_size=65536):
                if chunk:
                    fp.write(chunk)

        size_kb = round(os.path.getsize(out_path) / 1024)
        log.info(f"[{chat_id}] Downloaded {size_kb} KB. Sending...")
        safe_send(chat_id, f"🛡️ Sending protected APK ({size_kb} KB)...")

        try:
            with open(out_path, 'rb') as fp:
                bot.send_document(chat_id, fp, visible_file_name=safe_name)
            safe_send(chat_id, "✅ Done! Your protected APK has been sent.")
            log.info(f"[{chat_id}] Successfully sent {safe_name}")
        except Exception as e:
            log.error(f"[{chat_id}] send_document failed: {e}")
            safe_send(chat_id, "❌ Failed to send APK to Telegram. Please try again.")
        finally:
            if os.path.exists(out_path):
                os.remove(out_path)

    except Exception as e:
        log.exception(f"[{chat_id}] Unexpected error: {e}")
        safe_send(chat_id, "❌ An unexpected error occurred. Please try again.")
    finally:
        stop_heartbeat.set()
        with state_lock:
            processing.discard(chat_id)
        if os.path.exists(apk_path):
            os.remove(apk_path)
        log.info(f"[{chat_id}] Worker finished. Cleaned up.")

# ─────────────────────────────────────────────────────────
# STARTUP
# ─────────────────────────────────────────────────────────
if __name__ == '__main__':
    # Delete any existing webhook so long-polling works
    try:
        result = requests.get(
            f'https://api.telegram.org/bot{BOT_TOKEN}/deleteWebhook',
            timeout=10
        ).json()
        if result.get('result'):
            log.info("Webhook deleted successfully.")
        else:
            log.warning(f"deleteWebhook response: {result}")
    except Exception as e:
        log.warning(f"Could not delete webhook: {e}")

    log.info("Bot is starting... Listening for messages.")
    bot.infinity_polling(timeout=60, long_polling_timeout=60)
