import html
import re
import time
import logging
import threading
import requests
from datetime import datetime, timedelta
import mysql.connector

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# ==================== НАСТРОЙКИ ====================

# Адрес HTML-страницы на ПЕРВОМ сервере
PAGE_URL = 'http://213.155.14.226/telebot/tokens.html'

# Telegram
TELEGRAM_TOKEN = '8141522208:AAHZxJdI76anqke2CkxXf4wYMAxZREstmd4'

# ID двух групп в зависимости от возраста токена
GROUP_250_700_DAYS = '-1004349949778'      # токены от 250 до 700 дней
GROUP_700_PLUS_DAYS = '-1004298222785'     # токены от 700 дней и более (вставьте ID второй группы)

# Пороги возраста в днях
AGE_MIN_DAYS = 250
AGE_THRESHOLD_DAYS = 700

# Мониторинг роста капы
GROWTH_MIN_PERCENT = 5.0
GROWTH_MAX_PERCENT = 10.0
MONITOR_DAYS = 15

# Если токен между проверками перескочил диапазон
SEND_IF_JUMPED_OVER = True

# Минимальный возраст токена в секундах перед отправкой
MIN_AGE_BEFORE_SEND_SECONDS = 0

# DexScreener: 5 токенов через запятую в одном запросе, 1 запрос в секунду
DEXSCREENER_BATCH_SIZE = 5
DEXSCREENER_REQUEST_INTERVAL = 1

# Как часто забирать токены со страницы первого сервера, сек
FETCH_PAGE_INTERVAL = 30

# Настройки MySQL
DB_CONFIG = {
    'host': 'localhost',
    'user': 'admin',
    'password': 'aOjjw43hUBZF',
    'database': 'pump_tracker',
    'charset': 'utf8mb4',
}

# ==================================================


def get_db_connection():
    return mysql.connector.connect(**DB_CONFIG)


def to_int(value, default=None):
    if value is None:
        return default
    try:
        return int(value)
    except Exception:
        return default


def to_float(value, default=None):
    if value is None:
        return default
    try:
        return float(value)
    except Exception:
        return default


def column_exists(cursor, table_name, column_name):
    cursor.execute(
        """
        SELECT COUNT(*)
        FROM information_schema.COLUMNS
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = %s
          AND COLUMN_NAME = %s
        """,
        (table_name, column_name)
    )
    row = cursor.fetchone()
    return row[0] > 0


def init_db():
    conn = get_db_connection()
    cursor = conn.cursor()

    cursor.execute('''
    CREATE TABLE IF NOT EXISTS tracked_tokens (
        ca VARCHAR(100) PRIMARY KEY,
        name VARCHAR(255) DEFAULT NULL,
        symbol VARCHAR(50) DEFAULT NULL,
        initial_market_cap DECIMAL(20,2) DEFAULT NULL,
        current_market_cap DECIMAL(20,2) DEFAULT NULL,
        added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        expire_at DATETIME NOT NULL,
        added_at_ts BIGINT NULL,
        expire_at_ts BIGINT NULL,
        created_at DATE DEFAULT NULL,
        last_checked_at DATETIME DEFAULT NULL,
        status VARCHAR(20) DEFAULT 'pending'
    ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
    ''')

    cursor.execute('''
    CREATE TABLE IF NOT EXISTS tracked_history (
        ca VARCHAR(100) PRIMARY KEY,
        name VARCHAR(255) DEFAULT NULL,
        symbol VARCHAR(50) DEFAULT NULL,
        result VARCHAR(20) DEFAULT NULL,
        finished_at DATETIME DEFAULT CURRENT_TIMESTAMP
    ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
    ''')

    # Миграция для старой таблицы: добавляем Unix-время, если его ещё нет.
    if not column_exists(cursor, 'tracked_tokens', 'added_at_ts'):
        cursor.execute('ALTER TABLE tracked_tokens ADD COLUMN added_at_ts BIGINT NULL')

    if not column_exists(cursor, 'tracked_tokens', 'expire_at_ts'):
        cursor.execute('ALTER TABLE tracked_tokens ADD COLUMN expire_at_ts BIGINT NULL')

    # Миграция: добавляем колонку created_at, если её ещё нет
    if not column_exists(cursor, 'tracked_tokens', 'created_at'):
        cursor.execute('ALTER TABLE tracked_tokens ADD COLUMN created_at DATE DEFAULT NULL')

    # Перенос старых DATETIME значений в Unix-время.
    cursor.execute('''
        UPDATE tracked_tokens
        SET added_at_ts = UNIX_TIMESTAMP(added_at)
        WHERE added_at_ts IS NULL
          AND added_at IS NOT NULL
    ''')

    cursor.execute('''
        UPDATE tracked_tokens
        SET expire_at_ts = UNIX_TIMESTAMP(expire_at)
        WHERE expire_at_ts IS NULL
          AND expire_at IS NOT NULL
    ''')

    # Если по какой-то причине срок так и не получен, ставим 15 дней от текущего момента.
    cursor.execute('''
        UPDATE tracked_tokens
        SET expire_at_ts = UNIX_TIMESTAMP() + %s
        WHERE expire_at_ts IS NULL
    ''', (MONITOR_DAYS * 86400,))

    conn.commit()
    cursor.close()
    conn.close()

    logging.info("База данных инициализирована")


# ==================== ЗАБОР ТОКЕНОВ СО СТРАНИЦЫ ====================


def parse_money(value):
    """
    Парсит рыночную капитализацию из HTML.
    Понимает варианты:
      $1,234
      1234
      12.5K
      1.2M
      —
    """
    if value is None:
        return None

    text = str(value).strip()

    if not text or text == '—':
        return None

    text = text.replace('$', '').replace(',', '').replace(' ', '')

    if not text:
        return None

    multiplier = 1
    last_char = text[-1].upper()

    if last_char == 'K':
        multiplier = 1_000
        text = text[:-1]
    elif last_char == 'M':
        multiplier = 1_000_000
        text = text[:-1]
    elif last_char == 'B':
        multiplier = 1_000_000_000
        text = text[:-1]

    try:
        return float(text) * multiplier
    except Exception:
        return None


def parse_date(value):
    """
    Парсит дату создания токена из HTML.
    Формат: '📅 Создан: DD.MM.YYYY'
    Возвращает строку 'YYYY-MM-DD' или None
    """
    if value is None:
        return None

    text = str(value).strip()

    # Ищем формат DD.MM.YYYY
    match = re.search(r'(\d{2})\.(\d{2})\.(\d{4})', text)
    if match:
        day = match.group(1)
        month = match.group(2)
        year = match.group(3)
        return f"{year}-{month}-{day}"

    return None


def parse_page(page_text):
    """
    Разбирает tokens.html и возвращает список:
    {ca, name, symbol, market_cap, created_at}
    """
    tokens = []

    # Режем страницу по карточкам токенов.
    chunks = re.split(
        r'<div\s+class="card"\s*>',
        page_text,
        flags=re.I
    )[1:]

    for chunk in chunks:
        m_ca = re.search(r'data-ca="([^"]+)"', chunk, re.I)
        if not m_ca:
            continue

        ca = html.unescape(m_ca.group(1)).strip()

        # Проверка формата: base58, 32-44 символа.
        if not re.fullmatch(r'[1-9A-HJ-NP-Za-km-z]{32,44}', ca):
            logging.warning(f"Пропущен CA подозрительного формата: {ca}")
            continue

        name, symbol = None, None

        m_name = re.search(
            r'<div\s+class="token-name">(.*?)\s*<span\s+class="symbol">\((.*?)\)</span></div>',
            chunk,
            re.S | re.I
        )

        if m_name:
            name = html.unescape(m_name.group(1)).strip()
            symbol = html.unescape(m_name.group(2)).strip()

        market_cap = None

        m_mc = re.search(
            r'<div\s+class="mc">MC:\s*([^<]*)</div>',
            chunk,
            re.S | re.I
        )

        if m_mc:
            market_cap = parse_money(m_mc.group(1))

        # Парсим дату создания токена
        created_at = None
        m_created = re.search(r'📅\s*Создан:([^<\n]+)', chunk, re.I)
        if m_created:
            created_at = parse_date(m_created.group(1))

        tokens.append({
            'ca': ca,
            'name': name,
            'symbol': symbol,
            'market_cap': market_cap,
            'created_at': created_at,
        })

    return tokens


def fetch_and_store_tokens():
    """
    Скачивает страницу с первого сервера и сохраняет новые токены в базу.
    """
    try:
        r = requests.get(
            PAGE_URL,
            timeout=20,
            headers={'User-Agent': 'Mozilla/5.0'}
        )
    except Exception as e:
        logging.warning(f"Не удалось загрузить страницу {PAGE_URL}: {e}")
        return

    if r.status_code != 200:
        logging.warning(f"Страница вернула HTTP {r.status_code}")
        return

    # Явно указываем кодировку, чтобы кириллица/эмодзи не поехали.
    r.encoding = 'utf-8'

    tokens = parse_page(r.text)

    if not tokens:
        return

    for t in tokens:
        add_tracked_token(
            t['ca'],
            t['name'],
            t['symbol'],
            t['market_cap'],
            t['created_at']
        )


def add_tracked_token(ca, name=None, symbol=None, market_cap=None, created_at=None):
    """
    Добавляет токен на мониторинг, если его ещё нет.
    """
    if not ca:
        return False

    conn = get_db_connection()
    cursor = conn.cursor()

    try:
        cursor.execute(
            "SELECT ca FROM tracked_tokens WHERE ca = %s",
            (ca,)
        )

        if cursor.fetchone():
            return False

        cursor.execute(
            "SELECT ca FROM tracked_history WHERE ca = %s",
            (ca,)
        )

        if cursor.fetchone():
            return False

        cursor.execute(
            """
            INSERT INTO tracked_tokens (
                ca,
                name,
                symbol,
                initial_market_cap,
                current_market_cap,
                added_at,
                expire_at,
                added_at_ts,
                expire_at_ts,
                created_at,
                status
            )
            VALUES (
                %s,
                %s,
                %s,
                %s,
                %s,
                NOW(),
                DATE_ADD(NOW(), INTERVAL %s DAY),
                UNIX_TIMESTAMP(),
                UNIX_TIMESTAMP() + %s,
                %s,
                'pending'
            )
            """,
            (
                ca,
                name,
                symbol,
                market_cap,
                market_cap,
                MONITOR_DAYS,
                MONITOR_DAYS * 86400,
                created_at,
            )
        )

        conn.commit()

        logging.info(
            f"Новый токен на мониторинге ({MONITOR_DAYS} дней): {ca} | базовая капа: {market_cap} | создан: {created_at}"
        )

        return True

    except mysql.connector.IntegrityError:
        return False

    except Exception as e:
        logging.error(f"Ошибка добавления токена {ca}: {e}")
        return False

    finally:
        cursor.close()
        conn.close()


# ==================== DEXSCREENER ====================


def get_market_caps_batch(cas):
    """
    ОДИН запрос к DexScreener сразу на несколько токенов:
    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,
            headers={'User-Agent': 'Mozilla/5.0'}
        )

        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:
                continue

            market_cap = pair.get('marketCap')

            if market_cap is None:
                market_cap = pair.get('market_cap')

            if market_cap is None:
                continue

            try:
                market_cap = float(market_cap)
            except Exception:
                continue

            if market_cap <= 0:
                continue

            # Если по одному CA есть несколько пар, берём максимальную капсу.
            if ca not in result or market_cap > result[ca]:
                result[ca] = market_cap

    except Exception as e:
        logging.warning(f"Dexscreener batch error: {e}")

    return result


# ==================== МОНИТОРИНГ РОСТА ====================


def should_send_by_growth(initial_market_cap, current_market_cap):
    """
    True, если рост капы попал в диапазон 5-10%,
    или перескочил его, если SEND_IF_JUMPED_OVER = True.
    """
    initial = to_float(initial_market_cap)
    current = to_float(current_market_cap)

    if initial is None or current is None:
        return False

    if initial <= 0 or current <= 0:
        return False

    growth = (current - initial) / initial * 100.0

    if GROWTH_MIN_PERCENT <= growth <= GROWTH_MAX_PERCENT:
        return True

    if SEND_IF_JUMPED_OVER and growth > GROWTH_MAX_PERCENT:
        return True

    return False


def add_to_history(ca, name, symbol, result):
    if not ca:
        return

    conn = get_db_connection()
    cursor = conn.cursor()

    try:
        cursor.execute(
            """
            INSERT INTO tracked_history (
                ca,
                name,
                symbol,
                result,
                finished_at
            )
            VALUES (%s, %s, %s, %s, NOW())
            ON DUPLICATE KEY UPDATE
                result = VALUES(result),
                finished_at = NOW()
            """,
            (ca, name, symbol, result)
        )

        conn.commit()

    except Exception as e:
        logging.error(f"Ошибка записи истории {ca}: {e}")

    finally:
        cursor.close()
        conn.close()


def format_monitored_seconds(seconds):
    """
    Красиво показывает время мониторинга.
    Раньше было только '0 дн 0 ч'.
    Теперь есть минуты и секунды.
    """
    seconds = to_int(seconds)

    if seconds is None:
        return '—'

    if seconds < 0:
        seconds = 0

    days = seconds // 86400
    hours = (seconds % 86400) // 3600
    minutes = (seconds % 3600) // 60
    secs = seconds % 60

    if days > 0:
        return f"{days} дн {hours} ч"

    if hours > 0:
        return f"{hours} ч {minutes} мин"

    if minutes > 0:
        return f"{minutes} мин {secs} сек"

    return f"{secs} сек"


def update_tracked_tokens():
    """
    Один цикл = батч-проверка 5 токенов.
    """
    conn = get_db_connection()

    try:
        cursor = conn.cursor(dictionary=True)

        # Получаем текущее время от MySQL, чтобы не зависеть от часового пояса Python.
        cursor.execute("SELECT UNIX_TIMESTAMP(NOW()) AS now_ts")
        now_row = cursor.fetchone()

        now_ts = to_int(
            now_row.get('now_ts') if now_row else None,
            int(time.time())
        )

        cursor.execute(
            """
            SELECT
                ca,
                name,
                symbol,
                initial_market_cap,
                current_market_cap,
                COALESCE(added_at_ts, UNIX_TIMESTAMP(added_at)) AS added_ts,
                COALESCE(expire_at_ts, UNIX_TIMESTAMP(expire_at)) AS expire_ts,
                created_at,
                added_at,
                expire_at
            FROM tracked_tokens
            WHERE status = 'pending'
            ORDER BY
                CASE WHEN last_checked_at IS NULL THEN 0 ELSE 1 END,
                last_checked_at ASC,
                added_at ASC
            LIMIT %s
            """,
            (DEXSCREENER_BATCH_SIZE,)
        )

        rows = cursor.fetchall()
        cursor.close()

        if not rows:
            return

        active = []

        for row in rows:
            expire_ts = to_int(row.get('expire_ts'))

            # Срок мониторинга истёк.
            if expire_ts is not None and now_ts >= expire_ts:
                add_to_history(
                    row['ca'],
                    row['name'],
                    row['symbol'],
                    'expired'
                )

                cur = conn.cursor()
                cur.execute(
                    "DELETE FROM tracked_tokens WHERE ca = %s",
                    (row['ca'],)
                )
                conn.commit()
                cur.close()

                logging.info(
                    f"{MONITOR_DAYS} дней истекли без роста {GROWTH_MIN_PERCENT}-{GROWTH_MAX_PERCENT}%, "
                    f"токен удалён: {row['ca']}"
                )
            else:
                row['expire_ts'] = expire_ts
                active.append(row)

        if not active:
            return

        # ОДИН запрос на 5 токенов через запятую.
        caps = get_market_caps_batch([row['ca'] for row in active])

        for row in active:
            ca = row['ca']
            mc = caps.get(ca)

            cursor = conn.cursor()

            # Если DexScreener не дал капсу, просто обновляем время проверки.
            if mc is None or mc <= 0:
                cursor.execute(
                    """
                    UPDATE tracked_tokens
                    SET last_checked_at = NOW()
                    WHERE ca = %s
                    """,
                    (ca,)
                )
                conn.commit()
                cursor.close()
                continue

            initial = row['initial_market_cap']
            initial_f = to_float(initial)

            # Если базовой капы нет, берём первую полученную из DexScreener.
            if initial_f is None or initial_f <= 0:
                cursor.execute(
                    """
                    UPDATE tracked_tokens
                    SET initial_market_cap = %s,
                        current_market_cap = %s,
                        last_checked_at = NOW()
                    WHERE ca = %s
                    """,
                    (mc, mc, ca)
                )
                conn.commit()
                cursor.close()

                initial_f = float(mc)

            else:
                cursor.execute(
                    """
                    UPDATE tracked_tokens
                    SET current_market_cap = %s,
                        last_checked_at = NOW()
                    WHERE ca = %s
                    """,
                    (mc, ca)
                )
                conn.commit()
                cursor.close()

            if should_send_by_growth(initial_f, mc):
                added_ts = to_int(row.get('added_ts'))

                age_seconds = None
                if added_ts is not None:
                    age_seconds = max(0, now_ts - added_ts)

                # Если задана минимальная выдержка, а токен слишком свежий,
                # пока не отправляем.
                if MIN_AGE_BEFORE_SEND_SECONDS > 0:
                    if age_seconds is None or age_seconds < MIN_AGE_BEFORE_SEND_SECONDS:
                        continue

                growth = (float(mc) - float(initial_f)) / float(initial_f) * 100.0

                sent = send_growth_to_telegram(
                    row['name'] or 'Unknown',
                    row['symbol'] or 'UNK',
                    ca,
                    initial_f,
                    mc,
                    growth,
                    added_ts,
                    now_ts,
                    row.get('created_at')
                )

                if sent:
                    add_to_history(
                        ca,
                        row['name'],
                        row['symbol'],
                        'sent'
                    )

                    cur = conn.cursor()
                    cur.execute(
                        "DELETE FROM tracked_tokens WHERE ca = %s",
                        (ca,)
                    )
                    conn.commit()
                    cur.close()

                    logging.info(
                        f"Отправлен в новую группу по росту +{growth:.2f}%: {ca}"
                    )

    except Exception as e:
        logging.error(f"Ошибка мониторинга токенов: {e}")

    finally:
        conn.close()


# ==================== TELEGRAM ====================


def send_growth_to_telegram(
    name,
    symbol,
    ca,
    initial_mc,
    current_mc,
    growth_pct,
    added_ts,
    now_ts,
    created_at
):
    name = str(name or "Unknown")
    symbol = str(symbol or "UNK")
    ca = str(ca or "")

    safe_name = html.escape(name)
    safe_symbol = html.escape(symbol)
    safe_ca = html.escape(ca)

    # Определяем возраст токена в днях
    token_age_days = None
    token_age_txt = '—'

    try:
        if created_at:
            # created_at в формате YYYY-MM-DD
            created_date = datetime.strptime(str(created_at), '%Y-%m-%d')
            today = datetime.now()
            token_age_days = (today - created_date).days
            
            if token_age_days > 0:
                token_age_txt = f"{token_age_days} дн назад"
            elif token_age_days == 0:
                token_age_txt = "сегодня"
            else:
                token_age_txt = '—'
    except Exception:
        token_age_txt = '—'

    # Выбираем группу в зависимости от возраста токена
    target_group = None
    if token_age_days is not None:
        if token_age_days < AGE_THRESHOLD_DAYS:
            target_group = GROUP_250_700_DAYS
        else:
            target_group = GROUP_700_PLUS_DAYS
    else:
        # Если дату не удалось определить, отправляем в первую группу по умолчанию
        target_group = GROUP_250_700_DAYS

    monitored_txt = '—'

    try:
        if added_ts is not None and now_ts is not None:
            age_seconds = max(0, int(now_ts) - int(added_ts))
            monitored_txt = format_monitored_seconds(age_seconds)
    except Exception:
        monitored_txt = '—'

    text = (
        f"🚀 Рост капы: {safe_name} ({safe_symbol})\n"
        f"CA: <code>{safe_ca}</code>\n"
        f"https://pump.fun/{safe_ca}\n"
        f"Базовая капа: ${float(initial_mc):,.0f}\n"
        f"Текущая капа: ${float(current_mc):,.0f}\n"
        f"Рост: +{growth_pct:.2f}%\n"
        f"В мониторинге: {monitored_txt}\n"
        f"Возраст токена: {token_age_txt}"
    )

    markup = {
        "inline_keyboard": [
            [
                {
                    "text": "GMGN",
                    "url": f"https://gmgn.ai/sol/token/{ca}"
                }
            ]
        ]
    }

    payload = {
        "chat_id": target_group,
        "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,
            headers={'User-Agent': 'Mozilla/5.0'}
        )

        if response.status_code == 200:
            logging.info(
                f"Отправлено в группу (возраст {token_age_days} дн): {name} | +{growth_pct:.2f}%"
            )
            return True

        logging.error(
            f"TG error: {response.status_code} {response.text} | "
            f"name={name!r}, symbol={symbol!r}, ca={ca!r}"
        )

    except Exception as e:
        logging.error(f"TG error: {e}")

    return False


# ==================== ЗАПУСК ====================


def page_worker():
    logging.info(
        f"Page worker запущен: забираю токены с {PAGE_URL} "
        f"каждые {FETCH_PAGE_INTERVAL} сек"
    )

    while True:
        try:
            fetch_and_store_tokens()
        except Exception as e:
            logging.error(f"Page worker error: {e}")

        time.sleep(FETCH_PAGE_INTERVAL)


def monitor_worker():
    logging.info(
        f"Monitor worker запущен: {DEXSCREENER_BATCH_SIZE} токенов за запрос, "
        f"1 запрос в {DEXSCREENER_REQUEST_INTERVAL} сек, "
        f"диапазон роста {GROWTH_MIN_PERCENT}-{GROWTH_MAX_PERCENT}%"
    )

    while True:
        started = time.time()

        try:
            update_tracked_tokens()
        except Exception as e:
            logging.error(f"Monitor worker error: {e}")

        elapsed = time.time() - started
        rest = DEXSCREENER_REQUEST_INTERVAL - elapsed

        if rest > 0:
            time.sleep(rest)


def main():
    init_db()

    logging.info(
        f"Трекер запущен. Мониторинг {MONITOR_DAYS} дней, "
        f"отправка при росте {GROWTH_MIN_PERCENT}-{GROWTH_MAX_PERCENT}%"
    )

    threading.Thread(target=page_worker, daemon=True).start()
    threading.Thread(target=monitor_worker, daemon=True).start()

    while True:
        time.sleep(60)


if __name__ == "__main__":
    main()