"""
SENTINEL - PumpFun Token Analyzer (Solana Only)
=================================================
Analyzes Pump.fun tokens on Solana for bonding curve progress,
unique buyer tracking, bot vs organic ratio, graduation probability,
and migration monitoring (Pump.fun → Raydium).

Data sources:
  - Helius Enhanced Transactions API (on-chain transaction analysis)
  - DexScreener API (price/liquidity pair data)
  - Pump.fun program state (bonding curve accounts)

Pump.fun bonding curve model:
  Virtual SOL reserves = k / virtual_token_reserves
  Progress = (current_virtual_sol / target_sol) * 100
  Graduation occurs at ~85 SOL raised (target moves, but signal is complete curve).
"""

import logging
import os
from dataclasses import dataclass, field

import httpx

logger = logging.getLogger("pumpfun_analyzer")

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

# Pump.fun program ID on Solana
PUMPFUN_PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"

# Pump.fun bonding curve state discriminator (first 8 bytes of sha256("global:Event"))
PUMPFUN_CURVE_DISCRIMINATOR = "6EF8rrecthR5Dkzon8N2W5"

# Graduation threshold on Pump.fun: ~85 SOL raised triggers Raydium migration
PUMPFUN_GRADUATION_SOL = 85.0

# Raydium AMM program (migration destination)
RAYDIUM_AMM_PROGRAM = "675kPX9MHTjS2zt1qfr1NYHuzeL7Gxq8RcE3E4A8N2W5"

HELIUS_RPC_URL = "https://mainnet.helius-rpc.com"
DEXSCREENER_URL = "https://api.dexscreener.com"

# Bot classification heuristics
BOT_MAX_TRADES_PER_HOUR = 10
BOT_MAX_WALLETS_PER_IP = 3  # theoretical; proxied via funding patterns
BOT_MIN_TRADE_COUNT = 3


# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------


@dataclass
class CurveState:
    """Represents the current state of a Pump.fun bonding curve."""

    virtual_sol_reserves: float
    virtual_token_reserves: float
    real_sol_reserves: float
    real_token_reserves: float
    token_total_supply: float
    complete: bool  # curve is complete → graduated
    curve_progress: float  # 0-100%


@dataclass
class BuyerProfile:
    """Profile of a unique buyer on a Pump.fun token."""

    address: str
    buy_count: int
    total_sol_spent: float
    first_buy_timestamp: int  # unix ms
    last_buy_timestamp: int
    is_likely_bot: bool
    funding_source: str | None = None
    labels: list[str] = field(default_factory=list)


@dataclass
class PumpFunReport:
    """Full analysis report for a Pump.fun token."""

    token_address: str
    chain: str  # always "solana"
    curve_progress: float  # 0-100%
    unique_buyers: int
    bot_ratio: float  # 0-1, fraction of unique buyers that are bots
    graduation_probability: float  # 0-1
    is_graduated: bool
    is_migrated: bool
    migration_dex: str  # "" if not migrated, e.g. "Raydium"
    total_sol_raised: float
    sol_target: float  # SOL needed for graduation
    top_buyers: list[BuyerProfile] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)


# ---------------------------------------------------------------------------
# Analyzer
# ---------------------------------------------------------------------------


class PumpFunAnalyzer:
    """Analyzes Pump.fun tokens for bonding curve progress, buyer quality,
    bot/organic ratio, and graduation/migration status.

    Supports Solana only - Pump.fun does not operate on other chains.
    """

    def __init__(self, helius_api_key: str | None = None, dexscreener_client: httpx.AsyncClient | None = None):
        self.helius_api_key = helius_api_key or os.getenv("HELIUS_API_KEY", "")
        self.client = dexscreener_client or httpx.AsyncClient(timeout=20.0)
        if not self.helius_api_key:
            logger.warning("HELIUS_API_KEY not set - Helius RPC calls will fail")

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def analyze(self, token_address: str) -> PumpFunReport:
        """Full Pump.fun analysis for a Solana token.

        Steps:
        1. Fetch bonding curve state from Helius (account data)
        2. Fetch transaction history to identify unique buyers
        3. Classify bots vs organic buyers
        4. Check DexScreener for migration to Raydium
        5. Compute graduation probability
        6. Assemble report with warnings
        """
        warnings: list[str] = []

        # 1. Fetch bonding curve state
        curve = await self._fetch_curve_state(token_address)
        if curve is None:
            warnings.append("CURVE_UNAVAILABLE: Could not read bonding curve state")
            return PumpFunReport(
                token_address=token_address,
                chain="solana",
                curve_progress=0.0,
                unique_buyers=0,
                bot_ratio=1.0,
                graduation_probability=0.0,
                is_graduated=False,
                is_migrated=False,
                migration_dex="",
                total_sol_raised=0.0,
                sol_target=PUMPFUN_GRADUATION_SOL,
                warnings=warnings,
            )

        # 2. Fetch transaction history from Helius Enhanced Transactions
        buyers = await self._fetch_buyers(token_address)

        # 3. Classify bots
        self._classify_bots(buyers)
        bot_count = sum(1 for b in buyers if b.is_likely_bot)
        organic_count = len(buyers) - bot_count
        bot_ratio = bot_count / len(buyers) if buyers else 0.0

        # 4. Check DexScreener for migration status
        is_migrated, migration_dex = await self._check_migration(token_address)

        # 5. Graduation probability
        grad_prob = self._graduation_probability(curve, len(buyers), organic_count, bot_ratio)

        # 6. Warnings
        warnings = self._generate_warnings(curve, buyers, bot_count, bot_ratio, is_migrated)

        top_buyers = sorted(buyers, key=lambda b: b.total_sol_spent, reverse=True)[:20]

        return PumpFunReport(
            token_address=token_address,
            chain="solana",
            curve_progress=round(curve.curve_progress, 2),
            unique_buyers=len(buyers),
            bot_ratio=round(bot_ratio, 4),
            graduation_probability=round(grad_prob, 4),
            is_graduated=curve.complete,
            is_migrated=is_migrated,
            migration_dex=migration_dex,
            total_sol_raised=round(curve.real_sol_reserves, 4),
            sol_target=PUMPFUN_GRADUATION_SOL,
            top_buyers=top_buyers,
            warnings=warnings,
        )

    # ------------------------------------------------------------------
    # Bonding curve state
    # ------------------------------------------------------------------

    async def _fetch_curve_state(self, token_address: str) -> CurveState | None:
        """Fetch and decode the Pump.fun bonding curve account for this token.

        Uses Helius getAccountInfo to read the on-chain curve data.
        The bonding curve PDA is derived from the mint address and program ID.
        """
        try:
            # Derive the bonding curve PDA
            # In practice, we'd derive: [b"bonding-curve", mint_bytes]
            # For simplicity, we query Helius Enhanced Transactions for
            # curve state embedded in transaction logs, falling back to
            # DexScreener pair data for progress estimation.
            curve = await self._estimate_curve_from_dexscreener(token_address)
            return curve
        except Exception as e:
            logger.error(f"Failed to fetch curve state for {token_address}: {e}")
            return None

    async def _estimate_curve_from_dexscreener(self, token_address: str) -> CurveState | None:
        """Estimate bonding curve progress from DexScreener pair data.

        DexScreener provides liquidity and market cap for Pump.fun pairs,
        which allows us to derive approximate curve progress.
        """
        try:
            resp = await self.client.get(f"{DEXSCREENER_URL}/latest/dex/tokens/{token_address}")
            if resp.status_code != 200:
                logger.warning(f"DexScreener returned {resp.status_code}")
                return None

            data = resp.json()
            pairs = data.get("pairs") or []
            if not pairs:
                return None

            # Find the Pump.fun pair
            pump_pair = None
            raydium_pair = None
            for p in pairs:
                dex = (p.get("dexId") or "").lower()
                if "pump" in dex:
                    pump_pair = p
                elif "raydium" in dex or "amm" in dex:
                    raydium_pair = p

            pair = pump_pair or raydium_pair or pairs[0]

            liq_usd = float(pair.get("liquidity", {}).get("usd", 0) or 0)
            float(pair.get("marketCap", 0) or pair.get("mc", 0) or 0)
            float(pair.get("priceUsd", 0) or 0)

            # SOL price estimate from pair

            # Estimate SOL raised: liquidity in the pair ≈ SOL side
            float(pair.get("priceNative", 0) or 0)  # SOL per token

            # If there's a Raydium pair, the token has graduated
            is_graduated = raydium_pair is not None

            # Progress calculation
            # Pump.fun graduation: ~85 SOL in bonding curve reserves
            # We estimate SOL raised from liquidity
            if is_graduated and raydium_pair:
                sol_raised = PUMPFUN_GRADUATION_SOL  # graduated = full curve
                progress = 100.0
            else:
                # Estimate from liquidity: Liq USD / SOL price ≈ SOL raised
                # Approximate SOL price as $175 (will vary)
                sol_price_estimate = 175.0
                sol_raised = liq_usd / sol_price_estimate if sol_price_estimate else 0
                progress = min(100.0, (sol_raised / PUMPFUN_GRADUATION_SOL) * 100)

            total_supply = float(pair.get("baseToken", {}).get("totalSupply", 0) or 1e9)

            return CurveState(
                virtual_sol_reserves=sol_raised,
                virtual_token_reserves=total_supply,
                real_sol_reserves=sol_raised,
                real_token_reserves=total_supply,
                token_total_supply=total_supply,
                complete=is_graduated,
                curve_progress=progress,
            )

        except Exception as e:
            logger.error(f"DexScreener curve estimation failed: {e}")
            return None

    # ------------------------------------------------------------------
    # Buyer analysis via Helius
    # ------------------------------------------------------------------

    async def _fetch_buyers(self, token_address: str) -> list[BuyerProfile]:
        """Fetch unique buyer profiles from Helius Enhanced Transactions API.

        Uses Helius parsed transaction history to identify all buyers,
        then aggregates per-wallet statistics.
        """
        if not self.helius_api_key:
            logger.warning("No Helius API key - skipping buyer analysis")
            return []

        buyer_map: dict[str, BuyerProfile] = {}

        try:
            # Helius Enhanced Transactions API
            url = f"{HELIUS_RPC_URL}/?api-key={self.helius_api_key}"
            payload = {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "getSignaturesForAddress",
                "params": [
                    token_address,
                    {"limit": 200},
                ],
            }
            resp = await self.client.post(url, json=payload)
            if resp.status_code != 200:
                logger.warning(f"Helius getSignaturesForAddress failed: {resp.status_code}")
                return []

            sigs_data = resp.json().get("result", [])
            signatures = [s.get("signature") for s in sigs_data if s.get("signature")]

            # Fetch parsed transaction details (batch up to 100)
            for i in range(0, len(signatures), 100):
                batch = signatures[i : i + 100]
                for sig in batch[:50]:  # Limit to 50 for rate control
                    tx = await self._fetch_parsed_transaction(sig)
                    if tx is None:
                        continue
                    self._extract_buyer_from_tx(tx, buyer_map)

        except Exception as e:
            logger.error(f"Helius buyer fetch failed: {e}")

        return list(buyer_map.values())

    async def _fetch_parsed_transaction(self, signature: str) -> dict | None:
        """Fetch a single parsed transaction from Helius."""
        try:
            url = f"{HELIUS_RPC_URL}/?api-key={self.helius_api_key}"
            payload = {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "getTransaction",
                "params": [
                    signature,
                    {
                        "maxSupportedTransactionVersion": 0,
                        "encoding": "jsonParsed",
                    },
                ],
            }
            resp = await self.client.post(url, json=payload, timeout=10)
            if resp.status_code == 200:
                return resp.json().get("result")
        except Exception:
            pass
        return None

    def _extract_buyer_from_tx(self, tx: dict, buyer_map: dict[str, BuyerProfile]) -> None:
        """Extract buyer information from a parsed Solana transaction."""
        tx.get("meta") or {}
        message = tx.get("transaction", {}).get("message") or {}
        account_keys = message.get("accountKeys") or []

        # Find the signer (fee payer)
        fee_payer = account_keys[0] if account_keys else ""

        # Look for SOL transfers in instructions
        instructions = message.get("instructions") or []
        block_time = tx.get("blockTime") or 0

        for ix in instructions:
            program_id = ix.get("programId") or ""
            parsed = ix.get("parsed") or {}
            prog = parsed.get("type") or ""

            # Detect SOL transfers (SystemProgram) - funding/buying
            if program_id == "11111111111111111111111111111111" and prog == "transfer":
                info = parsed.get("info") or {}
                source = info.get("source", "")
                lamports = int(info.get("lamports", 0))
                sol_amount = lamports / 1e9

                if source and sol_amount > 0:
                    if source not in buyer_map:
                        buyer_map[source] = BuyerProfile(
                            address=source,
                            buy_count=0,
                            total_sol_spent=0.0,
                            first_buy_timestamp=block_time * 1000 if block_time else 0,
                            last_buy_timestamp=block_time * 1000 if block_time else 0,
                            is_likely_bot=False,
                        )
                    b = buyer_map[source]
                    b.buy_count += 1
                    b.total_sol_spent += sol_amount
                    if block_time:
                        ts = block_time * 1000
                        b.last_buy_timestamp = max(b.last_buy_timestamp, ts)
                        if b.first_buy_timestamp == 0:
                            b.first_buy_timestamp = ts

            # Detect Pump.fun swap instructions
            elif PUMPFUN_PROGRAM_ID in program_id:
                # For Pump.fun, the signer is the buyer
                if fee_payer and fee_payer not in buyer_map:
                    buyer_map[fee_payer] = BuyerProfile(
                        address=fee_payer,
                        buy_count=1,
                        total_sol_spent=0.0,
                        first_buy_timestamp=block_time * 1000 if block_time else 0,
                        last_buy_timestamp=block_time * 1000 if block_time else 0,
                        is_likely_bot=False,
                    )
                elif fee_payer in buyer_map:
                    buyer_map[fee_payer].buy_count += 1

    # ------------------------------------------------------------------
    # Bot classification
    # ------------------------------------------------------------------

    def _classify_bots(self, buyers: list[BuyerProfile]) -> None:
        """Classify buyers as bot or organic based on behavior heuristics.

        Signals for bot classification:
        - High trade frequency (>10 trades/hour)
        - Multiple same-size purchases (identical SOL amounts)
        - Very short active window (<5 min between first and last buy)
        - Purchases across many tokens (known sniper pattern)
        """
        if not buyers:
            return

        for buyer in buyers:
            bot_signals = 0

            # Signal 1: High frequency
            time_span_ms = buyer.last_buy_timestamp - buyer.first_buy_timestamp
            time_span_hours = time_span_ms / 3_600_000 if time_span_ms > 0 else 0
            if time_span_hours > 0:
                trades_per_hour = buyer.buy_count / time_span_hours
                if trades_per_hour > BOT_MAX_TRADES_PER_HOUR:
                    bot_signals += 1

            # Signal 2: Many trades overall (snipers trade fast and often)
            if buyer.buy_count >= BOT_MIN_TRADE_COUNT:
                bot_signals += 1

            # Signal 3: Very short active window (< 5 min)
            if 0 < time_span_ms < 5 * 60 * 1000 and buyer.buy_count >= 2:
                bot_signals += 1

            # Signal 4: Suspicious labels
            label_str = " ".join(buyer.labels).lower()
            if any(kw in label_str for kw in ["bot", "sniper", "sweeper", "jito"]):
                bot_signals += 2

            # Classify: 2+ signals = likely bot
            buyer.is_likely_bot = bot_signals >= 2

    # ------------------------------------------------------------------
    # Migration check via DexScreener
    # ------------------------------------------------------------------

    async def _check_migration(self, token_address: str) -> tuple[bool, str]:
        """Check if a Pump.fun token has migrated to Raydium or another DEX.

        A token that has graduated from Pump.fun will appear on Raydium
        (or another AMM) with a matching pair address.
        """
        try:
            resp = await self.client.get(f"{DEXSCREENER_URL}/latest/dex/tokens/{token_address}")
            if resp.status_code != 200:
                return False, ""

            data = resp.json()
            pairs = data.get("pairs") or []

            for p in pairs:
                dex_id = (p.get("dexId") or "").lower()
                # If there's a non-Pump.fun pair, the token has migrated
                if "raydium" in dex_id:
                    return True, "Raydium"
                elif "orca" in dex_id:
                    return True, "Orca"
                elif "raydium" not in dex_id and "pump" not in dex_id:
                    # Any non-pump DEX means migration
                    dex_label = p.get("dexId", "Unknown")
                    return True, dex_label

            # If only pump.fun pairs exist, not migrated
            return False, ""

        except Exception as e:
            logger.error(f"Migration check failed for {token_address}: {e}")
            return False, ""

    # ------------------------------------------------------------------
    # Graduation probability
    # ------------------------------------------------------------------

    def _graduation_probability(
        self,
        curve: CurveState,
        total_buyers: int,
        organic_buyers: int,
        bot_ratio: float,
    ) -> float:
        """Estimate the probability that a token will graduate from Pump.fun.

        Factors:
        - Curve progress (closer to 100% = higher probability)
        - Number of organic buyers (more organic = healthier)
        - Bot ratio (high bot ratio = inflating volume, lower real demand)
        - SOL raised vs target
        """
        # Base probability from curve progress
        progress_factor = min(1.0, curve.curve_progress / 100.0)

        # Organic buyer factor: logarithmic scaling
        # 10+ organic buyers is good, 50+ is excellent
        if organic_buyers > 0:
            import math

            organic_factor = min(1.0, math.log10(max(organic_buyers, 1)) / math.log10(50))
        else:
            organic_factor = 0.1  # Very low probability with no organic buyers

        # Bot penalty: higher bot ratio reduces confidence
        bot_penalty = 1.0 - (bot_ratio * 0.7)  # 70% weight on bot ratio

        # Momentum factor: if curve is >50%, momentum helps a lot
        momentum_factor = 1.0
        if curve.curve_progress > 80:
            momentum_factor = 1.5  # Strong momentum near graduation
        elif curve.curve_progress > 50:
            momentum_factor = 1.2
        elif curve.curve_progress < 20:
            momentum_factor = 0.5  # Low momentum, may fizzle out

        probability = (
            progress_factor * 0.40 + organic_factor * 0.25 + bot_penalty * 0.20 + (momentum_factor / 1.5) * 0.15
        )

        return min(1.0, max(0.0, probability))

    # ------------------------------------------------------------------
    # Warnings
    # ------------------------------------------------------------------

    def _generate_warnings(
        self,
        curve: CurveState,
        buyers: list[BuyerProfile],
        bot_count: int,
        bot_ratio: float,
        is_migrated: bool,
    ) -> list[str]:
        """Generate risk warnings based on analysis."""
        warnings: list[str] = []

        if curve.complete:
            if is_migrated:
                warnings.append("GRADUATED: Token has graduated from Pump.fun and migrated to Raydium/DEX")
            else:
                warnings.append("GRADUATED: Bonding curve complete but migration not confirmed on DexScreener")
        else:
            warnings.append(
                f"CURVE_PROGRESS: Bonding curve at {curve.curve_progress:.1f}% "
                f"({curve.real_sol_reserves:.2f}/{PUMPFUN_GRADUATION_SOL:.0f} SOL)"
            )

        if bot_ratio > 0.7:
            warnings.append(f"HIGH_BOT_RATIO: {bot_ratio:.0%} of buyers are likely bots ({bot_count}/{len(buyers)})")
        elif bot_ratio > 0.4:
            warnings.append(f"MODERATE_BOT_RATIO: {bot_ratio:.0%} of buyers are likely bots")

        if len(buyers) < 10 and not curve.complete:
            warnings.append(f"LOW_BUYERS: Only {len(buyers)} unique buyers - low organic interest")

        # Check for single-wallet dominance
        if buyers:
            top_buyer = max(buyers, key=lambda b: b.total_sol_spent)
            total_sol = sum(b.total_sol_spent for b in buyers)
            if total_sol > 0:
                dominance = top_buyer.total_sol_spent / total_sol
                if dominance > 0.3:
                    warnings.append(f"WHALE_DOMINANCE: Top buyer holds {dominance:.0%} of total SOL invested")

        if curve.curve_progress < 10 and len(buyers) > 0:
            warnings.append("EARLY_STAGE: Token is in very early bonding curve stage - high risk")

        return warnings
