import html
import os
import time
import requests
import logging
import threading
from datetime import datetime, timedelta
import mysql.connector

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# ==================== НАСТРОЙКИ ====================
TELEGRAM_TOKEN = '8141522208:AAHZxJdI76anqke2CkxXf4wYMAxZREstmd4'

# DexScreener: 20 токенов через запятую в одном запросе, 1 запрос в секунду
DEXSCREENER_BATCH_SIZE = 20
DEXSCREENER_REQUEST_INTERVAL = 1

# Сколько дней токен хранится на отслеживании (потом удаляется и пишется в историю)
PENDING_EXPIRE_DAYS = 30

# Четыре группы
GROUP_1_10 = '-1004299250656'
GROUP_10_250 = '-1003976017790'
GROUP_250_700 = '-1004310384363'
GROUP_700_PLUS = '-1004463516516'

# ---------------- HTML-СТРАНИЦА ----------------
WEB_TOKEN_TTL_MINUTES = 60
WEB_HTML_PATH = '/var/www/html/telebot/tokens.html'
WEB_REFRESH_SECONDS = 15
WEB_BROWSER_REFRESH = 30
# ==================================================

DB_CONFIG = {
    'host': 'localhost',
    'user': 'admin',
    'password': 'G_0k2V8Y6-6Sege!DAx',
    'database': 'pump_bot'
}
# ==================================================

def init_db():
    conn = mysql.connector.connect(**DB_CONFIG)
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS tokens (
            ca VARCHAR(100) PRIMARY KEY,
            name VARCHAR(255) DEFAULT NULL,
            symbol VARCHAR(50) DEFAULT NULL,
            added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            expires_at DATETIME NOT NULL,
            sent BOOLEAN DEFAULT FALSE
        )
    ''')
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS tokens_history (
            ca VARCHAR(100) PRIMARY KEY,
            name VARCHAR(255) DEFAULT NULL,
            symbol VARCHAR(50) DEFAULT NULL,
            first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
            last_sent DATETIME DEFAULT NULL,
            sent_count INT DEFAULT 0
        )
    ''')
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS sent_mints (
            mint VARCHAR(100) PRIMARY KEY,
            template_ca VARCHAR(100),
            initial_market_cap DECIMAL(20,2) DEFAULT NULL,
            current_market_cap DECIMAL(20,2) DEFAULT NULL,
            chat_id VARCHAR(100) DEFAULT NULL,
            name VARCHAR(255) DEFAULT NULL,
            symbol VARCHAR(50) DEFAULT NULL,
            age_days INT DEFAULT NULL,
            ath_market_cap DECIMAL(20,2) DEFAULT NULL,
            first_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            last_checked_at DATETIME DEFAULT NULL,
            send_after_at DATETIME DEFAULT NULL,
            status VARCHAR(20) DEFAULT 'pending',
            sent_at DATETIME DEFAULT NULL,
            skipped_at DATETIME DEFAULT NULL
        )
    ''')
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS web_tokens (
            ca VARCHAR(100) PRIMARY KEY,
            name VARCHAR(255) DEFAULT NULL,
            symbol VARCHAR(50) DEFAULT NULL,
            market_cap DECIMAL(20,2) DEFAULT NULL,
            added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            created_at DATETIME DEFAULT NULL
        )
    ''')
    cursor.execute("SHOW COLUMNS FROM sent_mints")
    existing_columns = {row[0] for row in cursor.fetchall()}

    column_definitions = {
        'sent_at': "ALTER TABLE sent_mints ADD COLUMN sent_at DATETIME DEFAULT NULL",
        'initial_market_cap': "ALTER TABLE sent_mints ADD COLUMN initial_market_cap DECIMAL(20,2) DEFAULT NULL",
        'current_market_cap': "ALTER TABLE sent_mints ADD COLUMN current_market_cap DECIMAL(20,2) DEFAULT NULL",
        'chat_id': "ALTER TABLE sent_mints ADD COLUMN chat_id VARCHAR(100) DEFAULT NULL",
        'name': "ALTER TABLE sent_mints ADD COLUMN name VARCHAR(255) DEFAULT NULL",
        'symbol': "ALTER TABLE sent_mints ADD COLUMN symbol VARCHAR(50) DEFAULT NULL",
        'age_days': "ALTER TABLE sent_mints ADD COLUMN age_days INT DEFAULT NULL",
        'ath_market_cap': "ALTER TABLE sent_mints ADD COLUMN ath_market_cap DECIMAL(20,2) DEFAULT NULL",
        'first_seen_at': "ALTER TABLE sent_mints ADD COLUMN first_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP",
        'last_checked_at': "ALTER TABLE sent_mints ADD COLUMN last_checked_at DATETIME DEFAULT NULL",
        'send_after_at': "ALTER TABLE sent_mints ADD COLUMN send_after_at DATETIME DEFAULT NULL",
        'status': "ALTER TABLE sent_mints ADD COLUMN status VARCHAR(20) DEFAULT 'pending'",
        'skipped_at': "ALTER TABLE sent_mints ADD COLUMN skipped_at DATETIME DEFAULT NULL"
    }

    for column_name, alter_sql in column_definitions.items():
        if column_name not in existing_columns:
            cursor.execute(alter_sql)

    # Новая колонка в web_tokens — дата создания токена
    cursor.execute("SHOW COLUMNS FROM web_tokens")
    web_columns = {row[0] for row in cursor.fetchall()}
    if 'created_at' not in web_columns:
        cursor.execute("ALTER TABLE web_tokens ADD COLUMN created_at DATETIME DEFAULT NULL")

    conn.commit()
    cursor.close()
    conn.close()
    logging.info("База данных инициализирована")

def is_already_sent(mint):
    """Проверяет, не обрабатывался ли токен уже (в работе или в истории)"""
    if not mint:
        return False
    conn = mysql.connector.connect(**DB_CONFIG)
    cursor = conn.cursor()
    cursor.execute("SELECT mint FROM sent_mints WHERE mint = %s", (mint,))
    exists = cursor.fetchone() is not None
    if exists:
        cursor.close()
        conn.close()
        return True
    cursor.execute("SELECT ca FROM tokens_history WHERE ca = %s", (mint,))
    exists = cursor.fetchone() is not None
    cursor.close()
    conn.close()
    return exists

def add_to_history(mint, name=None, symbol=None):
    """Записывает отправленный/удалённый токен в историю для защиты от повторок"""
    if not mint:
        return
    conn = mysql.connector.connect(**DB_CONFIG)
    cursor = conn.cursor()
    cursor.execute("""
        INSERT INTO tokens_history (ca, name, symbol, first_seen, last_sent, sent_count)
        VALUES (%s, %s, %s, NOW(), NOW(), 1)
        ON DUPLICATE KEY UPDATE last_sent = NOW(), sent_count = sent_count + 1
    """, (mint, name, symbol))
    conn.commit()
    cursor.close()
    conn.close()

def get_market_caps_batch(cas):
    """
    ОДИН запрос к DexScreener сразу на несколько токенов (до 20 CA через запятую):
    https://api.dexscreener.com/latest/dex/tokens/{ca1},{ca2},...
    Возвращает словарь {ca: market_cap}.
    """
    result = {}
    cas = [ca for ca in cas if ca]
    if not cas:
        return result
    url = 'https://api.dexscreener.com/latest/dex/tokens/' + ','.join(cas)
    try:
        r = requests.get(url, timeout=15)
        if r.status_code != 200:
            logging.warning(f"Dexscreener batch HTTP {r.status_code} для {len(cas)} токенов")
            return result
        data = r.json()
        if not isinstance(data, dict):
            return result
        pairs = data.get('pairs') or []
        if not isinstance(pairs, list):
            return result
        for pair in pairs:
            if not isinstance(pair, dict):
                continue
            base = pair.get('baseToken') or {}
            ca = base.get('address')
            if not ca or ca in result:
                continue
            market_cap = pair.get('marketCap')
            if market_cap is None:
                market_cap = pair.get('market_cap')
            if market_cap is None:
                continue
            try:
                result[ca] = float(market_cap)
            except (TypeError, ValueError):
                continue
    except Exception as e:
        logging.warning(f"Dexscreener batch error: {e}")
    return result

def register_pending_mint(mint, template_ca, chat_id, name=None, symbol=None, age_days=None, ath_market_cap=None):
    if not mint:
        return False

    conn = mysql.connector.connect(**DB_CONFIG)
    cursor = conn.cursor()
    cursor.execute("SELECT mint FROM sent_mints WHERE mint = %s", (mint,))
    if cursor.fetchone():
        cursor.close()
        conn.close()
        return False

    send_after = datetime.now() + timedelta(days=PENDING_EXPIRE_DAYS)
    cursor.execute("""
        INSERT INTO sent_mints (
            mint, template_ca, initial_market_cap, current_market_cap, chat_id, name, symbol, age_days, ath_market_cap,
            first_seen_at, last_checked_at, send_after_at, status
        ) VALUES (%s, %s, NULL, NULL, %s, %s, %s, %s, %s, NOW(), NULL, %s, 'pending')
    """, (mint, template_ca, chat_id, name, symbol, age_days, ath_market_cap, send_after))
    conn.commit()
    cursor.close()
    conn.close()
    logging.info(f"Добавлен на отслеживание market cap: {mint}")
    return True

def update_pending_mints():
    """Один цикл = одна батч-проверка 20 токенов (1 запрос к DexScreener)"""
    conn = mysql.connector.connect(**DB_CONFIG)
    try:
        cursor = conn.cursor(dictionary=True)
        cursor.execute("""
            SELECT mint, template_ca, initial_market_cap, current_market_cap, chat_id,
                   name, symbol, age_days, ath_market_cap, send_after_at
            FROM sent_mints
            WHERE status = 'pending'
            ORDER BY last_checked_at ASC, first_seen_at ASC
            LIMIT %s
        """, (DEXSCREENER_BATCH_SIZE,))
        rows = cursor.fetchall()
        cursor.close()
        if not rows:
            return

        now = datetime.now()
        active = []
        for row in rows:
            if row['send_after_at'] is not None and now >= row['send_after_at']:
                add_to_history(row['mint'], row['name'], row['symbol'])
                cur = conn.cursor()
                cur.execute("DELETE FROM sent_mints WHERE mint = %s", (row['mint'],))
                conn.commit()
                cur.close()
                logging.info(f"Токен не попал в диапазон market cap 1000-3300 за {PENDING_EXPIRE_DAYS} дней и удалён из БД: {row['mint']}")
            else:
                active.append(row)

        if not active:
            return

        caps = get_market_caps_batch([row['mint'] for row in active])

        for row in active:
            mint = row['mint']
            mc = caps.get(mint)
            cursor = conn.cursor()

            if mc is None:
                cursor.execute("UPDATE sent_mints SET last_checked_at = NOW() WHERE mint = %s", (mint,))
                conn.commit()
                cursor.close()
                continue

            if row['initial_market_cap'] is None:
                cursor.execute(
                    "UPDATE sent_mints SET initial_market_cap = %s, current_market_cap = %s, last_checked_at = NOW() WHERE mint = %s",
                    (mc, mc, mint)
                )
            else:
                cursor.execute(
                    "UPDATE sent_mints SET current_market_cap = %s, last_checked_at = NOW() WHERE mint = %s",
                    (mc, mint)
                )
            conn.commit()
            cursor.close()

            if should_send_by_market_cap(mc):
                if send_to_telegram(
                    row['name'] or 'Unknown',
                    row['symbol'] or 'UNK',
                    mint,
                    row['age_days'] or 0,
                    row['ath_market_cap'] or 0,
                    mc,
                    row['chat_id']
                ):
                    cursor = conn.cursor()
                    cursor.execute(
                        "UPDATE sent_mints SET status = 'sent', sent_at = NOW(), current_market_cap = %s WHERE mint = %s",
                        (mc, mint)
                    )
                    conn.commit()
                    cursor.close()
                    add_to_history(mint, row['name'], row['symbol'])
                    delete_sent_mint_record(mint)

                    # ===== Дата создания токена для HTML-страницы =====
                    age_days = row['age_days']
                    if age_days:
                        try:
                            token_created_at = datetime.now() - timedelta(days=int(age_days))
                        except (TypeError, ValueError):
                            token_created_at = None
                    else:
                        token_created_at = None

                    add_to_web_page(mint, row['name'], row['symbol'], mc, token_created_at)
                    logging.info(f"Отправлен в TG по диапазону market cap 1000-3300: {mint}")
    finally:
        conn.close()

def should_send_by_market_cap(current_market_cap):
    try:
        value = float(current_market_cap)
    except (TypeError, ValueError):
        return False
    return 1000.0 <= value <= 3300.0

def get_group_by_age(days_old):
    if days_old <= 10:
        return GROUP_1_10
    elif days_old < 250:
        return GROUP_10_250
    elif days_old <= 700:
        return GROUP_250_700
    else:
        return GROUP_700_PLUS

def check_new_coins():
    try:
        r = requests.get('https://frontend-api-v3.pump.fun/coins?offset=0&limit=60&sort=last_trade_timestamp&includeNsfw=false&order=DESC', timeout=15)
        if r.status_code != 200:
            return
        coins = r.json()

        for coin in coins:
            mint = coin.get('mint', '')
            if not mint:
                continue

            if is_already_sent(mint):
                continue

            created_ts = coin.get('created_timestamp')
            if not created_ts:
                continue

            age_days = (datetime.now() - datetime.fromtimestamp(created_ts / 1000)).days

            # Фильтр 1: возраст от 250 дней
            if age_days < 250:
                continue

            # Фильтр 2: не должно быть complete == True
            if coin.get('complete') is True:
                continue

            mc = coin.get('usd_market_cap', 0)
            ath = coin.get('ath_market_cap', 0)

            # Фильтр 3: ATH не должно превышать 40000
            if ath > 40000:
                continue

            if mc <= 0:
                continue

            name = coin.get('name')
            symbol = coin.get('symbol')
            template_ca = mint

            group_id = get_group_by_age(age_days)
            register_pending_mint(
                mint,
                template_ca,
                group_id,
                name,
                symbol,
                age_days,
                ath
            )
    except Exception as e:
        logging.error(f"check_new_coins error: {e}")

def send_to_telegram(name, symbol, mint, age_days, ath, usd_mc, chat_id):
    name = str(name or "Unknown")
    symbol = str(symbol or "UNK")
    mint = str(mint or "")

    try:
        age_days = int(age_days or 0)
    except Exception:
        age_days = 0
    try:
        ath = float(ath or 0)
    except Exception:
        ath = 0.0
    try:
        usd_mc = float(usd_mc or 0)
    except Exception:
        usd_mc = 0.0

    safe_name = html.escape(name)
    safe_symbol = html.escape(symbol)
    safe_mint = html.escape(mint)

    text = (
        f"{safe_name} ({safe_symbol})\n"
        f"CA: <code>{safe_mint}</code>\n"
        f"https://pump.fun/{safe_mint}\n"
        f"Возраст: {age_days} дней\n"
        f"ATH: ${ath:,.0f}\n"
        f"USD MC: ${usd_mc:,.0f}"
    )
    markup = {
        "inline_keyboard": [
            [
                {
                    "text": "GMGN",
                    "url": f"https://gmgn.ai/sol/token/{mint}"
                }
            ]
        ]
    }
    payload = {
        "chat_id": chat_id,
        "text": text,
        "parse_mode": "HTML",
        "reply_markup": markup,
        "disable_web_page_preview": True
    }
    try:
        response = requests.post(
            f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
            json=payload,
            timeout=15
        )
        if response.status_code == 200:
            logging.info(f"Отправлено: {name} | ATH: ${ath:,.0f}")
            return True
        logging.error(
            f"TG error: {response.status_code} {response.text} | "
            f"name={name!r}, symbol={symbol!r}, mint={mint!r}"
        )
    except Exception as e:
        logging.error(f"TG error: {e}")
    return False

def delete_sent_mint_record(mint):
    if not mint:
        return
    conn = mysql.connector.connect(**DB_CONFIG)
    cursor = conn.cursor()
    cursor.execute("DELETE FROM sent_mints WHERE mint = %s", (mint,))
    conn.commit()
    cursor.close()
    conn.close()

# ==================== HTML-СТРАНИЦА ====================
WEB_CSS = """
*{ margin:0; padding:0; box-sizing:border-box; }
body {
    background: radial-gradient(1200px 800px at 80% -10%, #1b2340 0%, #0b0e17 55%) fixed, #0b0e17;
    color:#e8ebf5; font-family:'Segoe UI',Roboto,Arial,sans-serif; min-height:100vh; padding:32px 16px;
}
.wrap { max-width:760px; margin:0 auto; }
.header { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; }
h1 { font-size:26px; letter-spacing:.5px; }
h1 span { color:#7aa2ff; }
.sub { color:#8a93ab; font-size:13px; margin:8px 0 24px; }
.badge { background:#182036; border:1px solid #2a3558; color:#9fb4ff; border-radius:999px; padding:6px 14px; font-size:13px; }
.card { background:#121828; border:1px solid #232d4a; border-radius:14px; padding:16px 18px; margin-bottom:14px;
        box-shadow:0 6px 18px rgba(0,0,0,.25); }
.card-top { display:flex; justify-content:space-between; flex-wrap:wrap; gap:6px; margin-bottom:10px; }
.token-name { font-size:18px; font-weight:600; }
.symbol { color:#7aa2ff; font-weight:500; }
.mc { color:#6ee7a8; font-size:14px; font-weight:600; }
.ca-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:10px; }
.ca { background:#0a0f1d; border:1px solid #26314f; border-radius:8px; padding:8px 10px;
      font-family:Consolas,'Courier New',monospace; font-size:13px; color:#cdd6f4;
      word-break:break-all; flex:1; min-width:220px; }
.copy { cursor:pointer; border:1px solid #35508c; background:#1c2a4d; color:#bcd0ff;
        border-radius:8px; padding:8px 12px; font-size:13px; }
.copy:hover { background:#24365f; }
.copy.done { border-color:#2f7d4f; background:#173324; color:#8ce6ae; }
.links { display:flex; gap:14px; flex-wrap:wrap; margin-bottom:10px; }
.links a { color:#8ab4ff; text-decoration:none; font-size:13px; }
.links a:hover { text-decoration:underline; }
.time-row { display:flex; justify-content:space-between; flex-wrap:wrap; gap:6px; color:#8a93ab;
            font-size:12.5px; border-top:1px dashed #26314f; padding-top:10px; }
.remain { color:#f0b36a; }
.created { color:#a5b4fc; }
.empty { text-align:center; color:#8a93ab; padding:60px 0; border:1px dashed #2a3558; border-radius:14px; line-height:1.7; }
.footer { margin-top:26px; text-align:center; color:#5c6580; font-size:12px; }
"""

WEB_JS = """
function copyCA(btn){
    var ca = btn.getAttribute('data-ca');
    function ok(){
        btn.textContent='Скопировано ✓'; btn.classList.add('done');
        setTimeout(function(){ btn.textContent='Копировать'; btn.classList.remove('done'); },1500);
    }
    function fallback(){
        var ta=document.createElement('textarea'); ta.value=ca; document.body.appendChild(ta);
        ta.select(); try{ document.execCommand('copy'); ok(); }catch(e){}
        document.body.removeChild(ta);
    }
    if (navigator.clipboard && navigator.clipboard.writeText){
        navigator.clipboard.writeText(ca).then(ok).catch(fallback);
    } else { fallback(); }
}
"""

def add_to_web_page(mint, name=None, symbol=None, market_cap=None, created_at=None):
    """Добавляет отправленный в Telegram токен на HTML-страницу"""
    if not mint:
        return
    conn = mysql.connector.connect(**DB_CONFIG)
    cursor = conn.cursor()
    cursor.execute("""
        INSERT INTO web_tokens (ca, name, symbol, market_cap, added_at, created_at)
        VALUES (%s, %s, %s, %s, NOW(), %s)
        ON DUPLICATE KEY UPDATE name = VALUES(name), symbol = VALUES(symbol),
        market_cap = VALUES(market_cap), added_at = NOW(), created_at = VALUES(created_at)
    """, (mint, name, symbol, market_cap, created_at))
    conn.commit()
    cursor.close()
    conn.close()
    logging.info(f"Токен добавлен на HTML-страницу: {mint}")

def render_web_html(rows):
    """Собирает готовый HTML из списка токенов"""
    now = datetime.now()
    items = []
    for row in rows:
        ca_raw = str(row['ca'] or '')
        ca_attr = html.escape(ca_raw, quote=True)
        name_esc = html.escape(str(row['name'] or 'Unknown'))
        symbol_esc = html.escape(str(row['symbol'] or 'UNK'))
        added = row['added_at'] or now

        age_sec = max(0, int((now - added).total_seconds()))
        age_min = age_sec // 60
        remain_min = max(0, WEB_TOKEN_TTL_MINUTES - age_min)
        age_txt = 'только что' if age_min < 1 else f'{age_min} мин назад'

        try:
            mc_txt = f"${float(row['market_cap']):,.0f}"
        except (TypeError, ValueError):
            mc_txt = '—'

        # ===== НОВОЕ: дата создания токена =====
        created = row.get('created_at')
        if created and isinstance(created, datetime):
            created_txt = created.strftime('%d.%m.%Y')
        else:
            created_txt = '—'

        items.append(f"""    <div class="card">
   <div class="card-top">
     <div class="token-name">{name_esc} <span class="symbol">({symbol_esc})</span></div>
     <div class="mc">MC: {mc_txt}</div>
   </div>
   <div class="ca-row">
     <code class="ca">{ca_attr}</code>
     <button class="copy" data-ca="{ca_attr}" onclick="copyCA(this)">Копировать</button>
   </div>
   <div class="links">
     <a href="https://pump.fun/{ca_attr}" target="_blank">pump.fun</a>
     <a href="https://gmgn.ai/sol/token/{ca_attr}" target="_blank">GMGN</a>
     <a href="https://dexscreener.com/solana/{ca_attr}" target="_blank">DexScreener</a>
   </div>
   <div class="time-row">
     <span>🕒 Добавлен: {added.strftime('%d.%m.%Y %H:%M:%S')}</span>
     <span class="created">📅 Создан: {created_txt}</span>
     <span>{age_txt}</span>
     <span class="remain">удалится через ~{remain_min} мин</span>
   </div>
 </div>""")

    if items:
        body_items = '\n'.join(items)
    else:
        body_items = ('<div class="empty">Пока нет токенов.<br>'
                      'Как только бот отправит токен в Telegram — он появится здесь.</div>')

    return f"""<!DOCTYPE html>
<html lang="ru">
 <head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <meta http-equiv="refresh" content="{WEB_BROWSER_REFRESH}">
 <title>Pump.fun — свежие токены</title>
 <style>{WEB_CSS}</style>
 </head>
 <body>
 <div class="wrap">
 <div class="header">
 <h1>Pump.fun <span>· свежие токены</span></h1>
 <div class="badge">Активных: {len(items)}</div>
 </div>
 <div class="sub">Токены автоматически удаляются через {WEB_TOKEN_TTL_MINUTES} минут · страница обновляется каждые {WEB_BROWSER_REFRESH} секунд</div>
{body_items}
 <div class="footer">Сформировано: {now.strftime('%d.%m.%Y %H:%M:%S')}</div>
 </div>
 <script>{WEB_JS}</script>
 </body>
 </html>"""

def update_web_html():
    """Удаляет токены старше 1 часа и пересобирает HTML-файл"""
    conn = mysql.connector.connect(**DB_CONFIG)
    try:
        cursor = conn.cursor(dictionary=True)
        cursor.execute(
            "DELETE FROM web_tokens WHERE added_at < DATE_SUB(NOW(), INTERVAL %s MINUTE)",
            (WEB_TOKEN_TTL_MINUTES,)
        )
        deleted = cursor.rowcount
        conn.commit()
        if deleted:
            logging.info(f"HTML: удалено токенов старше {WEB_TOKEN_TTL_MINUTES} мин: {deleted}")

        cursor.execute("""
            SELECT ca, name, symbol, market_cap, added_at, created_at
            FROM web_tokens
            ORDER BY added_at DESC
        """)
        rows = cursor.fetchall()
        cursor.close()
    finally:
        conn.close()

    page = render_web_html(rows)
    tmp_path = WEB_HTML_PATH + '.tmp'
    with open(tmp_path, 'w', encoding='utf-8') as f:
        f.write(page)
    os.replace(tmp_path, WEB_HTML_PATH)

def web_worker():
    logging.info(
        f"Web worker запущен: файл {WEB_HTML_PATH}, TTL {WEB_TOKEN_TTL_MINUTES} мин, "
        f"пересборка каждые {WEB_REFRESH_SECONDS} сек"
    )
    while True:
        try:
            update_web_html()
        except Exception as e:
            logging.error(f"Web worker error: {e}")
        time.sleep(WEB_REFRESH_SECONDS)

# ======================= ЗАПУСК ======================
def dexscreener_worker():
    logging.info(
        f"DexScreener worker запущен: {DEXSCREENER_BATCH_SIZE} токенов за запрос, "
        f"1 запрос в {DEXSCREENER_REQUEST_INTERVAL} сек, без пауз между циклами"
    )
    while True:
        started = time.time()
        try:
            update_pending_mints()
        except Exception as e:
            logging.error(f"Dexscreener worker error: {e}")
        elapsed = time.time() - started
        rest = DEXSCREENER_REQUEST_INTERVAL - elapsed
        if rest > 0:
            time.sleep(rest)

def coins_worker():
    while True:
        try:
            check_new_coins()
        except Exception as e:
            logging.error(f"Coins worker error: {e}")
        time.sleep(3)

def main():
    init_db()
    try:
        update_web_html()
    except Exception as e:
        logging.error(f"Первая сборка HTML error: {e}")
    logging.info("Бот запущен. Поиск всех токенов pump.fun (возраст >= 250, ATH <= 40000, не complete)")
    threading.Thread(target=coins_worker, daemon=True).start()
    threading.Thread(target=dexscreener_worker, daemon=True).start()
    threading.Thread(target=web_worker, daemon=True).start()
    while True:
        time.sleep(60)

if __name__ == "__main__":
    main()