#!/usr/bin/env python3
"""
BUNDLE & CLUSTER RAG INTEGRATION
=================================
Marries graph-based detection with semantic intelligence.

What RAG adds to bundle/cluster detection:
  1. BEHAVIORAL EMBEDDING — Convert cluster behavior to vectors, store in pgvector
  2. SEMANTIC LABELING — Auto-label clusters ("insider ring", "MEV bot farm", "sybil attack")
  3. SIMILARITY SEARCH — "Find clusters that look like this known scammer group"
  4. CROSS-CHAIN IDENTITY — Match behavioral fingerprints across chains
  5. EVIDENCE CHAIN — Link clusters to known scam patterns, forensic reports
  6. NL QUERYING — "Show me all wash trading clusters from the last week"

Flow:
  BundleDetector → finds bundles → embed bundle profile → store in RAG
  ClusterDetector → finds clusters → embed cluster behavior → semantic label → store
  User queries → embed query → ANN search → return labeled clusters with evidence
"""

import hashlib
import json
import logging
from typing import Any

import numpy as np

logger = logging.getLogger(__name__)


# ══════════════════════════════════════════════════════════════════════
# BUNDLE BEHAVIORAL EMBEDDER
# ══════════════════════════════════════════════════════════════════════


def embed_bundle_profile(bundle: dict[str, Any]) -> list[float]:
    """
    Convert a bundle detection result into a behavioral vector.
    Captures: timing patterns, wallet distribution, funding structure, concentration.

    Returns 128-dim vector that can be compared across bundles.
    """
    vec = np.zeros(128, dtype=np.float32)

    # ── Timing signals (dims 0-15) ──
    vec[0] = min(float(bundle.get("confidence", 0)), 1.0)
    vec[1] = min(float(bundle.get("atomic_block_score", 0)), 1.0)
    vec[2] = min(float(bundle.get("common_funder_score", 0)), 1.0)
    vec[3] = min(float(bundle.get("temporal_score", 0)), 1.0)
    vec[4] = min(float(bundle.get("distribution_anomaly_score", 0)), 1.0)
    vec[5] = min(float(bundle.get("concentration_score", 0)), 1.0)

    # ── Scale signals (dims 6-15) ──
    wallets = bundle.get("wallets_in_earliest_block", 0)
    vec[6] = min(float(wallets) / 100.0, 1.0)
    vec[7] = min(float(bundle.get("total_bundle_wallets", wallets)) / 100.0, 1.0)

    # Funding structure
    funders = bundle.get("unique_funders", 0)
    vec[8] = 1.0 / max(1.0, float(funders))  # fewer funders = more suspicious
    vec[9] = 1.0 if bundle.get("common_funder_address") else 0.0

    # Distribution
    top3_pct = float(bundle.get("top3_holder_percent", 0))
    vec[10] = min(top3_pct / 100.0, 1.0)
    top10_pct = float(bundle.get("top10_holder_percent", 0))
    vec[11] = min(top10_pct / 100.0, 1.0)

    # Temporal
    block_span = float(bundle.get("block_span", 1))
    vec[12] = min(1.0 / max(1.0, block_span), 1.0)  # narrower span = more bundled
    vec[13] = 1.0 if bundle.get("earliest_block", 0) == bundle.get("launch_block", 0) else 0.0  # block-0 bundle

    # ── Behavior fingerprint (dims 16-31) ──
    behaviors = bundle.get("behaviors", [])
    behavior_tags = [
        "coordinated_buy",
        "staggered_entry",
        "same_amount",
        "round_numbers",
        "gas_price_clustering",
        "mev_used",
        "jito_tip_paid",
        "flashbots_used",
        "same_dex_route",
        "same_slippage",
        "reverted_txns",
        "sandwich_pattern",
        "pump_then_dump",
        "slow_accumulation",
        "wash_trade",
        "sybil_pattern",
    ]
    for i, tag in enumerate(behavior_tags):
        if tag in behaviors:
            vec[16 + i] = 1.0

    # ── Entity hash of key addresses (dims 32-47) ──
    key_addrs = str(bundle.get("common_funder_address", ""))
    key_addrs += str(bundle.get("token_address", ""))
    key_addrs += ",".join(sorted(bundle.get("bundle_wallets", [])[:10]))
    addr_hash = hashlib.md5(key_addrs.encode()).digest()
    for i in range(16):
        vec[32 + i] = addr_hash[i] / 255.0

    # ── Chain/context (dims 48-63) ──
    chain = bundle.get("chain", "solana").lower()
    chain_list = [
        "solana",
        "ethereum",
        "base",
        "bsc",
        "arbitrum",
        "polygon",
        "optimism",
        "avalanche",
        "fantom",
        "tron",
        "sui",
        "aptos",
        "near",
        "injective",
        "sei",
        "blast",
    ]
    for i, ch in enumerate(chain_list):
        if ch in chain:
            vec[48 + i] = 1.0

    # ── Risk classification hash (dims 64-79) ──
    risk_tags = bundle.get("risk_tags", [])
    risk_names = [
        "scam",
        "rug",
        "honeypot",
        "wash_trade",
        "insider",
        "bot_farm",
        "sybil",
        "sandwich_bot",
        "mev_bot",
        "market_maker",
        "whale",
        "exchange",
        "vault",
        "bridge",
        "mixer",
        "unknown",
    ]
    for i, tag in enumerate(risk_names):
        if tag in risk_tags or tag in str(bundle.get("classification", "")):
            vec[64 + i] = 1.0

    # ── Numerical fingerprint (dims 80-127) ──
    # Encode key metrics as normalized values (dims 80-85)
    metrics = [
        ("avg_buy_amount", 10000),
        ("max_buy_amount", 100000),
        ("avg_hold_time_blocks", 1000),
        ("sell_ratio", 1.0),
        ("profit_ratio", 10.0),
        ("gas_spent_eth", 1.0),
    ]
    for i, (key, scale) in enumerate(metrics):
        val = float(bundle.get(key, 0) or 0)
        vec[80 + i] = min(val / max(1, scale), 1.0)

    # Structural hash of the full bundle data (dims 86-127)
    full_hash = hashlib.sha256(json.dumps(bundle, sort_keys=True, default=str).encode()).digest()
    for i in range(42):
        vec[86 + i] = full_hash[i % 32] / 255.0

    return vec.tolist()


# ══════════════════════════════════════════════════════════════════════
# CLUSTER BEHAVIORAL EMBEDDER
# ══════════════════════════════════════════════════════════════════════


def embed_cluster_profile(cluster: dict[str, Any]) -> list[float]:
    """
    Convert a wallet cluster into a 192-dim behavioral vector.
    Captures: size, density, activity patterns, token overlap, temporal cohesion.

    This allows: "find clusters similar to this rug pull ring"
    """
    vec = np.zeros(192, dtype=np.float32)

    # ── Size & density (dims 0-19) ──
    size = int(cluster.get("size", cluster.get("wallet_count", 1)))
    vec[0] = min(np.log1p(size) / 10.0, 1.0)
    vec[1] = min(float(size) / 1000.0, 1.0)

    density = float(cluster.get("density", cluster.get("edge_density", 0)))
    vec[2] = min(density, 1.0)

    vec[3] = min(float(cluster.get("avg_degree", 0)) / 100.0, 1.0)
    vec[4] = min(float(cluster.get("diameter", 1)) / 10.0, 1.0)

    # ── Activity patterns (dims 10-29) ──
    age_days = float(cluster.get("age_days", 1))
    vec[10] = min(age_days / 365.0, 1.0)

    txn_count = float(cluster.get("total_transactions", 0))
    vec[11] = min(np.log1p(txn_count) / 15.0, 1.0)

    volume = float(cluster.get("total_volume_usd", 0))
    vec[12] = min(np.log1p(volume) / 20.0, 1.0)

    vec[13] = min(float(cluster.get("txn_frequency_per_day", 0)) / 100.0, 1.0)

    # Burstiness
    vec[14] = min(float(cluster.get("burst_score", 0)), 1.0)
    vec[15] = min(float(cluster.get("peak_activity_ratio", 0)), 1.0)

    # Sleep/active patterns
    vec[16] = 1.0 if cluster.get("sleeper_cluster") else 0.0
    vec[17] = min(float(cluster.get("dormant_period_days", 0)) / 365.0, 1.0)

    # ── Token overlap (dims 20-39) ──
    tokens = cluster.get("common_tokens", [])
    vec[20] = min(len(tokens) / 500.0, 1.0)
    vec[21] = min(float(cluster.get("token_overlap_ratio", 0)), 1.0)

    token_categories = cluster.get("token_categories", [])
    cat_tags = [
        "memecoin",
        "defi",
        "nft",
        "gaming",
        "stablecoin",
        "wrapped",
        "bridge",
        "oracle",
        "governance",
        "mev",
    ]
    for i, cat in enumerate(cat_tags):
        if cat in token_categories:
            vec[22 + i] = 1.0

    # ── Behavior classification (dims 30-49) ──
    signals = cluster.get("signals", cluster.get("behavior_signals", []))
    signal_tags = [
        "coordinated_trading",
        "pump_and_dump",
        "wash_trading",
        "insider_trading",
        "front_running",
        "sandwich_attacks",
        "arbitrage",
        "liquidation_cascade",
        "flash_loan_pattern",
        "mixer_usage",
        "tornado_cash",
        "cex_deposit_pattern",
        "dex_only",
        "nft_wash",
        "airdrop_farming",
        "sybil_attack",
        "bot_activity",
        "mev_extraction",
        "cross_chain_bridge",
        "stablecoin_only",
    ]
    for i, tag in enumerate(signal_tags):
        if tag in signals or tag in str(cluster.get("classification", "")):
            vec[30 + i] = 1.0

    # ── Cross-chain signals (dims 50-59) ──
    chains = cluster.get("active_chains", [])
    chain_list = [
        "ethereum",
        "solana",
        "base",
        "bsc",
        "arbitrum",
        "polygon",
        "optimism",
        "avalanche",
        "fantom",
        "tron",
    ]
    for i, ch in enumerate(chain_list):
        if ch in [c.lower() for c in chains]:
            vec[50 + i] = 1.0

    # ── Entity fingerprint (dims 60-79) ──
    entity_id = str(cluster.get("entity_id", ""))
    if entity_id:
        eh = hashlib.md5(entity_id.encode()).digest()
        for i in range(16):
            vec[60 + i] = eh[i] / 255.0

    # ── Risk scoring (dims 80-89) ──
    vec[80] = min(float(cluster.get("scam_probability", 0)), 1.0)
    vec[81] = min(float(cluster.get("rug_probability", 0)), 1.0)
    vec[82] = min(float(cluster.get("honeypot_probability", 0)), 1.0)
    vec[83] = min(float(cluster.get("wash_trade_probability", 0)), 1.0)
    vec[84] = min(float(cluster.get("insider_probability", 0)), 1.0)
    vec[85] = min(float(cluster.get("bot_probability", 0)), 1.0)

    # ── Hash fingerprint (dims 90-191) ──
    ch = hashlib.sha256(json.dumps(cluster, sort_keys=True, default=str).encode()).digest()
    for i in range(102):
        vec[90 + i] = ch[i % 32] / 255.0

    return vec.tolist()


# ══════════════════════════════════════════════════════════════════════
# SEMANTIC LABELING
# ══════════════════════════════════════════════════════════════════════

CLUSTER_LABEL_TEMPLATES = [
    {
        "label": "insider_trading_ring",
        "description": "Cluster of wallets consistently buying before major announcements or listings, then selling into the pump.",
        "signals": ["coordinated_trading", "pump_and_dump", "insider_trading", "pre_listing_buys"],
        "severity": "high",
        "examples": "TRB insider ring, Binance listing front-runners",
    },
    {
        "label": "wash_trading_farm",
        "description": "Group of wallets trading the same tokens back and forth to simulate volume and attract real traders.",
        "signals": [
            "wash_trading",
            "circular_transfers",
            "same_amount_trades",
            "no_net_position_change",
        ],
        "severity": "high",
        "examples": "NFT wash trading rings, DEX volume inflation farms",
    },
    {
        "label": "sybil_attack_farm",
        "description": "Thousands of wallets controlled by one entity to manipulate voting, airdrops, or metrics.",
        "signals": [
            "sybil_attack",
            "airdrop_farming",
            "one_to_many_funding",
            "no_organic_activity",
        ],
        "severity": "high",
        "examples": "Hop Protocol sybils, Arbitrum airdrop farmers",
    },
    {
        "label": "mev_bot_network",
        "description": "Coordinated MEV bots running sandwich attacks, arbitrage, and liquidations.",
        "signals": [
            "mev_extraction",
            "sandwich_attacks",
            "arbitrage",
            "bot_activity",
            "flashbots_used",
        ],
        "severity": "medium",
        "examples": "jaredfromsubway.eth network, Banana Gun bot wallets",
    },
    {
        "label": "bundle_launch_ring",
        "description": "Creator uses 10-50 wallets to buy at launch (block 0), then dumps on retail.",
        "signals": ["coordinated_buy", "block_zero_bundle", "same_funder", "distributed_dump"],
        "severity": "critical",
        "examples": "Pump.fun bundle launches, sniper bot farms",
    },
    {
        "label": "liquidity_drain_cartel",
        "description": "Multiple wallets that sequentially drain liquidity from tokens after hype phase.",
        "signals": ["liquidity_removal", "multi_token_pattern", "sequential_rug", "same_deployer"],
        "severity": "critical",
        "examples": "Compounder finance drainers, sequential rug rings",
    },
    {
        "label": "market_maker_cluster",
        "description": "Legitimate market making operation — multiple wallets providing liquidity across DEXes.",
        "signals": ["market_maker", "arbitrage", "dex_only", "high_volume", "low_profit_margin"],
        "severity": "low",
        "examples": "Wintermute, Jump Trading, GSR wallet clusters",
    },
    {
        "label": "exchange_hot_wallet_ring",
        "description": "Cluster of wallets belonging to a centralized exchange's hot wallet system.",
        "signals": ["cex_deposit_pattern", "high_volume", "many_counterparties", "exchange"],
        "severity": "low",
        "examples": "Binance hot wallets, Coinbase deposit addresses",
    },
    {
        "label": "bridge_exploiter_ring",
        "description": "Wallets involved in cross-chain bridge exploits, often funded by the same mixer.",
        "signals": ["cross_chain_bridge", "mixer_usage", "tornado_cash", "one_time_use"],
        "severity": "critical",
        "examples": "Wormhole exploiter, Ronin bridge attacker wallets",
    },
    {
        "label": "nft_insider_mint_ring",
        "description": "Group minting rare NFTs before public reveal using insider knowledge of rarity.",
        "signals": ["nft_wash", "insider_trading", "pre_reveal_mints", "rarity_sniping"],
        "severity": "high",
        "examples": "OpenSea insider trading, Blur farmer rings",
    },
]


async def auto_label_cluster(
    cluster: dict[str, Any],
    cluster_vector: list[float],
) -> dict[str, Any]:
    """
    Auto-label a cluster by comparing its behavioral vector to known templates.
    Uses cosine similarity between cluster behavior and template descriptions.
    """
    from app.crypto_embeddings import get_embedder

    embedder = await get_embedder()
    signals = cluster.get("signals", cluster.get("behavior_signals", []))

    # Build semantic description of the cluster
    cluster_desc = f"""Wallet cluster with {cluster.get("size", "?")} wallets.
Age: {cluster.get("age_days", "?")} days.
Volume: ${cluster.get("total_volume_usd", 0):,.0f}.
Transactions: {cluster.get("total_transactions", 0)}.
Signals: {", ".join(signals[:10])}.
Active chains: {", ".join(cluster.get("active_chains", ["unknown"]))}.
Common tokens: {", ".join(cluster.get("common_tokens", [])[:5])}."""

    # Embed the cluster description
    try:
        cluster_semantic = await embedder._semantic_embed_one(cluster_desc, "semantic")
    except Exception:
        cluster_semantic = embedder._hash_embed(cluster_desc)

    # Compare to each label template
    matches = []
    for template in CLUSTER_LABEL_TEMPLATES:
        # Template description embedding
        template_text = f"{template['label']}: {template['description']} Examples: {template['examples']}"
        try:
            template_semantic = await embedder._semantic_embed_one(template_text, "semantic")
        except Exception:
            template_semantic = embedder._hash_embed(template_text)

        # Semantic similarity
        sem_sim = embedder.cosine_similarity(
            cluster_semantic[: min(len(cluster_semantic), len(template_semantic))],
            template_semantic[: min(len(cluster_semantic), len(template_semantic))],
        )

        # Signal overlap bonus
        signal_overlap = len(set(signals) & set(template["signals"]))
        signal_bonus = min(signal_overlap / max(1, len(template["signals"])), 0.3)

        combined = sem_sim + signal_bonus

        if combined > 0.4:
            matches.append(
                {
                    "label": template["label"],
                    "description": template["description"],
                    "severity": template["severity"],
                    "confidence": round(min(combined, 0.99), 4),
                    "semantic_sim": round(sem_sim, 4),
                    "signal_overlap": signal_overlap,
                }
            )

    matches.sort(key=lambda x: x["confidence"], reverse=True)

    return {
        "top_label": matches[0]["label"] if matches else "unknown",
        "top_confidence": matches[0]["confidence"] if matches else 0.0,
        "all_labels": matches[:3],
        "cluster_size": cluster.get("size", 0),
    }


# ══════════════════════════════════════════════════════════════════════
# CLUSTER SIMILARITY SEARCH
# ══════════════════════════════════════════════════════════════════════


async def find_similar_clusters(
    target_cluster: dict[str, Any],
    min_similarity: float = 0.6,
    limit: int = 10,
) -> list[dict[str, Any]]:
    """
    Find clusters similar to a target cluster using behavioral vector similarity.
    "This cluster looks like the Wintermute cluster from March"
    """
    from app.supabase_vector import get_vector_store

    # Embed the target cluster
    target_vec = embed_cluster_profile(target_cluster)
    len(target_vec)

    # Search in pgvector
    store = await get_vector_store()
    results = await store.search(
        target_vec,
        collection="wallet_clusters",
        limit=limit,
        min_similarity=min_similarity,
    )

    return results


async def find_similar_bundles(
    target_bundle: dict[str, Any],
    min_similarity: float = 0.6,
    limit: int = 10,
) -> list[dict[str, Any]]:
    """Find bundles similar to a target bundle."""
    from app.supabase_vector import get_vector_store

    target_vec = embed_bundle_profile(target_bundle)
    store = await get_vector_store()
    return await store.search(
        target_vec,
        collection="bundle_patterns",
        limit=limit,
        min_similarity=min_similarity,
    )


# ══════════════════════════════════════════════════════════════════════
# NL → CLUSTER SEARCH
# ══════════════════════════════════════════════════════════════════════


async def search_clusters_by_description(
    query: str,
    min_similarity: float = 0.5,
    limit: int = 10,
) -> list[dict[str, Any]]:
    """
    Natural language cluster search.
    "Show me all wash trading clusters from Solana in the last month"
    → embeds the query, searches against cluster behavioral vectors
    """
    from app.crypto_embeddings import get_embedder
    from app.supabase_vector import get_vector_store

    embedder = await get_embedder()

    # Embed the NL query
    query_vec = await embedder._semantic_embed_one(f"Wallet cluster with behavior: {query}", "semantic")

    store = await get_vector_store()

    # Hybrid search: semantic + keyword
    results = await store.hybrid_search(
        query_text=query,
        query_embedding=query_vec,
        collection="wallet_clusters",
        limit=limit,
    )

    # Auto-label results if not already labeled
    for r in results:
        if "label" not in r.get("metadata", {}):
            try:
                labeling = await auto_label_cluster(
                    r.get("metadata", {}),
                    r.get("metadata", {}).get("vector", []),
                )
                r["metadata"]["auto_label"] = labeling
            except Exception:
                pass

    return results


# ══════════════════════════════════════════════════════════════════════
# FULL BUNDLE/CLUSTER → RAG PIPELINE
# ══════════════════════════════════════════════════════════════════════


async def index_bundle_detection(bundle: dict[str, Any]) -> str:
    """
    After bundle detection runs, index the result in RAG.
    Store bundle behavioral vector + metadata for future similarity search.
    """
    from app.supabase_vector import get_vector_store

    vec = embed_bundle_profile(bundle)
    token = bundle.get("token_address", "unknown")
    bundle_id = hashlib.sha256(
        f"bundle:{token}:{bundle.get('earliest_block', 0)}:{bundle.get('wallets_in_earliest_block', 0)}".encode()
    ).hexdigest()[:16]

    content = f"""Bundle detected on token {token}.
Confidence: {bundle.get("confidence", 0):.2f}
Wallets in earliest block: {bundle.get("wallets_in_earliest_block", 0)}
Common funder: {bundle.get("common_funder_address", "none")}
Signals: atomic_block={bundle.get("atomic_block_score", 0):.2f}, common_funder={bundle.get("common_funder_score", 0):.2f}
Top3 holder %: {bundle.get("top3_holder_percent", 0):.1f}%"""

    store = await get_vector_store()
    await store.insert(
        doc_id=bundle_id,
        collection="bundle_patterns",
        embedding=vec,
        content=content,
        metadata={
            "token_address": token,
            "confidence": bundle.get("confidence", 0),
            "severity": "high" if bundle.get("confidence", 0) > 0.7 else "medium",
            "chain": bundle.get("chain", "solana"),
            "detection_type": "bundle",
        },
        source="bundle_detector",
        severity="high" if bundle.get("confidence", 0) > 0.7 else "medium",
    )

    logger.info(f"Indexed bundle {bundle_id} for token {token}")
    return bundle_id


async def index_cluster_detection(cluster: dict[str, Any]) -> dict[str, Any]:
    """
    After cluster detection runs, index + auto-label + store.
    """
    from app.supabase_vector import get_vector_store

    vec = embed_cluster_profile(cluster)

    cluster_id = str(cluster.get("cluster_id", cluster.get("id", "")))
    if not cluster_id:
        cluster_id = hashlib.sha256(json.dumps(cluster, sort_keys=True, default=str).encode()).hexdigest()[:16]

    # Auto-label the cluster
    labels = await auto_label_cluster(cluster, vec)

    content = f"""Wallet cluster: {labels["top_label"]} (confidence: {labels["top_confidence"]:.2f})
Size: {cluster.get("size", "?")} wallets
Volume: ${cluster.get("total_volume_usd", 0):,.0f}
Age: {cluster.get("age_days", "?")} days
Active chains: {", ".join(cluster.get("active_chains", ["unknown"]))}
Risk: scam={cluster.get("scam_probability", 0):.1%}, rug={cluster.get("rug_probability", 0):.1%}, bot={cluster.get("bot_probability", 0):.1%}
All labels: {json.dumps(labels["all_labels"])}"""

    store = await get_vector_store()
    await store.insert(
        doc_id=cluster_id,
        collection="wallet_clusters",
        embedding=vec,
        content=content,
        metadata={
            "cluster_id": cluster_id,
            "size": cluster.get("size", 0),
            "top_label": labels["top_label"],
            "label_confidence": labels["top_confidence"],
            "all_labels": labels["all_labels"],
            "scam_probability": cluster.get("scam_probability", 0),
            "severity": labels["all_labels"][0]["severity"] if labels["all_labels"] else "medium",
            "chain": cluster.get("active_chains", ["unknown"])[0] if cluster.get("active_chains") else "unknown",
        },
        source="cluster_detector",
        severity=labels["all_labels"][0]["severity"] if labels["all_labels"] else "medium",
    )

    logger.info(f"Indexed cluster {cluster_id} as '{labels['top_label']}' ({labels['top_confidence']:.2f})")

    return {
        "cluster_id": cluster_id,
        **labels,
    }


# ══════════════════════════════════════════════════════════════════════
# BULK BACKFILL
# ══════════════════════════════════════════════════════════════════════


async def backfill_label_templates():
    """Index all cluster label templates into pgvector for auto-labeling."""
    from app.crypto_embeddings import get_embedder
    from app.supabase_vector import get_vector_store

    embedder = await get_embedder()
    store = await get_vector_store()
    count = 0

    for template in CLUSTER_LABEL_TEMPLATES:
        label_id = hashlib.sha256(f"label_template:{template['label']}".encode()).hexdigest()[:16]
        content = f"LABEL: {template['label']}. {template['description']}. Examples: {template['examples']}. Signals: {', '.join(template['signals'])}."

        try:
            vec = await embedder._semantic_embed_one(content, "semantic")
        except Exception:
            vec = embedder._hash_embed(content)

        await store.insert(
            doc_id=label_id,
            collection="cluster_labels",
            embedding=vec,
            content=content,
            metadata=template,
            source="rmi-curated",
            severity=template["severity"],
        )
        count += 1

    logger.info(f"Backfilled {count} cluster label templates")
    return count
