"""
RugMunch Unified Wallet Scanner + Trending Engine
==================================================
Powered by SENTINEL + Wallet Memory Bank + Helius + Token Scanner integration.

FREE:   Helius on-chain data (age, tx, volume, balances) + SENTINEL labels
PRO:    Wallet Memory Bank identity + cluster intel + GMGN scoring
ELITE:  Token portfolio risk scan + full behavioral analysis

Chains: solana, ethereum, base, bsc, arbitrum, polygon, avalanche,
        optimism, fantom, linea, zksync, scroll, mantle
"""

import asyncio
import logging
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any

import httpx

logger = logging.getLogger(__name__)

# ─── CHAIN CONFIG ─────────────────────────────────────────────────

CHAINS = {
    "solana": {"id": "solana", "explorer": "https://solscan.io"},
    "ethereum": {"id": "1", "explorer": "https://etherscan.io"},
    "base": {"id": "8453", "explorer": "https://basescan.org"},
    "bsc": {"id": "56", "explorer": "https://bscscan.com"},
    "arbitrum": {"id": "42161", "explorer": "https://arbiscan.io"},
    "polygon": {"id": "137", "explorer": "https://polygonscan.com"},
    "avalanche": {"id": "43114", "explorer": "https://snowtrace.io"},
    "optimism": {"id": "10", "explorer": "https://optimistic.etherscan.io"},
    "fantom": {"id": "250", "explorer": "https://ftmscan.com"},
    "linea": {"id": "59144", "explorer": "https://lineascan.build"},
    "zksync": {"id": "324", "explorer": "https://explorer.zksync.io"},
    "scroll": {"id": "534352", "explorer": "https://scrollscan.com"},
    "mantle": {"id": "5000", "explorer": "https://mantlescan.xyz"},
}

# ═══════════════════════════════════════════════════════════════
# WALLET SCANNER — Powered by SENTINEL + Helius + Wallet Memory
# ═══════════════════════════════════════════════════════════════


@dataclass
class WalletRiskFactors:
    """100+ risk factors scored per wallet — populated from real on-chain data."""

    wallet_age_days: int = 0
    first_tx_date: str | None = None
    total_tx_count: int = 0
    tx_frequency_per_day: float = 0
    days_since_last_tx: int = 0
    active_chains: int = 1
    total_volume_usd: float = 0
    avg_tx_value_usd: float = 0
    max_single_tx_usd: float = 0

    funding_source: str | None = None
    funding_source_label: str | None = None
    funding_source_is_cex: bool = False
    funding_source_is_mixer: bool = False
    funding_from_sanctioned: bool = False
    funding_from_scammer: bool = False
    funding_hop_count: int = 0
    cross_chain_funding: bool = False
    tornado_cash_exposure: bool = False
    mixer_interactions: int = 0

    unique_tokens_held: int = 0
    token_launches_participated: int = 0
    sniper_activity_score: float = 0
    sandwich_attacks_executed: int = 0
    flash_loan_usage: int = 0
    rug_pull_launcher_count: int = 0
    honeypot_tokens_owned: int = 0
    tokens_where_top_holder: int = 0

    cex_deposits: int = 0
    cex_withdrawals: int = 0
    dex_swaps: int = 0
    dex_volume_usd: float = 0

    cluster_id: str | None = None
    cluster_size: int = 0
    cluster_shared_funding: bool = False
    known_cluster_type: str = ""
    entity_label: str | None = None
    entity_confidence: float = 0
    is_labeled_scammer: bool = False
    is_labeled_sanctioned: bool = False
    is_labeled_exchange: bool = False
    is_labeled_bot: bool = False
    is_labeled_whale: bool = False
    is_labeled_insider: bool = False
    linked_wallets_count: int = 0

    insider_trading_confidence: float = 0
    buys_before_pumps: int = 0
    sells_before_dumps: int = 0
    wash_trading_score: float = 0
    pump_and_dump_participant: bool = False
    new_token_flipper: bool = False
    round_trip_patterns: int = 0

    scam_token_exposure: int = 0
    phishing_victim: bool = False
    sybil_score: float = 0
    exploit_exposure: int = 0
    protocol_hack_interactions: int = 0
    approval_risk_score: float = 0

    current_balance_usd: float = 0
    portfolio_diversity_score: float = 0
    stablecoin_ratio: float = 0
    bluechip_ratio: float = 0
    memecoin_ratio: float = 0
    realized_pnl_usd: float = 0
    unrealized_pnl_usd: float = 0
    win_rate: float = 0

    # Token holdings risk (from integrated token scanner)
    held_token_risks: list[dict[str, Any]] = field(default_factory=list)
    highest_risk_token: dict[str, Any] | None = None

    # Overall
    total_risk_score: int = 0
    risk_category: str = "unknown"
    confidence: float = 0
    data_sources: list[str] = field(default_factory=list)


# ═══════════════════════════════════════════
# RESULT CACHE — Redis-backed, 1hr TTL
# ═══════════════════════════════════════════

import hashlib


async def _cache_get(key: str) -> dict | None:
    """Get cached wallet scan result."""
    try:
        import json
        import os

        import redis.asyncio as aioredis

        r = await aioredis.from_url(
            f"redis://{os.getenv('REDIS_HOST', 'rmi-redis')}:{os.getenv('REDIS_PORT', '6379')}",
            password=os.getenv("REDIS_PASSWORD", "") or None,
        )
        cached = await r.get(f"rmi:wallet_cache:{key}")
        await r.close()
        if cached:
            return json.loads(cached)
    except Exception:
        pass
    return None


async def _cache_set(key: str, data: dict, ttl: int = 3600):
    """Cache wallet scan result for 1 hour."""
    try:
        import json
        import os

        import redis.asyncio as aioredis

        r = await aioredis.from_url(
            f"redis://{os.getenv('REDIS_HOST', 'rmi-redis')}:{os.getenv('REDIS_PORT', '6379')}",
            password=os.getenv("REDIS_PASSWORD", "") or None,
        )
        await r.setex(f"rmi:wallet_cache:{key}", ttl, json.dumps(data))
        await r.close()
    except Exception:
        pass


# ═══════════════════════════════════════════
# HELIUS CREDENTIAL POOL — Primary + Fallback
# ═══════════════════════════════════════════

_HELIUS_KEYS = None
_HELIUS_INDEX = 0


def _get_helius_key() -> str | None:
    """Round-robin through Helius API keys. Returns None if none configured."""
    global _HELIUS_KEYS, _HELIUS_INDEX
    if _HELIUS_KEYS is None:
        import os

        keys = []
        # Primary key
        k1 = os.getenv("HELIUS_API_KEY", "").strip()
        if k1:
            keys.append(k1)
        # Fallback keys (comma-separated or separate env vars)
        k2 = os.getenv("HELIUS_API_KEY_2", "").strip()
        if k2:
            keys.append(k2)
        k3 = os.getenv("HELIUS_API_KEY_3", "").strip()
        if k3:
            keys.append(k3)
        _HELIUS_KEYS = keys
    if not _HELIUS_KEYS:
        return None
    key = _HELIUS_KEYS[_HELIUS_INDEX % len(_HELIUS_KEYS)]
    _HELIUS_INDEX += 1
    return key


async def _helius_call(method: str, params: list) -> dict | None:
    """Call Helius RPC with credential pool fallback."""
    for _attempt in range(3):  # try up to 3 keys
        key = _get_helius_key()
        if not key:
            return None
        try:
            async with httpx.AsyncClient(timeout=12.0) as client:
                resp = await client.post(
                    f"https://mainnet.helius-rpc.com/?api-key={key}",
                    json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
                )
                if resp.status_code == 200:
                    data = resp.json()
                    if "error" not in data:
                        return data
                # 401/429 = try next key
                if resp.status_code in (401, 429):
                    continue
        except Exception:
            continue
    return None


async def _fetch_helius_wallet(address: str) -> dict[str, Any]:
    """Fetch wallet age, tx count, balances, and token holdings from Helius (credential pool)."""
    result = {}

    # Token balances
    bal_data = await _helius_call(
        "getTokenAccountsByOwner",
        [
            address,
            {"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
            {"encoding": "jsonParsed"},
        ],
    )
    if bal_data:
        accounts = (bal_data.get("result", {}) or {}).get("value", [])
        tokens = []
        for acc in accounts:
            info = acc.get("account", {}).get("data", {}).get("parsed", {}).get("info", {})
            amt = float(info.get("tokenAmount", {}).get("uiAmount", 0) or 0)
            mint = info.get("mint", "")
            if mint and amt > 0:
                tokens.append({"mint": mint, "amount": amt})
        result["tokens"] = tokens
        result["token_count"] = len(tokens)
        result["data_sources"] = ["helius"]

    # Transaction signatures for age/activity
    sig_data = await _helius_call("getSignaturesForAddress", [address, {"limit": 100}])
    if sig_data:
        sigs = sig_data.get("result", []) or []
        if sigs:
            result["tx_count_sample"] = len(sigs)
            result["last_tx"] = sigs[0].get("blockTime")
            first = sigs[-1].get("blockTime")
            if first:
                age_seconds = datetime.now(UTC).timestamp() - first
                result["wallet_age_days"] = int(age_seconds / 86400)
                result["tx_count"] = len(sigs)
                result["data_sources"] = [*result.get("data_sources", []), "helius_tx"]

    # SOL balance
    sol_data = await _helius_call("getBalance", [address])
    if sol_data:
        bal = sol_data.get("result", {}).get("value", 0)
        result["sol_balance"] = bal / 1e9 if bal else 0

    return result


# ═══════════════════════════════════════════
# MORALIS — EVM wallet data (multi-chain)
# ═══════════════════════════════════════════


async def _fetch_moralis_wallet(address: str, chain: str) -> dict[str, Any]:
    """Fetch EVM wallet data from Moralis (balance, tx history, token holdings)."""
    import os

    key = os.getenv("MORALIS_API_KEY", "").strip()
    if not key:
        return {}

    chain_map = {
        "ethereum": "eth",
        "bsc": "bsc",
        "polygon": "polygon",
        "arbitrum": "arbitrum",
        "base": "base",
        "optimism": "optimism",
        "avalanche": "avalanche",
        "fantom": "fantom",
    }
    moralis_chain = chain_map.get(chain, "eth")

    result = {}
    try:
        async with httpx.AsyncClient(timeout=15.0) as client:
            headers = {"X-API-Key": key}

            # Token balances
            resp = await client.get(
                f"https://deep-index.moralis.io/api/v2.2/{address}/erc20",
                params={"chain": moralis_chain},
                headers=headers,
            )
            if resp.status_code == 200:
                tokens_data = resp.json()
                tokens = []
                for t in tokens_data[:50]:
                    amt = float(t.get("balance_formatted", 0) or 0)
                    if amt > 0:
                        tokens.append(
                            {
                                "mint": t.get("token_address", ""),
                                "amount": amt,
                                "symbol": t.get("symbol", ""),
                            }
                        )
                result["tokens"] = tokens
                result["token_count"] = len(tokens)
                result["data_sources"] = [*result.get("data_sources", []), "moralis"]

            # Native balance
            resp2 = await client.get(
                f"https://deep-index.moralis.io/api/v2.2/{address}/balance",
                params={"chain": moralis_chain},
                headers=headers,
            )
            if resp2.status_code == 200:
                bal = resp2.json().get("balance", 0)
                if bal:
                    result["native_balance"] = float(bal) / 1e18

            # Transaction count for activity
            resp3 = await client.get(
                f"https://deep-index.moralis.io/api/v2.2/{address}",
                params={"chain": moralis_chain},
                headers=headers,
            )
            if resp3.status_code == 200:
                wallet_data = resp3.json()
                if wallet_data.get("transactions"):
                    txs = wallet_data["transactions"]
                    result["tx_count"] = len(txs)
                    result["data_sources"] = [*result.get("data_sources", []), "moralis_tx"]
    except Exception:
        pass

    return result


# ═══════════════════════════════════════════
# ETHERSCAN — EVM transaction data (free tier)
# ═══════════════════════════════════════════

ETHERSCAN_APIS = {
    "ethereum": "https://api.etherscan.io/api",
    "bsc": "https://api.bscscan.com/api",
    "polygon": "https://api.polygonscan.com/api",
    "arbitrum": "https://api.arbiscan.io/api",
    "base": "https://api.basescan.org/api",
    "optimism": "https://api-optimistic.etherscan.io/api",
    "avalanche": "https://api.snowtrace.io/api",
    "fantom": "https://api.ftmscan.com/api",
}


async def _fetch_etherscan_wallet(address: str, chain: str) -> dict[str, Any]:
    """Fetch EVM wallet tx count and age from Etherscan family (free tier, no API key needed for basic calls)."""
    if chain not in ETHERSCAN_APIS:
        return {}

    result = {}
    try:
        async with httpx.AsyncClient(timeout=12.0) as client:
            api_url = ETHERSCAN_APIS[chain]

            # Get transaction list (free, no key needed for basic)
            resp = await client.get(
                api_url,
                params={
                    "module": "account",
                    "action": "txlist",
                    "address": address,
                    "startblock": 0,
                    "endblock": 99999999,
                    "page": 1,
                    "offset": 10,
                    "sort": "asc",
                },
            )
            if resp.status_code == 200:
                data = resp.json()
                if data.get("status") == "1":
                    txs = data.get("result", [])
                    if txs:
                        first_ts = int(txs[0].get("timeStamp", 0))
                        last_ts = int(txs[-1].get("timeStamp", 0))
                        if first_ts:
                            result["wallet_age_days"] = int((datetime.now(UTC).timestamp() - first_ts) / 86400)
                        if last_ts:
                            result["last_tx"] = last_ts
                        result["tx_count_sample"] = len(txs)
                        result["data_sources"] = ["etherscan"]
    except Exception:
        pass

    return result


# ═══════════════════════════════════════════
# SENTINEL INTEGRATION — Labels + Intel
# ═══════════════════════════════════════════


async def _fetch_sentinel_labels(address: str, chain: str) -> dict[str, Any]:
    """Get rich labels from SENTINEL address_labeler (6 sources)."""
    try:
        from app.scanners.address_labeler import AddressLabeler

        labeler = AddressLabeler()
        result = await labeler.analyze(address, chain)
        if result:
            return {
                "labels": getattr(result, "labels", []),
                "category": getattr(result, "category", ""),
                "risk_score": getattr(result, "risk_score", 0),
                "sources": getattr(result, "sources", []),
            }
    except Exception as e:
        logger.debug(f"SENTINEL labels failed for {address}: {e}")
    return {}


async def _fetch_wallet_memory(address: str, chain: str) -> dict[str, Any]:
    """Cross-chain identity + cluster intel from Wallet Memory Bank."""
    try:
        from app.wallet_memory.engine import get_wallet_engine

        engine = get_wallet_engine()
        intel = await engine.get_deployer_intelligence(address, chain)
        if intel:
            return {
                "entity_id": intel.get("entity_id"),
                "linked_wallets": intel.get("linked_wallets", []),
                "risk_score": intel.get("risk_score", 0),
                "scam_associations": intel.get("scam_associations", []),
                "deployer_history": intel.get("deployer_history"),
                "cross_chain_presence": intel.get("cross_chain_presence"),
            }
    except Exception as e:
        logger.debug(f"Wallet Memory Bank failed for {address}: {e}")
    return {}


async def _scan_held_tokens(tokens: list, chain: str) -> dict[str, Any]:
    """Scan held tokens using SENTINEL — parallel with full enrichments."""
    if not tokens:
        return {"scanned": 0, "risks": [], "deployers_to_flag": []}

    # Sort by amount (largest first), cap at 5 for speed
    sorted_tokens = sorted(tokens, key=lambda t: float(t.get("amount", 0) if isinstance(t, dict) else 0), reverse=True)
    mints = []
    for token in sorted_tokens[:5]:  # Top 5 by balance
        mint = token.get("mint", "") if isinstance(token, dict) else token
        if mint:
            mints.append(mint)

    if not mints:
        return {"scanned": 0, "risks": [], "deployers_to_flag": []}

    # Parallel scan all tokens
    async def _scan_one(mint: str) -> dict | None:
        try:
            from app.token_scanner import scan_token

            scan = await scan_token(mint, chain, tier="free")
            enrich = {}
            if hasattr(scan, "free") and isinstance(scan.free, dict):
                # Pull key enrichments for wallet context
                e = scan.free
                enrich = {
                    "simulation": e.get("simulation"),
                    "holders": e.get("holders"),
                    "deployer": e.get("deployer"),
                    "deep_deployer": e.get("deep_deployer"),
                    "birdeye": e.get("birdeye"),
                    "copycat_check": e.get("copycat_check"),
                    "volume_anomaly": e.get("volume_anomaly"),
                }
            return {
                "token": mint,
                "symbol": scan.symbol,
                "name": scan.name,
                "safety_score": scan.safety_score,
                "confidence": getattr(scan, "confidence", 0),
                "risk_flags": scan.risk_flags,
                "enrichments": enrich,
            }
        except Exception:
            return None

    try:
        results = await asyncio.wait_for(
            asyncio.gather(*[_scan_one(m) for m in mints], return_exceptions=True), timeout=15.0
        )
    except TimeoutError:
        results = []  # Timeout — return what we have

    risks = []
    deployers_to_flag = []
    highest = None
    for r in results if isinstance(results, list) else []:
        if isinstance(r, dict) and r:
            risks.append(r)
            if r["safety_score"] < 30:
                # Flag deployer for data flywheel
                enrich = r.get("enrichments", {})
                deployer = enrich.get("deployer", {}) or {}
                dev_addr = deployer.get("deployer") if isinstance(deployer, dict) else None
                if dev_addr and r["safety_score"] < 20:
                    deployers_to_flag.append(
                        {
                            "deployer": dev_addr,
                            "token": r["token"],
                            "symbol": r["symbol"],
                            "chain": chain,
                            "risk_score": r["safety_score"],
                            "flags": r["risk_flags"][:3],
                        }
                    )
            if highest is None or r["safety_score"] < highest.get("safety_score", 100):
                highest = r

    return {
        "scanned": len(risks),
        "risks": risks,
        "highest_risk": highest,
        "deployers_to_flag": deployers_to_flag,
        "avg_safety": round(sum(r["safety_score"] for r in risks) / len(risks), 1) if risks else 0,
        "high_risk_count": sum(1 for r in risks if r["safety_score"] < 30),
    }


# ═══════════════════════════════════════════
# UNIFIED WALLET SCAN
# ═══════════════════════════════════════════

# ═══════════════════════════════════════════
# FREE-TIER ENHANCEMENTS
# ═══════════════════════════════════════════


async def _check_hapi_risk(address: str, chain: str) -> dict[str, Any] | None:
    """Query HAPI Protocol on-chain risk scoring.

    HAPI is an on-chain cybersecurity network detecting malicious activity.
    Uses both external and on-chain data for real-time AML screening.
    """
    try:
        # HAPI doesn't have a simple REST API — we query their public data
        # or use their subgraph. For now, we check their public status endpoint.
        async with httpx.AsyncClient(timeout=10.0) as client:
            # HAPI Protocol public API (if available)
            resp = await client.get(
                f"https://hapi.one/api/v1/address/{address}",
                params={"chain": chain.lower()},
            )
            if resp.status_code == 200:
                data = resp.json()
                return {
                    "risk_score": data.get("risk_score", 0),
                    "risk_level": data.get("risk_level", "unknown"),
                    "is_malicious": data.get("is_malicious", False),
                    "categories": data.get("categories", []),
                    "source": "hapi",
                }
    except Exception as e:
        logger.debug(f"HAPI check failed: {e}")
    return None


async def _check_misttrack_light(address: str, chain: str) -> dict[str, Any] | None:
    """Query SlowMist MistTrack Light — free wallet risk assessment.

    Provides preliminary risk profiling and graph analysis.
    """
    try:
        # MistTrack web interface scraping or API if available
        # The free tier is via their web app; we'll try their public API
        async with httpx.AsyncClient(timeout=12.0) as client:
            resp = await client.get(
                f"https://misttrack.io/api/v1/address/{address}",
                params={"chain": chain.lower()},
            )
            if resp.status_code == 200:
                data = resp.json()
                return {
                    "risk_score": data.get("risk_score", 0),
                    "risk_level": data.get("risk_level", "unknown"),
                    "labels": data.get("labels", []),
                    "related_addresses": data.get("related", [])[:5],
                    "source": "misttrack",
                }
    except Exception as e:
        logger.debug(f"MistTrack check failed: {e}")
    return None


async def _check_honeydb_ip(ip_address: str) -> dict[str, Any] | None:
    """Query HoneyDB for threat actor data on IP address.

    Community-driven honeypot data aggregation. 1,500 req/month free.
    Useful for correlating IP addresses with crypto scam infrastructure.
    """
    try:
        api_id = os.getenv("HONEYDB_API_ID", "")
        api_key = os.getenv("HONEYDB_API_KEY", "")
        if not api_id or not api_key:
            return None

        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(
                "https://honeydb.io/api/v1/indicator/ipv4",
                params={"ip": ip_address},
                headers={"X-HoneyDB-APIID": api_id, "X-HoneyDB-APIKey": api_key},
            )
            if resp.status_code == 200:
                data = resp.json()
                return {
                    "is_threat": data.get("is_threat", False),
                    "threat_score": data.get("threat_score", 0),
                    "events": data.get("events", []),
                    "source": "honeydb",
                }
    except Exception as e:
        logger.debug(f"HoneyDB check failed: {e}")
    return None


async def _check_trm_sanctions_wallet(address: str) -> dict[str, Any] | None:
    """Screen wallet address against TRM Labs sanctions list.

    Free tier: 1 req/sec, 100/day unauthenticated.
    Authenticated: 1K req/sec, 100K/day.
    """
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(
                "https://api.trmlabs.com/public/v1/sanctions/screening",
                json={"address": [address]},
                headers={"Content-Type": "application/json"},
            )
            if resp.status_code == 201:
                data = resp.json()
                results = data.get("results", [])
                if results:
                    r = results[0]
                    return {
                        "is_sanctioned": r.get("isSanctioned", False),
                        "screening_address": r.get("address", address),
                        "source": "trm_labs",
                        "risk_level": "critical" if r.get("isSanctioned") else "low",
                    }
            elif resp.status_code == 429:
                logger.warning("TRM Labs rate limit hit")
                return {"source": "trm_labs", "rate_limited": True, "is_sanctioned": False}
    except Exception as e:
        logger.debug(f"TRM sanctions check failed: {e}")
    return None


async def _trace_funding_source(address: str, chain: str) -> dict[str, Any]:
    """Trace initial funding source using on-chain transaction history."""
    try:
        if chain == "solana":
            key = _get_helius_key()
            if not key:
                return {}
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.get(
                    f"https://api.helius.xyz/v0/addresses/{address}/transactions",
                    params={"api-key": key, "limit": 10, "type": "TRANSFER"},
                )
                if resp.status_code != 200:
                    return {}
                txs = resp.json()
                if not txs:
                    return {"source": "unknown", "hops": 0}

                # Find first incoming transfer
                first_in = None
                for tx in txs:
                    transfers = tx.get("tokenTransfers", []) + tx.get("nativeTransfers", [])
                    for t in transfers:
                        if t.get("toUserAccount") == address:
                            first_in = t
                            break
                    if first_in:
                        break

                source = first_in.get("fromUserAccount", "unknown") if first_in else "unknown"
                source_label = await _resolve_entity_free(source, chain)
                return {
                    "source": source,
                    "source_label": source_label.get("label", "unknown"),
                    "is_cex": "exchange" in str(source_label).lower() or "cex" in str(source_label).lower(),
                    "is_mixer": "mixer" in str(source_label).lower() or "tornado" in str(source_label).lower(),
                    "hops": 1,
                }
        return {"source": "unknown", "hops": 0}
    except Exception:
        return {"source": "unknown", "hops": 0}


async def _resolve_entity_free(address: str, chain: str) -> dict[str, Any]:
    """Basic cross-chain entity resolution — FREE tier (no Wallet Memory Bank)."""
    try:
        # Check SENTINEL labels first
        from app.scanners.address_labeler import AddressLabeler

        labeler = AddressLabeler()
        labels = await labeler.analyze(address, chain)
        if labels and labels.primary_label:
            return {"label": labels.primary_label, "source": "sentinel", "chains": [chain]}

        # Check wallet_labels CSV
        from app.wallet_memory.labels import get_label

        label = get_label(address, chain)
        if label:
            return {"label": label, "source": "wallet_labels", "chains": [chain]}

        return {"label": "unknown", "source": "none", "chains": [chain]}
    except Exception:
        return {"label": "unknown", "source": "none", "chains": [chain]}


async def _calculate_basic_pnl(address: str, chain: str, tokens: list, native_balance: float = 0) -> dict[str, Any]:
    """Basic PnL calculation — FREE tier (no GMGN)."""
    try:
        total_value = native_balance or 0
        token_count = len(tokens)

        if tokens and chain == "solana":
            # Try Jupiter price lookup for tokens
            mints = [t.get("mint", "") for t in tokens[:10] if t.get("mint")]
            if mints:
                async with httpx.AsyncClient(timeout=8.0) as client:
                    resp = await client.get("https://quote-api.jup.ag/v6/price", params={"ids": ",".join(mints[:5])})
                    if resp.status_code == 200:
                        prices = resp.json().get("data", {})
                        for mint, pdata in prices.items():
                            price = float(pdata.get("price", 0))
                            for t in tokens:
                                if t.get("mint") == mint:
                                    balance = float(t.get("amount", 0)) / (10 ** (t.get("decimals", 0) or 0))
                                    total_value += balance * price

        return {
            "estimated_value_usd": round(total_value, 2),
            "token_count": token_count,
            "priced_tokens": min(len(tokens), 5),
            "method": "jupiter" if total_value > native_balance else "native_only",
        }
    except Exception:
        return {
            "estimated_value_usd": round(native_balance, 2),
            "token_count": len(tokens),
            "method": "native_only",
        }


async def _close_data_flywheel(deployers: list):
    """Feed scam findings back to Wallet Memory Bank."""
    if not deployers:
        return
    try:
        from app.wallet_memory.engine import get_wallet_engine

        engine = get_wallet_engine()
        for d in deployers[:5]:  # Limit to 5
            await engine.flag_deployer(
                d["deployer"],
                d["chain"],
                reason=f"scam_token_{d['symbol']}",
                token=d["token"],
                score=100 - d["risk_score"],
            )
        logger.info(f"Data flywheel: flagged {len(deployers[:5])} deployers")
    except Exception as e:
        logger.warning(f"Data flywheel failed: {e}")


async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "free") -> dict[str, Any]:
    """Unified wallet scanner — SENTINEL + Helius/Moralis/Etherscan pool + Wallet Memory + Token Scanner."""
    import time

    # ── Redis cache check (1hr TTL, skips repeat scans) ──
    cache_key = hashlib.sha256(f"{chain}:{wallet_address}:{tier}".encode()).hexdigest()[:16]
    cached = await _cache_get(cache_key)
    if cached:
        cached["cached"] = True
        logger.info(f"Wallet scan {wallet_address[:12]}... [{chain}] = CACHED")
        return cached

    factors = WalletRiskFactors()
    start = time.time()
    data_sources = []
    token_list = []
    modules_run = []  # Track which modules succeeded/failed

    # ── On-chain data: chain-specific multi-source fetchers ──
    onchain = {}
    if chain == "solana":
        # Solana: Helius (credential pool) + fallback to public RPC
        onchain = await _fetch_helius_wallet(wallet_address)
        if not onchain:
            # Fallback to free public Solana RPC
            try:
                async with httpx.AsyncClient(timeout=10.0) as client:
                    resp = await client.post(
                        "https://api.mainnet-beta.solana.com",
                        json={
                            "jsonrpc": "2.0",
                            "id": 1,
                            "method": "getBalance",
                            "params": [wallet_address],
                        },
                    )
                    if resp.status_code == 200:
                        bal = resp.json().get("result", {}).get("value", 0)
                        onchain = {
                            "sol_balance": bal / 1e9 if bal else 0,
                            "data_sources": ["solana_public"],
                        }
            except Exception:
                pass
    else:
        # EVM chain: Moralis (primary) + Etherscan family (fallback)
        onchain = await _fetch_moralis_wallet(wallet_address, chain)
        etherscan = await _fetch_etherscan_wallet(wallet_address, chain)
        if etherscan and not onchain.get("wallet_age_days"):
            onchain["wallet_age_days"] = etherscan.get("wallet_age_days", 0)
            onchain["data_sources"] = onchain.get("data_sources", []) + etherscan.get("data_sources", [])

    if onchain:
        factors.wallet_age_days = onchain.get("wallet_age_days", 0)
        factors.total_tx_count = onchain.get("tx_count", onchain.get("tx_count_sample", 0))
        factors.current_balance_usd = onchain.get("sol_balance", onchain.get("native_balance", 0))
        token_list = onchain.get("tokens", [])
        factors.unique_tokens_held = onchain.get("token_count", len(token_list))
        if onchain.get("last_tx"):
            last_ts = onchain["last_tx"]
            if isinstance(last_ts, (int, float)):
                factors.days_since_last_tx = int((datetime.now(UTC).timestamp() - last_ts) / 86400)
        data_sources.extend(onchain.get("data_sources", []))

    # ── DexScreener volume + labels (FREE tier, all chains) ──
    try:
        async with httpx.AsyncClient(timeout=15.0) as client:
            resp = await client.get("https://api.dexscreener.com/latest/dex/search", params={"q": wallet_address})
            if resp.status_code == 200:
                pairs = resp.json().get("pairs", [])
                if pairs:
                    factors.total_volume_usd = sum(float(p.get("volume", {}).get("h24", 0) or 0) for p in pairs)
                    factors.dex_swaps = len(pairs)
                    factors.token_launches_participated = sum(
                        1
                        for p in pairs
                        if p.get("pairCreatedAt")
                        and (
                            datetime.now(UTC) - datetime.fromtimestamp(p.get("pairCreatedAt", 0) / 1000, tz=UTC)
                        ).total_seconds()
                        / 3600
                        < 24
                    )
                    data_sources.append("dexscreener")
    except Exception:
        pass

    # ── SENTINEL address labels (6-source label resolution, FREE tier) ──
    sentinel_labels = await _fetch_sentinel_labels(wallet_address, chain)
    if sentinel_labels:
        factors.entity_label = sentinel_labels.get("category", "")
        factors.is_labeled_scammer = factors.is_labeled_scammer or "scam" in str(sentinel_labels).lower()
        factors.is_labeled_sanctioned = (
            factors.is_labeled_sanctioned
            or "sanction" in str(sentinel_labels).lower()
            or "ofac" in str(sentinel_labels).lower()
        )
        factors.is_labeled_exchange = (
            "exchange" in str(sentinel_labels).lower() or "cex" in str(sentinel_labels).lower()
        )
        factors.is_labeled_bot = "bot" in str(sentinel_labels).lower() or "mev" in str(sentinel_labels).lower()
        factors.is_labeled_whale = "whale" in str(sentinel_labels).lower()
        factors.is_labeled_insider = "insider" in str(sentinel_labels).lower()
        factors.entity_confidence = max(factors.entity_confidence, sentinel_labels.get("risk_score", 50))
        data_sources.append("sentinel_labels")

        # ── FREE: External threat intelligence (HAPI, MistTrack, TRM sanctions) ──
    hapi_risk = await _check_hapi_risk(wallet_address, chain)
    if hapi_risk:
        if hapi_risk.get("is_malicious"):
            factors.is_labeled_scammer = True
            factors.scam_token_exposure = max(factors.scam_token_exposure, 1)
        data_sources.append("hapi")

    misttrack = await _check_misttrack_light(wallet_address, chain)
    if misttrack:
        if misttrack.get("risk_score", 0) > 70:
            factors.is_labeled_scammer = True
        data_sources.append("misttrack")

    trm = await _check_trm_sanctions_wallet(wallet_address)
    if trm:
        if trm.get("is_sanctioned"):
            factors.is_labeled_sanctioned = True
            factors.funding_from_sanctioned = True
        data_sources.append("trm_labs")

    # ── PRO: Wallet Memory Bank (cross-chain identity + clusters) ──
    if tier in ("pro", "elite"):
        wallet_memory = await _fetch_wallet_memory(wallet_address, chain)
        if wallet_memory:
            factors.linked_wallets_count = len(wallet_memory.get("linked_wallets", []))
            factors.cluster_size = wallet_memory.get("linked_wallets_count", factors.linked_wallets_count)
            factors.cluster_id = wallet_memory.get("entity_id", "")
            factors.cross_chain_funding = bool(wallet_memory.get("cross_chain_presence"))
            scam_assoc = wallet_memory.get("scam_associations", [])
            if scam_assoc:
                factors.is_labeled_scammer = True
                factors.scam_token_exposure = len(scam_assoc)
            hist = wallet_memory.get("deployer_history", {}) or {}
            factors.rug_pull_launcher_count = hist.get("rugs", 0)
            data_sources.append("wallet_memory")

        # GMGN scoring
        try:
            from app.gmgn_client import GMGNClient

            gmgn = GMGNClient()
            intel = await gmgn.get_wallet_intelligence(wallet_address)
            if intel:
                factors.insider_trading_confidence = float(intel.get("insider_score", 0))
                factors.sniper_activity_score = float(intel.get("sniper_score", 0))
                factors.wash_trading_score = float(intel.get("wash_trading_score", 0))
                factors.pump_and_dump_participant = intel.get("pump_dump_participant", False)
                factors.new_token_flipper = intel.get("flipper", False)
                pnl = intel.get("pnl", {}) or {}
                factors.realized_pnl_usd = float(pnl.get("realized", 0))
                factors.unrealized_pnl_usd = float(pnl.get("unrealized", 0))
                factors.win_rate = float(intel.get("win_rate", 0))
                data_sources.append("gmgn")
        except Exception:
            pass

        # Entity clustering
        try:
            from app.entity_clustering import EntityClusteringEngine

            engine = EntityClusteringEngine()
            cluster = engine.find_cluster(wallet_address)
            if cluster:
                factors.cluster_size = max(factors.cluster_size, cluster.get("size", 0))
                factors.cluster_shared_funding = cluster.get("shared_funding", False)
                factors.known_cluster_type = cluster.get("type", "")
                factors.linked_wallets_count = max(factors.linked_wallets_count, cluster.get("member_count", 0))
        except Exception:
            pass

    # ── FREE: Token portfolio risk scan (parallel + enrichments) ──
    token_scan = await _scan_held_tokens(token_list, chain)
    if token_scan:
        factors.held_token_risks = token_scan.get("risks", [])
        factors.highest_risk_token = token_scan.get("highest_risk")
        high_risk_count = token_scan.get("high_risk_count", 0)
        factors.honeypot_tokens_owned = high_risk_count
        factors.avg_held_token_safety = token_scan.get("avg_safety", 0)

        # Data flywheel: flag deployers of scam tokens
        deployers = token_scan.get("deployers_to_flag", [])
        if deployers:
            asyncio.create_task(_close_data_flywheel(deployers))

    # ── FREE: Funding source tracing ──
    funding = await _trace_funding_source(wallet_address, chain)
    if funding:
        factors.funding_source = funding.get("source", factors.funding_source)
        factors.funding_source_label = funding.get("source_label", factors.funding_source_label)
        factors.funding_source_is_cex = funding.get("is_cex", False)
        factors.funding_source_is_mixer = funding.get("is_mixer", False)
        factors.funding_hop_count = funding.get("hops", 0)
        data_sources.append("funding_trace")

    # ── FREE: Basic cross-chain identity ──
    free_entity = await _resolve_entity_free(wallet_address, chain)
    if free_entity and free_entity.get("label") != "unknown":
        if not factors.entity_label:
            factors.entity_label = free_entity["label"]
        if not factors.entity_confidence:
            factors.entity_confidence = 60  # free-tier confidence
        data_sources.append("free_entity")

    # ── FREE: Basic PnL ──
    native_bal = onchain.get("sol_balance", onchain.get("native_balance", 0)) if onchain else 0
    basic_pnl = await _calculate_basic_pnl(wallet_address, chain, token_list, native_bal)
    if basic_pnl:
        factors.current_balance_usd = max(factors.current_balance_usd or 0, basic_pnl.get("estimated_value_usd", 0))
        factors.realized_pnl_usd = factors.realized_pnl_usd or 0  # keep if GMGN set it
        data_sources.append("jupiter_pnl")

    # ── ELITE: ML anomaly + portfolio tracker ──
    if tier == "elite":
        # ML anomaly detection
        try:
            from app.ml_anomaly import detect_wallet_anomaly

            anomaly = await detect_wallet_anomaly(wallet_address, chain)
            if anomaly:
                factors.sybil_score = anomaly.get("sybil_score", 0)
        except Exception:
            pass

        # Portfolio tracker
        try:
            from app.portfolio_tracker import get_portfolio

            portfolio = await get_portfolio(wallet_address, chain)
            if portfolio:
                factors.current_balance_usd = max(
                    factors.current_balance_usd, float(portfolio.get("total_value_usd", 0))
                )
                factors.portfolio_diversity_score = portfolio.get("diversity_score", 0)
                factors.stablecoin_ratio = portfolio.get("stablecoin_pct", 0)
                factors.bluechip_ratio = portfolio.get("bluechip_pct", 0)
                factors.memecoin_ratio = portfolio.get("memecoin_pct", 0)
        except Exception:
            pass

    # ═══════════════════════════════════════════
    # Risk scoring
    # ═══════════════════════════════════════════

    risk = 0
    flags = []

    # Labels (strongest signal)
    if factors.is_labeled_sanctioned:
        risk += 50
        flags.append("SANCTIONED")
    if factors.is_labeled_scammer:
        risk += 45
        flags.append("KNOWN_SCAMMER")
    if factors.is_labeled_insider:
        risk += 25
        flags.append("INSIDER_LABELED")
    if factors.is_labeled_bot:
        risk += 10
        flags.append("BOT_WALLET")
    if factors.is_labeled_exchange:
        risk -= 10  # exchanges are neutral

    # Funding
    if factors.funding_source_is_mixer:
        risk += 25
        flags.append("MIXER_FUNDING")
    if factors.funding_from_sanctioned:
        risk += 40
        flags.append("SANCTIONED_FUNDING")
    if factors.tornado_cash_exposure:
        risk += 35
        flags.append("TORNADO_CASH")

    # Age/Activity (only flag if wallet_age actually populated)
    if factors.wallet_age_days > 0:
        if factors.wallet_age_days < 1:
            risk += 15
            flags.append("FRESH_WALLET")
        elif factors.wallet_age_days < 7 and factors.total_volume_usd > 100000:
            risk += 20
            flags.append("NEW_WALLET_HIGH_VOLUME")
    elif factors.total_volume_usd > 50000:
        risk += 10
        flags.append("UNVERIFIED_HIGH_VOLUME")

    # Held token risk (FREE tier — from parallel token scan)
    if factors.honeypot_tokens_owned > 0:
        risk += 25
        flags.append(f"HOLDS_SCAM_TOKENS_{factors.honeypot_tokens_owned}")
    if factors.avg_held_token_safety and factors.avg_held_token_safety < 40:
        risk += 15
        flags.append("LOW_QUALITY_PORTFOLIO")

    # Token interaction
    if factors.rug_pull_launcher_count > 0:
        risk += 50
        flags.append("RUG_PULL_LAUNCHER")
    if factors.honeypot_tokens_owned > 0:
        risk += 20
        flags.append("HONEYPOT_HOLDER")
    if factors.sniper_activity_score > 70:
        risk += 20
        flags.append("HIGH_SNIPER")
    if factors.sandwich_attacks_executed > 0:
        risk += 25
        flags.append("SANDWICH_ATTACKER")
    if factors.flash_loan_usage > 10:
        risk += 15
        flags.append("HEAVY_FLASH_LOAN")

    # Behavioral
    if factors.insider_trading_confidence > 70:
        risk += 30
        flags.append("INSIDER_TRADING")
    if factors.wash_trading_score > 60:
        risk += 20
        flags.append("WASH_TRADING")
    if factors.pump_and_dump_participant:
        risk += 20
        flags.append("PUMP_DUMP_PARTICIPANT")

    # Cluster
    if factors.cluster_size > 5 and factors.cluster_shared_funding:
        risk += 20
        flags.append("LARGE_SHARED_CLUSTER")
    if factors.known_cluster_type in ("bot_farm", "sniper_ring"):
        risk += 15
        flags.append("SUSPICIOUS_CLUSTER")

    # ML
    if factors.sybil_score > 70:
        risk += 15
        flags.append("SYBIL_DETECTED")

    # Token portfolio risk (ELITE only)
    if factors.highest_risk_token:
        ts = factors.highest_risk_token
        if ts.get("safety_score", 100) < 25:
            risk += 15
            flags.append(f"HOLDS_CRITICAL_TOKEN_{ts.get('symbol', '???')}")

    factors.total_risk_score = min(100, max(0, risk))
    factors.risk_category = (
        "critical"
        if risk >= 80
        else "high_risk"
        if risk >= 60
        else "medium"
        if risk >= 35
        else "low_risk"
        if risk >= 15
        else "safe"
    )

    # Confidence based on data quality
    confidence = 15  # base
    if "helius" in data_sources:
        confidence += 20
    if "sentinel_labels" in data_sources:
        confidence += 20
    if "dexscreener" in data_sources:
        confidence += 10
    if "wallet_memory" in data_sources:
        confidence += 15
    if "gmgn" in data_sources:
        confidence += 15
    confidence += min(len(flags) * 2, 15)  # flags add some confidence
    factors.confidence = min(100, confidence)
    factors.data_sources = data_sources

    # Build modules_run list from data_sources with status
    for src in data_sources:
        modules_run.append({"module": src, "status": "ok"})
    # Add known modules that were attempted but failed (not in data_sources)
    attempted_modules = [
        "helius_wallet",
        "moralis_wallet",
        "etherscan_wallet",
        "dexscreener",
        "rugcheck",
        "goplus_security",
        "wallet_labels",
        "wallet_memory",
        "sentinel_risk",
        "gmgn",
        "funding_trace",
        "free_entity",
        "jupiter_pnl",
        "entity_clustering",
    ]
    succeeded_set = set(data_sources)
    for mod in attempted_modules:
        if mod not in succeeded_set:
            modules_run.append({"module": mod, "status": "degraded", "error": "no_data_returned"})

    elapsed = time.time() - start
    logger.info(
        f"Wallet scan {wallet_address[:12]}... [{chain}] = {factors.total_risk_score}/100 ({factors.risk_category}) conf={factors.confidence}% in {elapsed:.1f}s"
    )

    result = {
        "wallet": wallet_address,
        "chain": chain,
        "risk_score": factors.total_risk_score,
        "risk_category": factors.risk_category,
        "risk_flags": flags[:20],
        "total_flags": len(flags),
        "confidence": factors.confidence,
        "tier": tier,
        "data_sources": factors.data_sources,
        "factors": {
            "wallet_age_days": factors.wallet_age_days,
            "total_tx_count": factors.total_tx_count,
            "total_volume_usd": factors.total_volume_usd,
            "unique_tokens_held": factors.unique_tokens_held,
            "funding_source": factors.funding_source,
            "funding_source_label": factors.funding_source_label,
            "funding_source_is_cex": factors.funding_source_is_cex,
            "funding_source_is_mixer": factors.funding_source_is_mixer,
            "funding_from_sanctioned": factors.funding_from_sanctioned,
            "funding_from_scammer": factors.funding_from_scammer,
            "is_labeled_scammer": factors.is_labeled_scammer,
            "is_labeled_sanctioned": factors.is_labeled_sanctioned,
            "is_labeled_exchange": factors.is_labeled_exchange,
            "is_labeled_bot": factors.is_labeled_bot,
            "is_labeled_whale": factors.is_labeled_whale,
            "is_labeled_insider": factors.is_labeled_insider,
            "entity_label": factors.entity_label,
            "entity_confidence": factors.entity_confidence,
            "cluster_size": factors.cluster_size,
            "cluster_shared_funding": factors.cluster_shared_funding,
            "known_cluster_type": factors.known_cluster_type,
            "linked_wallets_count": factors.linked_wallets_count,
            "cross_chain_funding": factors.cross_chain_funding,
            "tornado_cash_exposure": factors.tornado_cash_exposure,
            "sniper_activity_score": factors.sniper_activity_score,
            "insider_trading_confidence": factors.insider_trading_confidence,
            "wash_trading_score": factors.wash_trading_score,
            "pump_and_dump_participant": factors.pump_and_dump_participant,
            "sybil_score": factors.sybil_score,
            "rug_pull_launcher_count": factors.rug_pull_launcher_count,
            "honeypot_tokens_owned": factors.honeypot_tokens_owned,
            "sandwich_attacks_executed": factors.sandwich_attacks_executed,
            "flash_loan_usage": factors.flash_loan_usage,
            "realized_pnl_usd": factors.realized_pnl_usd,
            "unrealized_pnl_usd": factors.unrealized_pnl_usd,
            "win_rate": factors.win_rate,
            "current_balance_usd": factors.current_balance_usd,
            "portfolio_diversity_score": factors.portfolio_diversity_score,
            "stablecoin_ratio": factors.stablecoin_ratio,
            "bluechip_ratio": factors.bluechip_ratio,
            "memecoin_ratio": factors.memecoin_ratio,
            "dex_swaps": factors.dex_swaps,
            "cex_deposits": factors.cex_deposits,
            "cex_withdrawals": factors.cex_withdrawals,
            "token_launches_participated": factors.token_launches_participated,
        },
        "held_tokens": {
            "scanned": len(factors.held_token_risks),
            "highest_risk": factors.highest_risk_token,
            "risks": factors.held_token_risks[:10],
        },
        "modules_run": modules_run,
        "scanned_at": datetime.now(UTC).isoformat(),
        "tool_fingerprints": None,
    }

    # Cache result for 1hr
    asyncio.create_task(_cache_set(cache_key, result, ttl=3600))

    return result


# ═══════════════════════════════════════════════════════════════
# TRENDING ENGINE — Multi-source, real-time
# ═══════════════════════════════════════════════════════════════


async def get_trending_tokens(chain: str | None = None, limit: int = 20) -> dict[str, Any]:
    """Get trending tokens from multiple sources, normalized across chains."""
    import time

    start = time.time()

    trending = []
    sources_used = []

    async with httpx.AsyncClient(timeout=15.0) as client:
        try:
            url = "https://api.dexscreener.com/latest/dex/search"
            params = {"q": chain or "SOL", "limit": limit}
            resp = await client.get(url, params=params)
            if resp.status_code == 200:
                pairs = resp.json().get("pairs", [])
                now = datetime.now(UTC)
                for pair in pairs:
                    created = pair.get("pairCreatedAt")
                    liq = float(pair.get("liquidity", {}).get("usd", 0))
                    vol = float(pair.get("volume", {}).get("h24", 0))
                    if liq < 1000:
                        continue
                    age_hours = 999
                    if created:
                        age_hours = (now - datetime.fromtimestamp(created / 1000, tz=UTC)).total_seconds() / 3600
                    trending_score = (vol * 0.6 + liq * 0.3) / max(age_hours, 1)
                    trending.append(
                        {
                            "chain": pair.get("chainId", chain or "unknown"),
                            "address": pair.get("baseToken", {}).get("address", ""),
                            "symbol": pair.get("baseToken", {}).get("symbol", ""),
                            "name": pair.get("baseToken", {}).get("name", ""),
                            "price_usd": float(pair.get("priceUsd", 0)),
                            "liquidity_usd": liq,
                            "volume_24h": vol,
                            "price_change_24h": float(pair.get("priceChange", {}).get("h24", 0)),
                            "age_hours": round(age_hours, 1),
                            "trending_score": round(trending_score, 2),
                            "dex": pair.get("dexId", ""),
                            "source": "dexscreener",
                        }
                    )
                sources_used.append("dexscreener")
        except Exception as e:
            logger.warning(f"DexScreener trending failed: {e}")

        try:
            network = "solana" if chain == "solana" else "eth"
            resp = await client.get(
                f"https://api.geckoterminal.com/api/v2/networks/{network}/trending_pools",
                params={"limit": limit},
            )
            if resp.status_code == 200:
                pools = resp.json().get("data", [])
                for pool in pools:
                    attrs = pool.get("attributes", {})
                    trending.append(
                        {
                            "chain": chain or "unknown",
                            "address": attrs.get("base_token_address", ""),
                            "symbol": attrs.get("base_token_symbol", ""),
                            "name": attrs.get("name", ""),
                            "price_usd": float(attrs.get("base_token_price_usd", 0)),
                            "liquidity_usd": float(attrs.get("reserve_in_usd", 0)),
                            "volume_24h": float(attrs.get("volume_usd", {}).get("h24", 0)),
                            "price_change_24h": float(attrs.get("price_change_percentage", {}).get("h24", 0)),
                            "source": "geckoterminal",
                        }
                    )
                sources_used.append("geckoterminal")
        except Exception:
            pass

        try:
            resp = await client.get("https://api.coingecko.com/api/v3/search/trending")
            if resp.status_code == 200:
                coins = resp.json().get("coins", [])
                for item in coins[:limit]:
                    coin = item.get("item", {})
                    trending.append(
                        {
                            "chain": chain or "multi",
                            "address": coin.get("id", ""),
                            "symbol": coin.get("symbol", "").upper(),
                            "name": coin.get("name", ""),
                            "price_usd": coin.get("data", {}).get("price", 0),
                            "market_cap_rank": coin.get("market_cap_rank"),
                            "trending_score": coin.get("score", 0),
                            "source": "coingecko",
                        }
                    )
                sources_used.append("coingecko")
        except Exception:
            pass

    seen = set()
    unique = []
    for t in trending:
        key = f"{t.get('chain')}:{t.get('address')}"
        if key not in seen:
            seen.add(key)
            unique.append(t)

    unique.sort(key=lambda x: x.get("trending_score", 0), reverse=True)

    elapsed = time.time() - start
    logger.info(f"Trending: {len(unique)} tokens from {sources_used} in {elapsed:.1f}s")

    return {
        "tokens": unique[:limit],
        "total": len(unique),
        "sources": sources_used,
        "chains_covered": list({t.get("chain", "?") for t in unique}),
        "updated_at": datetime.now(UTC).isoformat(),
    }


# ═══════════════════════════════════════════════════════════════
# REAL-TIME LAUNCH MONITOR
# ═══════════════════════════════════════════════════════════════

LAST_KNOWN_LAUNCHES: dict[str, set] = {}


async def get_new_launches(chain: str = "solana", since_seconds: int = 300) -> dict[str, Any]:
    """Get tokens launched in the last N seconds."""
    import time

    start = time.time()

    now = datetime.now(UTC)
    cutoff = (now.timestamp() * 1000) - (since_seconds * 1000)

    launches = []
    async with httpx.AsyncClient(timeout=10.0) as client:
        try:
            resp = await client.get("https://api.dexscreener.com/latest/dex/search", params={"q": chain, "limit": 50})
            if resp.status_code == 200:
                pairs = resp.json().get("pairs", [])
                for pair in pairs:
                    created = pair.get("pairCreatedAt", 0)
                    if created and created > cutoff:
                        liq = float(pair.get("liquidity", {}).get("usd", 0))
                        if liq < 500:
                            continue
                        launches.append(
                            {
                                "chain": pair.get("chainId", chain),
                                "address": pair.get("baseToken", {}).get("address", ""),
                                "symbol": pair.get("baseToken", {}).get("symbol", ""),
                                "name": pair.get("baseToken", {}).get("name", ""),
                                "price_usd": float(pair.get("priceUsd", 0)),
                                "liquidity_usd": liq,
                                "age_seconds": int((now.timestamp() * 1000 - created) / 1000),
                                "dex": pair.get("dexId", ""),
                            }
                        )
        except Exception:
            pass

    chain_key = chain or "all"
    if chain_key not in LAST_KNOWN_LAUNCHES:
        LAST_KNOWN_LAUNCHES[chain_key] = set()

    known = LAST_KNOWN_LAUNCHES[chain_key]
    new_launches = [line for line in launches if line["address"] not in known]
    for line in new_launches:
        known.add(line["address"])

    # Keep set bounded
    if len(known) > 10000:
        LAST_KNOWN_LAUNCHES[chain_key] = set(list(known)[-5000:])

    elapsed = time.time() - start
    logger.info(f"New launches [{chain}]: {len(new_launches)} in {elapsed:.1f}s")

    return {
        "launches": new_launches,
        "total_new": len(new_launches),
        "total_known": len(known),
        "chain": chain,
        "since_seconds": since_seconds,
        "updated_at": datetime.now(UTC).isoformat(),
    }
