"""
Bundle Detection Engine — Atomic block co-occurrence analysis.
Detects Jito bundles, Flashbots bundles, and coordinated launches.
Implements: atomic-block grouping, common funder, temporal clustering,
distribution anomaly detection, holder concentration scoring.

References:
- Section 2.2, Bundle Detection Heuristics
- HNUT: 78% early activity was bundled transactions
"""

import logging
from collections import defaultdict
from dataclasses import dataclass

logger = logging.getLogger(__name__)


@dataclass
class BundleDetection:
    """Result of bundle detection on a token."""

    token_address: str
    chain: str
    is_bundled: bool
    confidence: float  # 0-1

    # Individual signals
    atomic_block_score: float = 0.0
    common_funder_score: float = 0.0
    temporal_score: float = 0.0
    distribution_anomaly_score: float = 0.0
    concentration_score: float = 0.0

    # Details
    earliest_block: int | None = None
    wallets_in_earliest_block: int = 0
    common_funder_address: str | None = None
    funded_wallets_count: int = 0
    time_window_seconds: float = 0
    identical_amount_count: int = 0
    round_amount_count: int = 0
    top10_holder_pct: float = 0.0
    top3_holder_pct: float = 0.0
    holder_count: int = 0

    risk_label: str = "unknown"  # critical, high, medium, low


class BundleDetector:
    """Multi-signal bundle detection for Solana tokens."""

    def __init__(self):
        self.min_holders_for_analysis = 5

    async def detect(
        self,
        token_address: str,
        chain: str = "solana",
        holders: list[dict] | None = None,
        transactions: list[dict] | None = None,
    ) -> BundleDetection:
        """Run all bundle detection signals and return combined result."""
        result = BundleDetection(token_address=token_address, chain=chain, is_bundled=False, confidence=0.0)

        if not holders or len(holders) < self.min_holders_for_analysis:
            result.risk_label = "unknown"
            return result

        result.holder_count = len(holders)

        # 1. Atomic block co-occurrence
        if transactions:
            self._atomic_block_signal(result, transactions)

        # 2. Common funding source
        if transactions:
            self._common_funder_signal(result, transactions)

        # 3. Temporal clustering
        if transactions:
            self._temporal_signal(result, transactions)

        # 4. Distribution anomaly detection
        self._distribution_anomaly_signal(result, holders)

        # 5. Holder concentration
        self._concentration_signal(result, holders)

        # Combine signals into final score
        result.confidence = self._combined_score(result)
        result.is_bundled = result.confidence >= 0.5
        result.risk_label = self._risk_label(result.confidence)

        return result

    def _atomic_block_signal(self, result: BundleDetection, txs: list[dict]):
        """Check if many holders acquired tokens in the same block (atomic bundle)."""
        block_wallets = defaultdict(set)
        for tx in txs:
            block = tx.get("blockNumber") or tx.get("slot")
            wallet = tx.get("from") or tx.get("signer") or tx.get("wallet")
            if block and wallet:
                block_wallets[block].add(wallet)

        if not block_wallets:
            return

        # Find block with most wallet activity
        max_block = max(block_wallets, key=lambda b: len(block_wallets[b]))
        max_wallets = len(block_wallets[max_block])

        result.earliest_block = max_block
        result.wallets_in_earliest_block = max_wallets

        if max_wallets >= 10:
            result.atomic_block_score = 0.9
        elif max_wallets >= 5:
            result.atomic_block_score = 0.7
        elif max_wallets >= 3:
            result.atomic_block_score = 0.4
        else:
            result.atomic_block_score = 0.1

    def _common_funder_signal(self, result: BundleDetection, txs: list[dict]):
        """Detect if multiple buyers were funded from the same source wallet."""
        funder_counts = defaultdict(int)
        for tx in txs:
            funder = tx.get("from") or tx.get("signer")
            recipient = tx.get("to") or tx.get("recipient")
            if funder and recipient and funder != recipient:
                funder_counts[funder] += 1

        if not funder_counts:
            return

        top_funder = max(funder_counts, key=funder_counts.get)
        top_count = funder_counts[top_funder]

        result.common_funder_address = top_funder
        result.funded_wallets_count = top_count

        if top_count >= 20:
            result.common_funder_score = 0.9
        elif top_count >= 10:
            result.common_funder_score = 0.7
        elif top_count >= 5:
            result.common_funder_score = 0.5
        elif top_count >= 3:
            result.common_funder_score = 0.3

    def _temporal_signal(self, result: BundleDetection, txs: list[dict]):
        """Check if wallets appeared within a narrow time window."""
        timestamps = []
        for tx in txs:
            ts = tx.get("timestamp") or tx.get("blockTime")
            if ts:
                timestamps.append(ts)

        if len(timestamps) < 2:
            return

        timestamps.sort()
        window = timestamps[-1] - timestamps[0]
        result.time_window_seconds = window

        if window <= 60:  # All within 1 minute
            result.temporal_score = 0.9
        elif window <= 300:  # 5 minutes
            result.temporal_score = 0.7
        elif window <= 900:  # 15 minutes
            result.temporal_score = 0.5
        elif window <= 3600:  # 1 hour
            result.temporal_score = 0.3

    def _distribution_anomaly_signal(self, result: BundleDetection, holders: list[dict]):
        """Check for flat/rounded amounts — hallmark of bundled distribution."""
        amounts = []
        for h in holders:
            amt = h.get("amount", 0)
            if isinstance(amt, (int, float)) and amt > 0:
                amounts.append(amt)

        if not amounts:
            return

        # Identical amounts
        from collections import Counter

        amount_counts = Counter(amounts)
        identical = sum(1 for count in amount_counts.values() if count >= 3)
        result.identical_amount_count = identical

        # Round number amounts (multiples of 100, 1000, 10000)
        round_count = sum(1 for a in amounts if a >= 100 and (a % 100 == 0 or a % 1000 == 0 or a % 10000 == 0))
        result.round_amount_count = round_count

        identical_pct = identical / len(amounts) if amounts else 0
        round_pct = round_count / len(amounts) if amounts else 0

        # Combine
        anomaly_pct = max(identical_pct, round_pct)
        if anomaly_pct > 0.5:
            result.distribution_anomaly_score = 0.9
        elif anomaly_pct > 0.3:
            result.distribution_anomaly_score = 0.7
        elif anomaly_pct > 0.15:
            result.distribution_anomaly_score = 0.5
        elif anomaly_pct > 0.05:
            result.distribution_anomaly_score = 0.3

    def _concentration_signal(self, result: BundleDetection, holders: list[dict]):
        """Check top-10 and top-3 holder concentration."""
        amounts = []
        for h in holders:
            amt = h.get("amount", 0)
            if isinstance(amt, (int, float)) and amt > 0:
                amounts.append(amt)

        if not amounts:
            return

        amounts.sort(reverse=True)
        total = sum(amounts)

        top3 = sum(amounts[:3]) / total if total > 0 else 0
        top10 = sum(amounts[: min(10, len(amounts))]) / total if total > 0 else 0

        result.top3_holder_pct = round(top3 * 100, 1)
        result.top10_holder_pct = round(top10 * 100, 1)

        if top3 > 0.5 or top10 > 0.8:
            result.concentration_score = 0.9
        elif top3 > 0.3 or top10 > 0.6:
            result.concentration_score = 0.7
        elif top3 > 0.15 or top10 > 0.4:
            result.concentration_score = 0.4
        elif top3 > 0.05:
            result.concentration_score = 0.2

    def _combined_score(self, r: BundleDetection) -> float:
        """Weighted combination of all signals."""
        scores = [
            (r.atomic_block_score, 0.30),  # Atomic block is strongest signal
            (r.common_funder_score, 0.25),  # Common funder second strongest
            (r.temporal_score, 0.15),
            (r.distribution_anomaly_score, 0.20),
            (r.concentration_score, 0.10),
        ]
        weighted = sum(s * w for s, w in scores)
        # Boost if multiple strong signals
        strong_signals = sum(1 for s, _ in scores if s >= 0.7)
        if strong_signals >= 3:
            weighted = min(1.0, weighted * 1.3)
        elif strong_signals >= 2:
            weighted = min(1.0, weighted * 1.15)
        return round(weighted, 4)

    def _risk_label(self, confidence: float) -> str:
        if confidence >= 0.8:
            return "critical"
        elif confidence >= 0.6:
            return "high"
        elif confidence >= 0.4:
            return "medium"
        return "low"


# Singleton
_detector: BundleDetector | None = None


def get_bundle_detector() -> BundleDetector:
    global _detector
    if _detector is None:
        _detector = BundleDetector()
    return _detector
