import sys
import time
from typing import Optional, Dict, List

import requests
import pandas as pd

PUMPFUN_URL_TEMPLATE = "https://frontend-api-v3.pump.fun/coins/{mint}"
DEXSCREENER_URL_TEMPLATE_SINGLE = "https://api.dexscreener.com/latest/dex/tokens/{mint}"
DEXSCREENER_URL_TEMPLATE_MULTI = "https://api.dexscreener.com/latest/dex/tokens/{mints}"


def log(msg: str) -> None:
    print(msg, flush=True)


def get_pumpfun_ath_and_complete(mint: str) -> (Optional[int], Optional[bool]):
    """
    Запрос к pump.fun:
      - возвращает (ath_market_cap_усечённый_до_int, complete_true_false)
      - если что-то не получилось — (None, None) или (None, complete), если complete есть.
    """
    url = PUMPFUN_URL_TEMPLATE.format(mint=mint)
    log("    [pump.fun] GET %s" % url)
    try:
        resp = requests.get(url, timeout=10)
    except Exception as e:
        log("    [pump.fun] Ошибка запроса: %s" % e)
        return None, None

    if resp.status_code != 200:
        log("    [pump.fun] Неверный статус ответа: %s" % resp.status_code)
        return None, None

    try:
        data = resp.json()
    except Exception as e:
        log("    [pump.fun] Ошибка парсинга JSON: %s" % e)
        return None, None

    ath_mc = data.get("ath_market_cap")
    complete = data.get("complete")  # true / false / None

    if ath_mc is None:
        log("    [pump.fun] 'ath_market_cap' отсутствует или = null")
        return None, complete

    try:
        value = int(ath_mc)  # усечение, как 10541.8103 -> 10541
        return value, complete
    except (TypeError, ValueError) as e:
        log("    [pump.fun] Ошибка преобразования ath_market_cap: %s" % e)
        return None, complete


def get_dexscreener_fdv_single(mint: str) -> Optional[int]:
    """
    Fallback: одиночный запрос к dexscreener для одного токена:
      - выбираем пул (solana/raydium/SOL, если есть, иначе первый)
      - возвращаем fdv (int) или None.
    """
    url = DEXSCREENER_URL_TEMPLATE_SINGLE.format(mint=mint)
    log("    [dexscreener single] GET %s" % url)
    try:
        resp = requests.get(url, timeout=10)
    except Exception as e:
        log("    [dexscreener single] Ошибка запроса: %s" % e)
        return None

    if resp.status_code != 200:
        log("    [dexscreener single] Неверный статус ответа: %s" % resp.status_code)
        return None

    try:
        data = resp.json()
    except Exception as e:
        log("    [dexscreener single] Ошибка парсинга JSON: %s" % e)
        return None

    pairs = data.get("pairs") or []
    if not isinstance(pairs, list) or not pairs:
        log("    [dexscreener single] 'pairs' пустой или отсутствует")
        return None

    chosen_pair = choose_best_pair(pairs)

    fdv = chosen_pair.get("fdv")
    if fdv is None:
        log("    [dexscreener single] 'fdv' отсутствует или = null")
        return None

    try:
        value = int(fdv)
        return value
    except (TypeError, ValueError) as e:
        log("    [dexscreener single] Ошибка преобразования fdv: %s" % e)
        return None


def choose_best_pair(pairs: List[dict]) -> dict:
    """
    Выбор "лучшего" пула:
      - сначала пробуем solana/raydium/SOL
      - если не нашли — берём первый.
    """
    if not pairs:
        return {}

    for pair in pairs:
        try:
            if (
                pair.get("chainId") == "solana"
                and pair.get("dexId") == "raydium"
                and pair.get("quoteToken", {}).get("symbol") == "SOL"
            ):
                return pair
        except AttributeError:
            continue

    return pairs[0]


def get_dexscreener_market_caps_batch(mints: List[str]) -> Dict[str, Optional[int]]:
    """
    Запрос к dexscreener по пачке токенов (до 20):
      - https://api.dexscreener.com/latest/dex/tokens/{mint1},{mint2},...
      - для каждого mint выбираем лучший пул и берём 'marketCap'
      - возвращаем словарь {mint: marketCap_int или None}
    """
    result: Dict[str, Optional[int]] = {mint: None for mint in mints if mint}

    mints_clean = [m for m in mints if m]
    if not mints_clean:
        return result

    mints_str = ",".join(mints_clean)
    url = DEXSCREENER_URL_TEMPLATE_MULTI.format(mints=mints_str)
    log("  [dexscreener batch] GET %s" % url)
    try:
        resp = requests.get(url, timeout=15)
    except Exception as e:
        log("  [dexscreener batch] Ошибка запроса: %s" % e)
        return result

    if resp.status_code != 200:
        log("  [dexscreener batch] Неверный статус ответа: %s" % resp.status_code)
        return result

    try:
        data = resp.json()
    except Exception as e:
        log("  [dexscreener batch] Ошибка парсинга JSON: %s" % e)
        return result

    pairs = data.get("pairs") or []
    if not isinstance(pairs, list) or not pairs:
        log("  [dexscreener batch] 'pairs' пустой или отсутствует")
        return result

    # группируем пары по baseToken.address
    pairs_by_mint: Dict[str, List[dict]] = {}
    for pair in pairs:
        base_addr = (pair.get("baseToken") or {}).get("address")
        if not base_addr:
            continue
        pairs_by_mint.setdefault(base_addr, []).append(pair)

    # для каждого mint выбираем лучший пул и берём marketCap
    for mint in mints_clean:
        mint_pairs = pairs_by_mint.get(mint, [])
        if not mint_pairs:
            log("    [dexscreener batch] Для токена %s не найдено ни одного пула" % mint)
            result[mint] = None
            continue

        chosen_pair = choose_best_pair(mint_pairs)
        mc = chosen_pair.get("marketCap")
        if mc is None:
            log("    [dexscreener batch] Для токена %s 'marketCap' отсутствует или = null" % mint)
            result[mint] = None
            continue

        try:
            value = int(mc)
            result[mint] = value
        except (TypeError, ValueError) as e:
            log("    [dexscreener batch] Ошибка преобразования marketCap для %s: %s" % (mint, e))
            result[mint] = None

    return result


def process_ods(
    input_path: str = "OG.ods",
    output_path: str = "OG_with_caps.ods",
    delay: float = 0.5,
    batch_size: int = 20,
) -> None:
    """
    1) Читает OG.ods без заголовков (первая строка — сразу токен).
    2) Для каждого токена:
       - pump.fun: ath_market_cap -> 2-й столбец, complete -> 3-й столбец.
       - если ath_market_cap не получилось -> dexscreener single fdv -> 2-й столбец.
    3) После этого делает батчевые запросы к dexscreener по 20 токенов и пишет
       marketCap в 4-й столбец.
    """

    log("Читаю файл: %s" % input_path)
    # ВАЖНО: header=None, т.к. первая строка — это токен, а не название колонки
    df = pd.read_excel(input_path, engine="odf", header=None)

    # Первый столбец — токен
    df.columns = ["token"] + [f"col_{i}" for i in range(2, len(df.columns) + 1)]

    tokens = df["token"].astype(str)
    total = len(tokens)

    ath_caps: List[Optional[int]] = []
    completes: List[Optional[bool]] = []

    log("\n=== ЭТАП 1: pump.fun + fallback fdv ===")

    for idx, mint in enumerate(tokens, start=1):
        mint = mint.strip()
        log("\n[%d/%d] Токен: %r" % (idx, total, mint))

        if not mint:
            log("  Пустое значение токена, пропускаю.")
            ath_caps.append(None)
            completes.append(None)
            continue

        # pump.fun
        log("  -> Запрос к pump.fun...")
        ath_cap, complete = get_pumpfun_ath_and_complete(mint)
        time.sleep(delay)

        if complete is not None:
            log("  --> complete = %r" % complete)
        else:
            log("  --> complete = None")

        if ath_cap is not None:
            log("  --> ath_market_cap (усечённый) = %s" % ath_cap)
            ath_caps.append(ath_cap)
            completes.append(complete)
        else:
            # fallback на dexscreener fdv
            log("  -> Значение ath_market_cap не получено, пробуем dexscreener (fdv)...")
            fdv_cap = get_dexscreener_fdv_single(mint)
            time.sleep(delay)

            if fdv_cap is not None:
                log("  --> Успех: fdv = %s (записываем как cap)" % fdv_cap)
                ath_caps.append(fdv_cap)
            else:
                log("  --> Не удалось получить cap ни из pump.fun, ни из dexscreener (fdv).")
                ath_caps.append(None)

            completes.append(complete)

    # Записываем результаты этапа 1
    df["ath_cap"] = ath_caps       # 2-й столбец (по смыслу)
    df["complete"] = completes     # 3-й столбец

    # ЭТАП 2: батчевые marketCap с dexscreener
    log("\n=== ЭТАП 2: dexscreener batch marketCap ===")
    market_caps: List[Optional[int]] = [None] * total

    # идём по токенам пачками по batch_size
    idx_start = 0
    batch_num = 1
    while idx_start < total:
        idx_end = min(idx_start + batch_size, total)
        batch_tokens = [t.strip() for t in tokens.iloc[idx_start:idx_end].tolist()]

        log("\n[batch %d] Обрабатываю строки %d–%d" % (batch_num, idx_start + 1, idx_end))

        # убираем пустые, но сохраняем исходные индексы для записи
        non_empty_tokens = []
        index_map = []  # локальный индекс -> глобальный индекс df

        for local_idx, mint in enumerate(batch_tokens):
            if mint:
                non_empty_tokens.append(mint)
                index_map.append(idx_start + local_idx)

        if not non_empty_tokens:
            log("  В этой пачке нет непустых токенов, пропускаю.")
            idx_start = idx_end
            batch_num += 1
            continue

        batch_caps = get_dexscreener_market_caps_batch(non_empty_tokens)
        time.sleep(delay)

        # записываем значения в массив по глобальным индексам
        for mint, global_idx in zip(non_empty_tokens, index_map):
            market_caps[global_idx] = batch_caps.get(mint)

        idx_start = idx_end
        batch_num += 1

    df["marketCap"] = market_caps  # 4-й столбец по смыслу

    # Сохраняем результат
    df.to_excel(output_path, engine="odf", index=False)
    log("\nГотово! Результат записан в файл: %s" % output_path)


if __name__ == "__main__":
    try:
        log("Запуск скрипта...")
        log("Версия Python: %s" % sys.version.replace("\n", " "))
        process_ods()
    except Exception as e:
        log("\n[ОШИБКА] Скрипт упал: %r" % e)
        raise
