"""
Static Label Importer — Async boot-time loader for 105K+ wallet labels.
========================================================================
Replaces the sync Redis-only wallet_label_loader.py with an async module that:

1. Downloads/locates crypto-wallet-address-labels datasets (Solana CEX, DApp, DeFi)
2. Loads etherscan CSVs from /root/backend/data/
3. Fetches OFAC crypto-sanctions-list
4. Stores everything through WalletStorage (Redis cache + ClickHouse)

Sources:
  - crypto-wallet-address-labels GitHub (105K+ Solana)
  - etherscan_combined_labels.csv (50K rows)
  - etherscan_phish_hack.csv (111 rows)
  - OFAC crypto-sanctions-list
"""

import contextlib
import csv
import json
import logging
import os
import time
from datetime import UTC, datetime
from typing import Any

import httpx

logger = logging.getLogger("wallet_memory.label_importer")

# ── Paths ──────────────────────────────────────────────────────────────

DATA_DIR = os.getenv("RMI_DATA_DIR", "/app/data")
LABELS_DIR = os.path.join(DATA_DIR, "wallet-labels")
ETHERSCAN_COMBINED = os.path.join(DATA_DIR, "etherscan_combined_labels.csv")
ETHERSCAN_PHISH_HACK = os.path.join(DATA_DIR, "etherscan_phish_hack.csv")

# GitHub base for Solana label CSVs
SOLANA_LABELS_REPO = (
    "https://raw.githubusercontent.com/ImMike/"
    "crypto-wallet-address-labels/main/datasets/solana-wallet-and-program-labels"
)

# Additional ImMike repo dataset paths
BSC_LABELS_REPO = (
    "https://raw.githubusercontent.com/ImMike/crypto-wallet-address-labels/main/datasets/bsc-ethereum-address-tags"
)
ETH_EXCHANGE_LABELS_REPO = (
    "https://raw.githubusercontent.com/ImMike/"
    "crypto-wallet-address-labels/main/datasets/ethereum-exchange-and-dex-labels"
)
ETH_CLUSTER_LABELS_REPO = (
    "https://raw.githubusercontent.com/ImMike/"
    "crypto-wallet-address-labels/main/datasets/ethereum-cluster-exchange-labels"
)
ETH_CONTRACT_LABELS_REPO = (
    "https://raw.githubusercontent.com/ImMike/crypto-wallet-address-labels/main/datasets/ethereum-contract-name-labels"
)

# OFAC sanctions URL
OFAC_URL = "https://raw.githubusercontent.com/OFAC/crypto-sanctions-list/main/crypto-sanctions-list.json"

# Solana label file specs: (filename, chain, address_col, label_col_map)
SOLANA_CSV_SPECS = [
    (
        "solana_cex_labels.csv",
        "solana",
        "address",
        {
            "label_type": "label_type",
            "label_subtype": "label_subtype",
            "name": "address_name",
            "project": "project_name",
        },
    ),
    (
        "solana_dapp_labels.csv",
        "solana",
        "address",
        {
            "name": "dapp_name",
        },
    ),
    (
        "solana_defi_labels.csv",
        "solana",
        "address",
        {
            "name": "protocol_name",
        },
    ),
]

# Additional Ethereum phishing/scam CSV specs from the same repo
ETH_CSV_SPECS = [
    (
        "phishing_scams.csv",
        "ethereum",
        "address",
        {
            "name": "etherscan_tag",
            "label_type": "etherscan_labels",
        },
    ),
    (
        "etherscan_malicious_labels.csv",
        "ethereum",
        "banned_address",
        {
            "name": "wallet_tag",
            "label_type": "data_source",
        },
    ),
    (
        "malicious_smart_contracts.csv",
        "ethereum",
        "contract_address",
        {
            "name": "contract_tag",
            "label_type": "source",
        },
    ),
]

# ClickHouse DDL for wallet_labels table (added to schema on first run)
WALLET_LABELS_DDL = """
CREATE TABLE IF NOT EXISTS wallet_labels (
    address         String,
    chain_id        LowCardinality(String),
    label_name      String DEFAULT '',
    label_category  LowCardinality(String) DEFAULT '',
    label_subtype   String DEFAULT '',
    source          LowCardinality(String) DEFAULT '',
    is_sanctioned   UInt8 DEFAULT 0,
    loaded_at       DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(loaded_at)
ORDER BY (address, chain_id, source);
"""

# ── Download helpers ────────────────────────────────────────────────────

_http_client: httpx.AsyncClient | None = None


async def _get_http() -> httpx.AsyncClient:
    global _http_client
    if _http_client is None or _http_client.is_closed:
        _http_client = httpx.AsyncClient(
            timeout=30.0,
            follow_redirects=True,
            headers={"User-Agent": "RMI-LabelImporter/1.0"},
        )
    return _http_client


async def _download_file(url: str, dest: str) -> bool:
    """Download a URL to dest path. Returns True on success."""
    try:
        client = await _get_http()
        resp = await client.get(url)
        if resp.status_code != 200:
            logger.warning(f"Download failed {url}: HTTP {resp.status_code}")
            return False
        os.makedirs(os.path.dirname(dest), exist_ok=True)
        with open(dest, "wb") as f:
            f.write(resp.content)
        logger.info(f"Downloaded {url} -> {dest} ({len(resp.content)} bytes)")
        return True
    except Exception as e:
        logger.warning(f"Download error {url}: {e}")
        return False


async def _download_json(url: str) -> Any | None:
    """Download and parse JSON from URL. Returns None on failure."""
    try:
        client = await _get_http()
        resp = await client.get(url)
        if resp.status_code != 200:
            logger.warning(f"JSON download failed {url}: HTTP {resp.status_code}")
            return None
        return resp.json()
    except Exception as e:
        logger.warning(f"JSON download error {url}: {e}")
        return None


# ── Solana labels ──────────────────────────────────────────────────────


async def _ensure_solana_labels() -> dict[str, int]:
    """Download Solana label CSVs if not present. Returns {filename: size}."""
    os.makedirs(LABELS_DIR, exist_ok=True)
    results = {}
    for filename, _, _, _ in SOLANA_CSV_SPECS:
        dest = os.path.join(LABELS_DIR, filename)
        if os.path.exists(dest) and os.path.getsize(dest) > 0:
            size = os.path.getsize(dest)
            logger.info(f"Solana labels already present: {filename} ({size} bytes)")
            results[filename] = size
        else:
            url = f"{SOLANA_LABELS_REPO}/{filename}"
            ok = await _download_file(url, dest)
            results[filename] = os.path.getsize(dest) if ok else 0
            if not ok:
                logger.warning(f"Failed to download {filename} — skipping")
    return results


async def _ensure_eth_scam_labels() -> dict[str, int]:
    """Download ETH phishing/scam CSVs if not present."""
    eth_scam_repo = (
        "https://raw.githubusercontent.com/ImMike/"
        "crypto-wallet-address-labels/main/datasets/ethereum-phishing-and-scam-labels"
    )
    os.makedirs(LABELS_DIR, exist_ok=True)
    results = {}
    for filename, _, _, _ in ETH_CSV_SPECS:
        dest = os.path.join(LABELS_DIR, filename)
        if os.path.exists(dest) and os.path.getsize(dest) > 0:
            size = os.path.getsize(dest)
            logger.info(f"ETH scam labels already present: {filename} ({size} bytes)")
            results[filename] = size
        else:
            url = f"{eth_scam_repo}/{filename}"
            ok = await _download_file(url, dest)
            results[filename] = os.path.getsize(dest) if ok else 0
            if not ok:
                logger.warning(f"Failed to download {filename} — skipping")
    return results


# ── OFAC sanctions ─────────────────────────────────────────────────────


async def _load_ofac_sanctions(storage) -> int:
    """
    Load OFAC crypto-sanctions-list into storage.
    Tries local file first, then downloads from GitHub.
    """
    # Try local cached copy first
    local_ofac = os.path.join(LABELS_DIR, "ofac_sanctions.json")
    sanctions = None

    if os.path.exists(local_ofac) and os.path.getsize(local_ofac) > 0:
        try:
            with open(local_ofac, encoding="utf-8") as f:
                sanctions = json.load(f)
            logger.info(f"Loaded OFAC from local cache ({len(sanctions)} entries)")
        except Exception as e:
            logger.warning(f"Local OFAC file corrupt: {e}")
            sanctions = None

    if sanctions is None:
        sanctions = await _download_json(OFAC_URL)
        if sanctions is None:
            logger.warning("OFAC sanctions download failed — trying fallback")
            # Fallback: try alternate URL formats
            alt_urls = [
                "https://raw.githubusercontent.com/OFAC/crypto-sanctions-list/master/crypto-sanctions-list.json",
                "https://raw.githubusercontent.com/OFAC/crypto-sanctions-list/main/crypto_sanctions_list.json",
            ]
            for alt in alt_urls:
                sanctions = await _download_json(alt)
                if sanctions is not None:
                    break

    if sanctions is None:
        logger.error("OFAC sanctions unavailable from all sources")
        return 0

    # Save local cache for next boot
    try:
        os.makedirs(LABELS_DIR, exist_ok=True)
        with open(local_ofac, "w", encoding="utf-8") as f:
            json.dump(sanctions, f)
    except Exception:
        pass

    # Store in Redis and ClickHouse
    count = 0
    ch_rows = []
    for entry in sanctions:
        addr = ""
        if isinstance(entry, dict):
            addr = entry.get("address", entry.get("Address", ""))
        if not addr or len(str(addr).strip()) < 10:
            continue

        addr_lower = str(addr).strip().lower()
        entry_json = json.dumps(entry)

        # Redis: rmi:label:ofac:{address}
        await storage.redis_set(f"rmi:label:ofac:{addr_lower}", entry_json)
        await storage.redis_sadd("rmi:labels:ofac", addr_lower)

        # ClickHouse row
        name = ""
        if isinstance(entry, dict):
            name = entry.get("name", entry.get("Name", entry.get("entity", "")))
        ch_rows.append(
            (
                addr_lower,
                "",
                str(name) if name else "OFAC Sanctioned",
                "sanctioned",
                "",
                "ofac",
                1,
                datetime.now(UTC),
            )
        )
        count += 1

    # Batch insert to ClickHouse
    if ch_rows and storage._ch:
        try:
            storage._ch.execute(
                """INSERT INTO wallet_labels
                (address, chain_id, label_name, label_category, label_subtype,
                 source, is_sanctioned, loaded_at) VALUES""",
                ch_rows,
            )
        except Exception as e:
            logger.warning(f"ClickHouse OFAC batch insert failed: {e}")

    await storage.redis_set("rmi:labels:ofac:count", str(count))
    logger.info(f"Loaded {count} OFAC sanctions")
    return count


# ── CSV loading ─────────────────────────────────────────────────────────


async def _load_csv_labels(
    storage,
    path: str,
    chain: str,
    addr_col: str,
    label_cols: dict[str, str],
    source_name: str = "",
) -> int:
    """
    Load a CSV of labeled addresses into Redis + ClickHouse via WalletStorage.
    Returns count of addresses loaded.
    """
    if not os.path.exists(path):
        logger.warning(f"CSV not found: {path}")
        return 0

    src = source_name or os.path.basename(path)

    # Skip if source already has labels in ClickHouse (prevents re-import)
    if storage._ch:
        try:
            existing = storage._ch.execute("SELECT count() FROM wallet_labels WHERE source = %(src)s", {"src": src})
            if existing and existing[0][0] > 100:
                logger.info(f"Source '{src}' already has {existing[0][0]:,} labels in CH — skipping re-import")
                return existing[0][0]
        except Exception:
            pass  # CH unavailable — import anyway for Redis

    count = 0
    ch_rows = []
    redis_pipe_ops = []  # Collect for batch Redis ops

    try:
        with open(path, newline="", encoding="utf-8") as f:
            reader = csv.DictReader(f)
            # Normalize headers to lowercase for case-insensitive matching
            if reader.fieldnames:
                reader.fieldnames = [h.lower().strip() for h in reader.fieldnames]
            for row in reader:
                addr = row.get(addr_col, "").strip().lower()
                if not addr or len(addr) < 10:
                    continue

                # Build label data dict
                label = {"chain": chain, "address": addr, "source": src}
                for key, col in label_cols.items():
                    val = row.get(col, "").strip()
                    if val:
                        label[key] = val

                # Also pick up any extra columns we recognise
                for extra in ("name", "label", "label_type", "label_subtype"):
                    if extra not in label:
                        val = row.get(extra, "").strip()
                        if val:
                            label[extra] = val

                label_json = json.dumps(label)

                # Redis: rmi:label:{chain}:{address} (matching wallet_label_loader format)
                redis_key = f"rmi:label:{chain}:{addr}"
                redis_pipe_ops.append((redis_key, label_json, addr))

                # ClickHouse row
                name = label.get(
                    "address_name",
                    label.get("dapp_name", label.get("protocol_name", label.get("name", ""))),
                )
                category = label.get("label_type", label.get("label", ""))
                subtype = label.get("label_subtype", "")
                ch_rows.append(
                    (
                        addr,
                        chain,
                        str(name) if name else "",
                        str(category) if category else "",
                        str(subtype) if subtype else "",
                        src,
                        0,
                        datetime.now(UTC),
                    )
                )

                count += 1

                # Flush in batches of 5000
                if count % 5000 == 0:
                    await _flush_batch(storage, redis_pipe_ops, ch_rows, chain)
                    redis_pipe_ops = []
                    ch_rows = []

    except Exception as e:
        logger.warning(f"Failed to load CSV {path}: {e}")

    # Flush remaining
    if redis_pipe_ops or ch_rows:
        await _flush_batch(storage, redis_pipe_ops, ch_rows, chain)

    logger.info(f"Loaded {count} labels from {src} (chain={chain})")
    return count


async def _flush_batch(
    storage,
    redis_ops: list[tuple[str, str, str]],
    ch_rows: list[tuple],
    chain: str,
):
    """Flush a batch of Redis writes and ClickHouse inserts."""
    # Batch Redis writes
    if storage._redis and redis_ops:
        try:
            async with storage._redis.pipeline() as pipe:
                for redis_key, label_json, addr in redis_ops:
                    pipe.set(redis_key, label_json)
                    pipe.sadd(f"rmi:labels:{chain}", addr)
                await pipe.execute()
        except Exception as e:
            # Fallback: individual writes
            logger.debug(f"Redis pipeline failed, falling back: {e}")
            for redis_key, label_json, addr in redis_ops:
                await storage.redis_set(redis_key, label_json)
                await storage.redis_sadd(f"rmi:labels:{chain}", addr)

    # Batch ClickHouse insert
    if ch_rows and storage._ch:
        try:
            storage._ch.execute(
                """INSERT INTO wallet_labels
                (address, chain_id, label_name, label_category, label_subtype,
                 source, is_sanctioned, loaded_at) VALUES""",
                ch_rows,
            )
        except Exception as e:
            logger.warning(f"ClickHouse batch insert failed: {e}")


# ── Etherscan combined labels ──────────────────────────────────────────


async def _load_etherscan_combined(storage) -> int:
    """
    Load etherscan_combined_labels.csv.
    Columns: address, name, label, chain, source
    """
    if not os.path.exists(ETHERSCAN_COMBINED):
        logger.warning(f"Etherscan combined labels not found: {ETHERSCAN_COMBINED}")
        return 0

    count = 0
    ch_rows = []
    redis_ops = []

    try:
        with open(ETHERSCAN_COMBINED, newline="", encoding="utf-8") as f:
            reader = csv.DictReader(f)
            for row in reader:
                addr = row.get("address", "").strip().lower()
                if not addr or len(addr) < 10:
                    continue

                chain_raw = row.get("chain", "ethereum").strip().lower()
                # Normalize chain names
                chain = _normalize_chain(chain_raw)

                name = row.get("name", "").strip()
                label = row.get("label", "").strip()
                row.get("source", "etherscan").strip()

                label_data = {
                    "chain": chain,
                    "address": addr,
                    "address_name": name,
                    "label_type": label,
                    "source": "etherscan_combined",
                }
                label_json = json.dumps(label_data)

                redis_key = f"rmi:label:{chain}:{addr}"
                redis_ops.append((redis_key, label_json, addr))

                ch_rows.append(
                    (
                        addr,
                        chain,
                        name,
                        label,
                        "",
                        "etherscan_combined",
                        0,
                        datetime.now(UTC),
                    )
                )

                count += 1

                if count % 5000 == 0:
                    await _flush_batch(storage, redis_ops, ch_rows, chain)
                    redis_ops = []
                    ch_rows = []

    except Exception as e:
        logger.warning(f"Failed to load etherscan combined: {e}")

    if redis_ops or ch_rows:
        await _flush_batch(storage, redis_ops, ch_rows, "ethereum")

    logger.info(f"Loaded {count} etherscan combined labels")
    return count


# ── Etherscan phishing/hack labels ─────────────────────────────────────


async def _load_etherscan_phish_hack(storage) -> int:
    """
    Load etherscan_phish_hack.csv.
    Columns: address, name_tag, label_type, chain, balance, txn_count, source
    """
    if not os.path.exists(ETHERSCAN_PHISH_HACK):
        logger.warning(f"Etherscan phish/hack not found: {ETHERSCAN_PHISH_HACK}")
        return 0

    count = 0
    ch_rows = []
    redis_ops = []

    try:
        with open(ETHERSCAN_PHISH_HACK, newline="", encoding="utf-8") as f:
            reader = csv.DictReader(f)
            for row in reader:
                addr = row.get("address", "").strip().lower()
                if not addr or len(addr) < 10:
                    continue

                chain_raw = row.get("chain", "ethereum").strip().lower()
                chain = _normalize_chain(chain_raw)

                name_tag = row.get("name_tag", "").strip()
                label_type = row.get("label_type", "").strip()
                balance = row.get("balance", "").strip()
                txn_count = row.get("txn_count", "").strip()

                label_data = {
                    "chain": chain,
                    "address": addr,
                    "address_name": name_tag,
                    "label_type": label_type,
                    "source": "etherscan_phish_hack",
                }
                if balance:
                    label_data["balance"] = balance
                if txn_count:
                    label_data["txn_count"] = txn_count

                label_json = json.dumps(label_data)
                redis_key = f"rmi:label:{chain}:{addr}"
                redis_ops.append((redis_key, label_json, addr))

                # These are phishing/hack labels — also store in scam_addresses
                is_scam = 1 if label_type.lower() in ("scam", "phishing", "hack") else 0

                ch_rows.append(
                    (
                        addr,
                        chain,
                        name_tag,
                        label_type,
                        "",
                        "etherscan_phish_hack",
                        is_scam,
                        datetime.now(UTC),
                    )
                )

                # Also add to the scam_addresses ClickHouse table
                if is_scam and storage._ch:
                    with contextlib.suppress(Exception):
                        storage._ch.execute(
                            """INSERT INTO scam_addresses
                            (address, chain_id, source, threat_type, confidence,
                             first_seen_at, evidence) VALUES""",
                            [
                                (
                                    addr,
                                    chain,
                                    "etherscan_phish_hack",
                                    label_type,
                                    0.95,
                                    datetime.now(UTC),
                                    json.dumps({"name_tag": name_tag, "balance": balance}),
                                )
                            ],
                        )

                count += 1

    except Exception as e:
        logger.warning(f"Failed to load etherscan phish/hack: {e}")

    if redis_ops or ch_rows:
        await _flush_batch(storage, redis_ops, ch_rows, "ethereum")

    logger.info(f"Loaded {count} etherscan phish/hack labels")
    return count


# ── BSC address tags ────────────────────────────────────────────────────


async def _ensure_bsc_labels() -> bool:
    """Download BSC detail CSV if not present."""
    dest = os.path.join(LABELS_DIR, "2024-01-31-detail.csv")
    if os.path.exists(dest) and os.path.getsize(dest) > 0:
        logger.info(f"BSC detail labels already present ({os.path.getsize(dest)} bytes)")
        return True
    url = f"{BSC_LABELS_REPO}/2024-01-31-detail.csv"
    return await _download_file(url, dest)


async def _load_bsc_detail(storage) -> int:
    """Load BSC address tag detail CSV. 15K BSC addresses with entity labels."""
    path = os.path.join(LABELS_DIR, "2024-01-31-detail.csv")
    if not os.path.exists(path):
        logger.warning(f"BSC detail CSV not found: {path}")
        return 0

    count = 0
    ch_rows = []
    redis_ops = []

    try:
        with open(path, newline="", encoding="utf-8") as f:
            reader = csv.DictReader(f)
            for row in reader:
                addr = row.get("address", "").strip().lower()
                if not addr or len(addr) < 10:
                    continue

                label_name = row.get("label_name", "").strip()
                name_tag = row.get("name_tag", "").strip()
                addr_type = row.get("address_type", "").strip()
                chain_code = row.get("chain_code", "BSC").strip().lower()
                chain = _normalize_chain(chain_code)

                category = (
                    "exchange"
                    if "exchange" in (label_name + name_tag).lower()
                    else "defi"
                    if any(w in (label_name + name_tag).lower() for w in ("protocol", "swap", "defi", "lend"))
                    else "bridge"
                    if "bridge" in (label_name + name_tag).lower()
                    else "wallet"
                )

                label_data = {
                    "chain": chain,
                    "address": addr,
                    "address_name": label_name,
                    "label_type": name_tag or label_name,
                    "label_subtype": addr_type,
                    "source": "bsc_detail",
                }
                label_json = json.dumps(label_data)

                redis_key = f"rmi:label:{chain}:{addr}"
                redis_ops.append((redis_key, label_json, addr))

                ch_rows.append(
                    (
                        addr,
                        chain,
                        name_tag or label_name,
                        category,
                        addr_type,
                        "bsc_detail",
                        0,
                        datetime.now(UTC),
                    )
                )

                count += 1
                if count % 5000 == 0:
                    await _flush_batch(storage, redis_ops, ch_rows, chain)
                    redis_ops = []
                    ch_rows = []

    except Exception as e:
        logger.warning(f"Failed to load BSC detail: {e}")

    if redis_ops or ch_rows:
        await _flush_batch(storage, redis_ops, ch_rows, "bsc")

    logger.info(f"Loaded {count} BSC detail labels")
    return count


# ── Exchange cluster labels ─────────────────────────────────────────────


async def _ensure_exchange_cluster_labels() -> bool:
    """Download exchange cluster CSV."""
    dest = os.path.join(LABELS_DIR, "exchanges_cluster.csv")
    if os.path.exists(dest) and os.path.getsize(dest) > 0:
        return True
    url = f"{ETH_CLUSTER_LABELS_REPO}/exchanges.csv"
    return await _download_file(url, dest)


async def _load_exchange_clusters(storage) -> int:
    """Load exchange cluster entity labels. ~200 exchange addresses."""
    path = os.path.join(LABELS_DIR, "exchanges_cluster.csv")
    # Also support direct-fetched path
    if not os.path.exists(path):
        path = os.path.join(LABELS_DIR, "exchanges.csv")
    if not os.path.exists(path):
        logger.warning("Exchange cluster CSV not found")
        return 0

    count = 0
    ch_rows = []
    redis_ops = []

    try:
        with open(path, newline="", encoding="utf-8") as f:
            reader = csv.DictReader(f)
            for row in reader:
                addr = row.get("address", "").strip().lower()
                if not addr or len(addr) < 10:
                    continue

                name = row.get("name", "").strip()
                acct_type = row.get("accountType", "").strip()
                entity_type = row.get("type", "").strip()
                chain = "ethereum"

                label_data = {
                    "chain": chain,
                    "address": addr,
                    "address_name": name,
                    "label_type": entity_type,
                    "label_subtype": acct_type,
                    "source": "exchange_cluster",
                }
                label_json = json.dumps(label_data)

                redis_key = f"rmi:label:{chain}:{addr}"
                redis_ops.append((redis_key, label_json, addr))

                ch_rows.append(
                    (
                        addr,
                        chain,
                        name,
                        entity_type.lower(),
                        acct_type,
                        "exchange_cluster",
                        0,
                        datetime.now(UTC),
                    )
                )

                count += 1

    except Exception as e:
        logger.warning(f"Failed to load exchange clusters: {e}")

    if redis_ops or ch_rows:
        await _flush_batch(storage, redis_ops, ch_rows, "ethereum")

    logger.info(f"Loaded {count} exchange cluster labels")
    return count


# ── Contract name labels (name_dict.txt) ────────────────────────────────


async def _ensure_contract_name_labels() -> bool:
    """Download contract name dict."""
    dest = os.path.join(LABELS_DIR, "name_dict.txt")
    if os.path.exists(dest) and os.path.getsize(dest) > 0:
        return True
    url = f"{ETH_CONTRACT_LABELS_REPO}/name_dict.txt"
    return await _download_file(url, dest)


async def _load_contract_names(storage) -> int:
    """Load contract name labels from name_dict.txt. ~900 Ethereum contracts."""
    path = os.path.join(LABELS_DIR, "name_dict.txt")
    if not os.path.exists(path):
        logger.warning("Contract name dict not found")
        return 0

    count = 0
    ch_rows = []
    redis_ops = []

    try:
        with open(path, encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                # Format: "0xADDRESS - Name"
                parts = line.split(" - ", 1)
                if len(parts) < 2:
                    continue

                addr = parts[0].strip().lower()
                name = parts[1].strip()

                if not addr.startswith("0x") or len(addr) < 10:
                    continue

                chain = "ethereum"

                label_data = {
                    "chain": chain,
                    "address": addr,
                    "address_name": name,
                    "label_type": "contract",
                    "source": "contract_name_labels",
                }
                label_json = json.dumps(label_data)

                redis_key = f"rmi:label:{chain}:{addr}"
                redis_ops.append((redis_key, label_json, addr))

                ch_rows.append(
                    (
                        addr,
                        chain,
                        name,
                        "contract",
                        "",
                        "contract_name_labels",
                        0,
                        datetime.now(UTC),
                    )
                )

                count += 1

    except Exception as e:
        logger.warning(f"Failed to load contract names: {e}")

    if redis_ops or ch_rows:
        await _flush_batch(storage, redis_ops, ch_rows, "ethereum")

    logger.info(f"Loaded {count} contract name labels")
    return count


# ── Exchange/DEX wallet labels ──────────────────────────────────────────

_EXCHANGE_WALLET_FILES = [
    ("exchange.csv", "exchange_wallets", "ethereum"),
    ("dex.csv", "dex_wallets", "ethereum"),
    ("icowallets.csv", "ico_wallets", "ethereum"),
    ("mining1.csv", "mining_wallets_part1", "ethereum"),
    ("mining2.csv", "mining_wallets_part2", "ethereum"),
    ("walletapp.csv", "walletapp_wallets", "ethereum"),
]


async def _ensure_exchange_wallet_labels() -> dict[str, int]:
    """Download all exchange/DeFi wallet label CSVs."""
    os.makedirs(LABELS_DIR, exist_ok=True)
    results = {}
    for filename, _, _ in _EXCHANGE_WALLET_FILES:
        dest = os.path.join(LABELS_DIR, filename)
        if os.path.exists(dest) and os.path.getsize(dest) > 0:
            results[filename] = os.path.getsize(dest)
        else:
            url = f"{ETH_EXCHANGE_LABELS_REPO}/{filename}"
            ok = await _download_file(url, dest)
            results[filename] = os.path.getsize(dest) if ok else 0
    return results


async def _load_exchange_wallets(storage) -> int:
    """Load exchange/dex/ico/mining wallet labels. ~350 labeled wallets."""
    # Files that have NO headers (address,entity,balance,txcount format)
    HEADERLESS_FILES = {"exchange.csv", "mining1.csv", "mining2.csv"}

    total = 0
    for filename, _, chain in _EXCHANGE_WALLET_FILES:
        path = os.path.join(LABELS_DIR, filename)
        if not os.path.exists(path):
            logger.warning(f"Exchange wallet CSV not found: {filename}")
            continue

        count = 0
        ch_rows = []
        redis_ops = []

        try:
            if filename in HEADERLESS_FILES:
                # Headerless CSV: address,entity,balance,txcount
                with open(path, encoding="utf-8-sig") as f:
                    reader = csv.reader(f)
                    for row in reader:
                        if len(row) < 2:
                            continue
                        addr = row[0].strip().lower()
                        entity = row[1].strip() if len(row) > 1 else ""
                        balance = row[2].strip().strip('"') if len(row) > 2 else ""
                        txcount = row[3].strip().strip('"') if len(row) > 3 else ""

                        if not addr or len(addr) < 10:
                            continue

                        # Infer category
                        cat = "exchange"
                        if "mining" in filename:
                            cat = "mining"

                        label_data = {
                            "chain": chain,
                            "address": addr,
                            "address_name": entity,
                            "label_type": cat,
                            "source": "exchange_wallets",
                        }
                        if balance:
                            label_data["balance"] = balance
                        if txcount:
                            label_data["txn_count"] = txcount

                        label_json = json.dumps(label_data)
                        redis_key = f"rmi:label:{chain}:{addr}"
                        redis_ops.append((redis_key, label_json, addr))
                        ch_rows.append(
                            (
                                addr,
                                chain,
                                entity,
                                cat,
                                "",
                                "exchange_wallets",
                                0,
                                datetime.now(UTC),
                            )
                        )
                        count += 1
            else:
                # Standard CSVs with headers
                with open(path, newline="", encoding="utf-8-sig") as f:
                    reader = csv.DictReader(f)
                    for row in reader:
                        addr = row.get("address", "") or row.get("from_address", "")
                        addr = addr.strip().lower()
                        if not addr or len(addr) < 10:
                            continue

                        entity = row.get("entity", row.get("name", "")).strip()
                        balance = row.get("balance", "")
                        txcount = row.get("txcount", row.get("txn_count", ""))

                        cat = "exchange"
                        if "dex" in filename:
                            cat = "defi"
                        elif "ico" in filename:
                            cat = "ico"
                        elif "walletapp" in filename:
                            cat = "wallet"

                        label_data = {
                            "chain": chain,
                            "address": addr,
                            "address_name": entity,
                            "label_type": cat,
                            "source": "exchange_wallets",
                        }
                        if balance:
                            label_data["balance"] = balance
                        if txcount:
                            label_data["txn_count"] = txcount

                        label_json = json.dumps(label_data)
                        redis_key = f"rmi:label:{chain}:{addr}"
                        redis_ops.append((redis_key, label_json, addr))
                        ch_rows.append(
                            (
                                addr,
                                chain,
                                entity,
                                cat,
                                "",
                                "exchange_wallets",
                                0,
                                datetime.now(UTC),
                            )
                        )
                        count += 1

        except Exception as e:
            logger.warning(f"Failed to load {filename}: {e}")
            continue

        if redis_ops or ch_rows:
            await _flush_batch(storage, redis_ops, ch_rows, chain)

        logger.info(f"Loaded {count} labels from {filename}")
        total += count

    logger.info(f"Loaded {total} total exchange wallet labels")
    return total


# ── Chain normalization ─────────────────────────────────────────────────

_CHAIN_ALIASES = {
    "eth": "ethereum",
    "eth_mainnet": "ethereum",
    "bsc": "bsc",
    "bnb": "bsc",
    "bnb_chain": "bsc",
    "polygon": "polygon",
    "matic": "polygon",
    "arbitrum": "arbitrum",
    "arb": "arbitrum",
    "optimism": "optimism",
    "op": "optimism",
    "avalanche": "avalanche",
    "avax": "avalanche",
    "fantom": "fantom",
    "ftm": "fantom",
    "base": "base",
    "sol": "solana",
    "solana": "solana",
}


def _normalize_chain(chain: str) -> str:
    """Normalize chain name to canonical form."""
    return _CHAIN_ALIASES.get(chain.lower().strip(), chain.lower().strip()) or "ethereum"


# ── ClickHouse schema ───────────────────────────────────────────────────


async def _ensure_wallet_labels_table(storage):
    """Create the wallet_labels table in ClickHouse if it doesn't exist."""
    if not storage._ch:
        return
    try:
        storage._ch.execute("CREATE DATABASE IF NOT EXISTS wallet_memory")
        storage._ch.execute(WALLET_LABELS_DDL)
        logger.info("wallet_labels table ensured in ClickHouse")
    except Exception as e:
        logger.warning(f"Failed to create wallet_labels table: {e}")


# ── Main entry point ────────────────────────────────────────────────────

_loading = False
_last_load_counts: dict[str, int] = {}


async def load_all_static_labels() -> dict[str, int]:
    """
    Load all static wallet label datasets on boot.

    Sources:
      1. Solana CEX labels (crypto-wallet-address-labels GitHub)
      2. Solana DApp labels
      3. Solana DeFi labels
      4. Etherscan combined labels (50K rows)
      5. Etherscan phishing/hack labels (111 rows)
      6. OFAC crypto sanctions
      7. BSC address tags (15K rows)
      8. Exchange cluster labels (~200)
      9. Contract name labels (~900)
     10. Exchange/DEX/ICO/Mining wallet labels (~350)

    Returns counts per source.
    """
    global _loading, _last_load_counts

    if _loading:
        logger.info("Label import already in progress — skipping")
        return _last_load_counts

    _loading = True
    start = time.time()
    counts: dict[str, int] = {}

    try:
        from .storage import get_storage

        storage = get_storage()

        # Ensure connections
        await storage._ensure_connections()
        logger.info(
            f"Storage connections: redis={'ok' if storage._redis else 'no'}, clickhouse={'ok' if storage._ch else 'no'}"
        )

        # Ensure ClickHouse table exists
        await _ensure_wallet_labels_table(storage)

        # 1. Download Solana label CSVs if needed
        await _ensure_solana_labels()

        # 2. Load Solana CSVs
        for filename, chain, addr_col, label_cols in SOLANA_CSV_SPECS:
            path = os.path.join(LABELS_DIR, filename)
            key = filename.replace(".csv", "").replace("_labels", "")
            if os.path.exists(path):
                c = await _load_csv_labels(storage, path, chain, addr_col, label_cols, source_name=key)
                counts[key] = c
            else:
                counts[key] = 0
                logger.warning(f"Solana labels missing: {filename}")

        # 2b. Load additional ETH phishing/scam CSVs from wallet-labels dir
        for filename, chain, addr_col, label_cols in ETH_CSV_SPECS:
            path = os.path.join(LABELS_DIR, filename)
            key = filename.replace(".csv", "").replace("_labels", "")
            if os.path.exists(path):
                c = await _load_csv_labels(storage, path, chain, addr_col, label_cols, source_name=key)
                counts[key] = c
            else:
                counts[key] = 0

        # 3. Load Etherscan combined labels
        counts["etherscan_combined"] = await _load_etherscan_combined(storage)

        # 4. Load Etherscan phishing/hack labels
        counts["etherscan_phish_hack"] = await _load_etherscan_phish_hack(storage)

        # 5. Load OFAC sanctions
        counts["ofac"] = await _load_ofac_sanctions(storage)

        # === NEW SOURCES ===

        # 6. Download and load BSC address tags
        await _ensure_bsc_labels()
        counts["bsc_detail"] = await _load_bsc_detail(storage)

        # 7. Download and load exchange cluster labels
        await _ensure_exchange_cluster_labels()
        counts["exchange_cluster"] = await _load_exchange_clusters(storage)

        # 8. Download and load contract name labels
        await _ensure_contract_name_labels()
        counts["contract_names"] = await _load_contract_names(storage)

        # 9. Download and load exchange/DEX wallet labels
        await _ensure_exchange_wallet_labels()
        counts["exchange_wallets"] = await _load_exchange_wallets(storage)

        # Store aggregate counts in Redis
        total = sum(counts.values())
        counts["total"] = total
        await storage.redis_set("rmi:labels:counts", json.dumps(counts))

        elapsed = time.time() - start
        logger.info(f"Static label import complete: {total} labels in {elapsed:.1f}s — {counts}")

    except Exception as e:
        logger.error(f"Static label import failed: {e}", exc_info=True)
        counts["error"] = str(e)
    finally:
        _loading = False

    _last_load_counts = counts
    return counts


def get_last_load_counts() -> dict[str, int]:
    """Return counts from the most recent label import."""
    return _last_load_counts


def is_loading() -> bool:
    """Check if a label import is currently in progress."""
    return _loading


# ── Clean Data Loader (from label quality fixer) ─────────────────────

CLEAN_DIR = os.path.join(DATA_DIR, "wallet-labels-clean")


async def load_clean_labels(storage) -> dict:
    """Load pre-cleaned, deduplicated labels from the quality fixer.
    Returns stats dict with counts."""
    import csv

    stats = {"ethereum": 0, "solana": 0, "errors": 0}

    for chain in ["ethereum", "solana"]:
        path = os.path.join(CLEAN_DIR, f"wallet_labels_{chain}.csv")
        if not os.path.exists(path):
            logger.warning(f"Clean labels file not found: {path}")
            continue

        labels = []
        with open(path) as f:
            reader = csv.DictReader(f)
            for row in reader:
                labels.append(
                    {
                        "address": row["address"],
                        "chain": row["chain"],
                        "source": row.get("source", "clean_import"),
                        "name": row.get("name", ""),
                        "label_type": row.get("label_type", ""),
                        "label_subtype": row.get("label_subtype", ""),
                        "project": row.get("project", ""),
                        "entity_type": row.get("entity_type", "unknown"),
                    }
                )

        if labels and storage:
            try:
                await storage.bulk_import(labels, source=f"clean_{chain}")
                stats[chain] = len(labels)
                logger.info(f"Loaded {len(labels):,} clean {chain} labels")
            except Exception as e:
                logger.error(f"Failed to import clean {chain} labels: {e}")
                stats["errors"] += 1

    logger.info(f"Clean label import complete: {stats}")
    return stats
