"""
Exit Scam Forecaster (Rug Pull Predictor)
==========================================
Predictive model identifying tokens at high risk of rug pull.
Multi-signal analysis combining liquidity patterns, holder behavior,
deployer history, and social signals for early warning.

The model evaluates 6 risk dimensions, each weighted by predictive power,
to produce a single 0-100 rug score with explainable per-signal breakdown.

TOOL  : rug_pull_predictor
TIER  : premium / intelligence
PRICE : $0.10 (100000 atoms)
TRIAL : 1 free check

Signals Analyzed:
  - LIQUIDITY: LP lock status, liquidity depth vs market cap, sudden changes
  - HOLDER_CONCENTRATION: Top-10 concentration, single-entity cluster ratio
  - DEPLOYER: Previous project history, rug count, anonymous deployer
  - LAUNCH_PATTERN: Bundled distribution, sniper activity, presale structure
  - TRADING_BEHAVIOR: Seller concentration, wash trading, suspicious transfers
  - SOCIAL_SIGNALS: Low engagement, anonymous team, no community presence

Data Sources (all free):
  - DexScreener — token pairs, liquidity, price
  - Solscan (public) — SPL token holder data
  - Etherscan/BscScan (public free) — EVM token info
  - Birdeye public — basic token metrics
  - GoPlus (free) — token security info (if available)
"""

import asyncio
import json
import logging
import re
import time
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from typing import Any

import httpx

__all__ = ["RiskLevel", "RugPullAnalysis", "SignalResult", "analyze", "predict_rug_pull"]

logger = logging.getLogger(__name__)

# ── Constants ────────────────────────────────────────────────────

FREE_APIS = {
    "dexscreener": "https://api.dexscreener.com/latest/dex",
    "birdeye_public": "https://public-api.birdeye.so",
    "goplus": "https://api.gopluslabs.io/api/v1",
}

EVM_ADDR_RE = re.compile(r"^0x[a-fA-F0-9]{40}$")
SOLANA_ADDR_RE = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")

# Default risk thresholds
DEFAULT_WEIGHTS = {
    "liquidity": 0.25,
    "holder_concentration": 0.20,
    "deployer": 0.15,
    "launch_pattern": 0.18,
    "trading_behavior": 0.12,
    "social_signals": 0.10,
}

CHAIN_MAP: dict[str, str] = {
    "solana": "solana",
    "ethereum": "ethereum",
    "bsc": "bsc",
    "polygon": "polygon",
    "arbitrum": "arbitrum",
    "optimism": "optimism",
    "base": "base",
    "avalanche": "avalanche",
}

# ── Data Models ──────────────────────────────────────────────────


class RiskLevel(Enum):
    SAFE = "safe"
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


@dataclass
class SignalResult:
    """Individual signal analysis result."""

    signal: str
    score: float  # 0 (safe) - 100 (risky)
    weight: float
    findings: list[str] = field(default_factory=list)
    details: dict[str, Any] = field(default_factory=dict)

    @property
    def weighted_score(self) -> float:
        return self.score * self.weight

    @property
    def level(self) -> RiskLevel:
        if self.score < 20:
            return RiskLevel.SAFE
        elif self.score < 40:
            return RiskLevel.LOW
        elif self.score < 60:
            return RiskLevel.MEDIUM
        elif self.score < 80:
            return RiskLevel.HIGH
        return RiskLevel.CRITICAL


@dataclass
class RugPullAnalysis:
    """Complete rug pull risk analysis result."""

    token_address: str
    chain: str
    token_symbol: str
    token_name: str

    overall_score: float  # 0 (safe) - 100 (critical risk)
    overall_level: RiskLevel
    signals: list[SignalResult] = field(default_factory=list)
    summary: str = ""
    recommendations: list[str] = field(default_factory=list)
    analysis_timestamp: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "token_address": self.token_address,
            "chain": self.chain,
            "token_symbol": self.token_symbol,
            "token_name": self.token_name,
            "rug_score": round(self.overall_score, 1),
            "risk_level": self.overall_level.value,
            "signals": [
                {
                    "name": s.signal,
                    "score": round(s.score, 1),
                    "level": s.level.value,
                    "findings": s.findings,
                }
                for s in self.signals
            ],
            "summary": self.summary,
            "recommendations": self.recommendations,
            "analyzed_at": self.analysis_timestamp,
        }


# ── Helpers ──────────────────────────────────────────────────────


def is_valid_address(addr: str) -> bool:
    addr = addr.strip()
    return bool(EVM_ADDR_RE.match(addr) or SOLANA_ADDR_RE.match(addr))


def _normalize_chain(chain: str) -> str:
    chain = chain.strip().lower()
    return CHAIN_MAP.get(chain, chain)


def _level_from_score(score: float) -> RiskLevel:
    if score < 20:
        return RiskLevel.SAFE
    elif score < 40:
        return RiskLevel.LOW
    elif score < 60:
        return RiskLevel.MEDIUM
    elif score < 80:
        return RiskLevel.HIGH
    return RiskLevel.CRITICAL


# ── Signal Analyzers ────────────────────────────────────────────


async def _analyze_liquidity(client: httpx.AsyncClient, token_address: str, chain: str) -> SignalResult:
    """Analyze liquidity risk: depth, lock status, sudden changes."""
    signal = SignalResult(
        signal="liquidity",
        score=0.0,
        weight=DEFAULT_WEIGHTS["liquidity"],
        findings=[],
    )

    try:
        # Try DexScreener for pair data
        url = f"{FREE_APIS['dexscreener']}/search?q={token_address}"
        resp = await client.get(url, timeout=10)
        if resp.status_code == 200:
            data = resp.json()
            pairs = data.get("pairs", [])
            if pairs:
                pair = pairs[0]
                liquidity_usd = float(pair.get("liquidity", {}).get("usd", 0) or 0)
                fdv = float(pair.get("fdv", 0) or 0)
                volume_24h = float(pair.get("volume", {}).get("h24", 0) or 0)

                signal.details["liquidity_usd"] = liquidity_usd
                signal.details["fdv"] = fdv
                signal.details["volume_24h"] = volume_24h

                # Score: low liquidity = higher risk
                if liquidity_usd < 1000:
                    signal.score += 40
                    signal.findings.append("Extremely low liquidity (<$1K)")
                elif liquidity_usd < 10000:
                    signal.score += 25
                    signal.findings.append("Low liquidity (<$10K)")
                elif liquidity_usd < 100000:
                    signal.score += 15
                else:
                    signal.score += 5

                # Score: low liquidity-to-FDV ratio = manipulation risk
                if fdv > 0 and liquidity_usd > 0:
                    liq_ratio = liquidity_usd / fdv
                    if liq_ratio < 0.01:
                        signal.score += 20
                        signal.findings.append("Critical: Liquidity-to-FDV ratio <1% - easy to dump")
                    elif liq_ratio < 0.05:
                        signal.score += 10
                        signal.findings.append("Low liquidity-to-FDV ratio")

                # Score: zero volume or very low = suspicious
                if volume_24h < 100:
                    signal.score += 15
                    signal.findings.append("Near-zero 24h trading volume")
    except Exception as e:
        logger.warning("Liquidity analysis failed: %s", e)
        signal.findings.append("Liquidity data unavailable - scoring conservative")

    signal.score = min(signal.score, 100)
    return signal


async def _analyze_holder_concentration(client: httpx.AsyncClient, token_address: str, chain: str) -> SignalResult:
    """Analyze token holder distribution for concentration risk."""
    signal = SignalResult(
        signal="holder_concentration",
        score=0.0,
        weight=DEFAULT_WEIGHTS["holder_concentration"],
        findings=[],
    )

    if chain == "solana":
        try:
            # Try Birdeye public API for holder info
            resp = await client.get(
                f"{FREE_APIS['birdeye_public']}/public/token/token_owner?address={token_address}",
                headers={"accept": "application/json"},
                timeout=10,
            )
            if resp.status_code == 200:
                data = resp.json()
                holders = data.get("data", {}).get("tokenOwner", []) or data.get("data", {}).get("list", []) or []
                if holders:
                    total_supply_analyzed = sum(float(h.get("uiAmount", 0) or 0) for h in holders)
                    top10 = sorted(holders, key=lambda h: float(h.get("uiAmount", 0) or 0), reverse=True)[:10]
                    top10_pct = (
                        sum(float(h.get("uiAmount", 0) or 0) for h in top10) / total_supply_analyzed
                        if total_supply_analyzed > 0
                        else 0
                    )

                    signal.details["top_10_concentration_pct"] = round(top10_pct * 100, 1)

                    if top10_pct > 0.8:
                        signal.score += 45
                        signal.findings.append(
                            f"Top 10 holders control {top10_pct * 100:.0f}% of supply - extreme concentration"
                        )
                    elif top10_pct > 0.5:
                        signal.score += 30
                        signal.findings.append(f"Top 10 holders control {top10_pct * 100:.0f}% - high concentration")
                    elif top10_pct > 0.3:
                        signal.score += 15
                    else:
                        signal.score += 5
        except Exception as e:
            logger.warning("Holder concentration analysis failed: %s", e)
            signal.findings.append("Holder data unavailable")

    # Conservative scoring if we lack data
    if not signal.findings:
        signal.score = 30  # moderate default
        signal.findings.append("Limited holder data - scored conservatively")

    signal.score = min(signal.score, 100)
    return signal


async def _analyze_deployer(client: httpx.AsyncClient, deployer: str | None, chain: str) -> SignalResult:
    """Analyze deployer risk based on available data."""
    signal = SignalResult(
        signal="deployer",
        score=0.0,
        weight=DEFAULT_WEIGHTS["deployer"],
        findings=[],
    )

    if not deployer:
        signal.score = 50
        signal.findings.append("Deployer address unknown - cannot verify history")
        signal.score = min(signal.score, 100)
        return signal

    try:
        if chain == "solana":
            # Try to check if deployer has multiple tokens
            resp = await client.get(
                f"{FREE_APIS['birdeye_public']}/public/token/list?creator={deployer}",
                headers={"accept": "application/json"},
                timeout=10,
            )
            if resp.status_code == 200:
                data = resp.json()
                tokens_deployed = data.get("data", {}).get("items", [])
                token_count = len(tokens_deployed)

                signal.details["deployer_token_count"] = token_count

                if token_count > 20:
                    signal.score += 40
                    signal.findings.append(f"Serial deployer: {token_count} tokens created - potential rug factory")
                elif token_count > 10:
                    signal.score += 25
                    signal.findings.append(f"Active deployer: {token_count} tokens created")
                elif token_count > 5:
                    signal.score += 10
                else:
                    signal.score += 5
        else:
            # EVM - check if deployer is a known contract factory
            signal.score += 10
            signal.findings.append("EVM deployer - limited public history available")
    except Exception as e:
        logger.warning("Deployer analysis failed: %s", e)
        signal.findings.append("Deployer data unavailable")

    signal.score = min(signal.score, 100)
    return signal


async def _analyze_launch_pattern(client: httpx.AsyncClient, token_address: str, chain: str) -> SignalResult:
    """Analyze launch pattern for manipulation signs."""
    signal = SignalResult(
        signal="launch_pattern",
        score=20.0,  # Start at moderate risk as baseline
        weight=DEFAULT_WEIGHTS["launch_pattern"],
        findings=[],
    )

    try:
        # Use DexScreener for trading data
        url = f"{FREE_APIS['dexscreener']}/search?q={token_address}"
        resp = await client.get(url, timeout=10)
        if resp.status_code == 200:
            data = resp.json()
            pairs = data.get("pairs", [])
            if pairs:
                pair = pairs[0]
                pair_age_hours = None
                if pair.get("pairCreatedAt"):
                    created = pair["pairCreatedAt"] / 1000  # ms to s
                    pair_age_hours = (time.time() - created) / 3600
                    signal.details["age_hours"] = round(pair_age_hours, 1)

                txns = pair.get("txns", {})
                buys_h24 = int(txns.get("h24", {}).get("buys", 0))
                sells_h24 = int(txns.get("h24", {}).get("sells", 0))

                signal.details["buys_24h"] = buys_h24
                signal.details["sells_24h"] = sells_h24

                # Very young pair = higher risk
                if pair_age_hours is not None:
                    if pair_age_hours < 1:
                        signal.score += 25
                        signal.findings.append(f"Pair is {pair_age_hours:.1f}h old - extremely young, high risk")
                    elif pair_age_hours < 24:
                        signal.score += 15
                        signal.findings.append(f"Pair is {pair_age_hours:.1f}h old - recent launch")

                # Abnormal buy/sell ratio
                total_txns = buys_h24 + sells_h24
                if total_txns > 0:
                    sell_ratio = sells_h24 / total_txns
                    if sell_ratio > 0.8:
                        signal.score += 20
                        signal.findings.append(f"Sell ratio {sell_ratio * 100:.0f}% - heavy selling pressure")
                    elif sell_ratio > 0.6:
                        signal.score += 10
                    elif sell_ratio < 0.1 and buys_h24 > 50:
                        signal.score += 15
                        signal.findings.append("Abnormally low sell ratio - possible bot wash trading")
    except Exception as e:
        logger.warning("Launch pattern analysis failed: %s", e)
        signal.findings.append("Launch data partially available")

    signal.score = min(signal.score, 100)
    return signal


async def _analyze_trading_behavior(client: httpx.AsyncClient, token_address: str, chain: str) -> SignalResult:
    """Analyze trading behavior for manipulation signs."""
    signal = SignalResult(
        signal="trading_behavior",
        score=10.0,
        weight=DEFAULT_WEIGHTS["trading_behavior"],
        findings=[],
    )

    try:
        url = f"{FREE_APIS['dexscreener']}/search?q={token_address}"
        resp = await client.get(url, timeout=10)
        if resp.status_code == 200:
            data = resp.json()
            pairs = data.get("pairs", [])
            if pairs:
                pair = pairs[0]
                liquidity_usd = float(pair.get("liquidity", {}).get("usd", 0) or 0)
                volume_24h = float(pair.get("volume", {}).get("h24", 0) or 0)

                # Suspicious volume-to-liquidity ratio (wash trading indicator)
                if liquidity_usd > 0 and volume_24h > 0:
                    vol_liq_ratio = volume_24h / liquidity_usd
                    if vol_liq_ratio > 20:
                        signal.score += 25
                        signal.findings.append(
                            f"Volume/liquidity ratio of {vol_liq_ratio:.0f}x - possible wash trading"
                        )
                    elif vol_liq_ratio > 10:
                        signal.score += 10
                elif liquidity_usd == 0 and volume_24h > 0:
                    signal.score += 20
                    signal.findings.append("Volume with zero liquidity - suspicious")

                # Price change indicators
                price_change_h24 = pair.get("priceChange", {}).get("h24", 0)
                if isinstance(price_change_h24, int | float):
                    if price_change_h24 < -50:
                        signal.score += 20
                        signal.findings.append(f"Price dropped {abs(price_change_h24):.0f}% in 24h")
                    elif price_change_h24 > 500:
                        signal.score += 10
                        signal.findings.append(f"Price surged {price_change_h24:.0f}% in 24h - potential pump")
    except Exception as e:
        logger.warning("Trading behavior analysis failed: %s", e)

    signal.score = min(signal.score, 100)
    return signal


async def _analyze_social_signals(token_address: str, chain: str) -> SignalResult:
    """Analyze social signals (placeholder - relies on chain metadata)."""
    signal = SignalResult(
        signal="social_signals",
        score=20.0,  # Default conservative
        weight=DEFAULT_WEIGHTS["social_signals"],
        findings=["Social signal analysis limited with free data sources"],
    )

    # Conservative scoring: no social data = moderate risk
    signal.score = 30
    return signal


# ── Main Analysis ───────────────────────────────────────────────


async def predict_rug_pull(
    token_address: str,
    chain: str = "solana",
    deployer: str | None = None,
    weights: dict[str, float] | None = None,
) -> RugPullAnalysis:
    """
    Predict rug pull risk for a given token.

    Args:
        token_address: The token contract/mint address
        chain: Blockchain (solana, ethereum, bsc, etc.)
        deployer: Optional deployer/creator address for history check
        weights: Optional custom signal weights (must sum to 1.0)

    Returns:
        RugPullAnalysis with overall score and per-signal breakdown
    """
    if not is_valid_address(token_address):
        raise ValueError(f"Invalid token address: {token_address}")

    chain = _normalize_chain(chain)
    effective_weights = weights or DEFAULT_WEIGHTS

    # Validate weights
    if abs(sum(effective_weights.values()) - 1.0) > 0.01:
        raise ValueError("Signal weights must sum to 1.0")

    token_symbol = ""
    token_name = ""

    # Run all signal analyzers in parallel (single client for efficiency)
    async with httpx.AsyncClient(timeout=30.0) as client:
        # Quick metadata fetch for display
        try:
            search_resp = await client.get(f"{FREE_APIS['dexscreener']}/search?q={token_address}", timeout=10)
            if search_resp.status_code == 200:
                pairs = search_resp.json().get("pairs", [])
                if pairs:
                    token_symbol = pairs[0].get("baseToken", {}).get("symbol", "")
                    token_name = pairs[0].get("baseToken", {}).get("name", "")
        except Exception:
            pass
        results = await asyncio.gather(
            _analyze_liquidity(client, token_address, chain),
            _analyze_holder_concentration(client, token_address, chain),
            _analyze_deployer(client, deployer, chain),
            _analyze_launch_pattern(client, token_address, chain),
            _analyze_trading_behavior(client, token_address, chain),
            _analyze_social_signals(token_address, chain),
            return_exceptions=True,
        )

    signals: list[SignalResult] = []
    for r in results:
        if isinstance(r, SignalResult):
            signals.append(r)
        elif isinstance(r, Exception):
            logger.warning("Signal analysis exception: %s", r)
            signals.append(
                SignalResult(
                    signal="unknown",
                    score=50.0,
                    weight=0.05,
                    findings=[f"Analysis error: {r}"],
                )
            )

    # Calculate weighted overall score using ternary
    total_weight = sum(s.weight for s in signals)
    overall_score = sum(s.score * s.weight for s in signals) / total_weight if total_weight > 0 else 50.0

    overall_score = min(max(overall_score, 0), 100)
    overall_level = _level_from_score(overall_score)

    # Generate summary
    if overall_score >= 70:
        summary = (
            f"CRITICAL RISK: This token shows strong rug pull indicators "
            f"(score: {overall_score:.0f}/100). Multiple red flags detected "
            f"across {sum(1 for s in signals if s.score > 50)} risk dimensions."
        )
    elif overall_score >= 50:
        summary = (
            f"HIGH RISK: This token has concerning signals "
            f"(score: {overall_score:.0f}/100). Proceed with extreme caution."
        )
    elif overall_score >= 30:
        summary = (
            f"MODERATE RISK: Some concerning patterns detected "
            f"(score: {overall_score:.0f}/100). Standard due diligence recommended."
        )
    else:
        summary = (
            f"LOW RISK: No major rug pull indicators detected "
            f"(score: {overall_score:.0f}/100). Continue standard safety checks."
        )

    # Generate recommendations
    recommendations = []
    if overall_score >= 50:
        recommendations.append("AVOID trading this token - high rug pull probability")
    if any(s.score > 60 for s in signals if s.signal == "liquidity"):
        recommendations.append("Liquidity is critically low or manipulable")
    if any(s.score > 50 for s in signals if s.signal == "holder_concentration"):
        recommendations.append("Top holders control excessive supply - potential dump risk")
    if any(s.score > 50 for s in signals if s.signal == "deployer"):
        recommendations.append("Deployer has concerning history - check their other projects")
    if recommendations:
        recommendations.append("Always verify with RugShield or SENTINEL deep scan before trading")

    return RugPullAnalysis(
        token_address=token_address,
        chain=chain,
        token_symbol=token_symbol or "UNKNOWN",
        token_name=token_name or "Unknown Token",
        overall_score=overall_score,
        overall_level=overall_level,
        signals=signals,
        summary=summary,
        recommendations=recommendations or ["No specific concerns detected"],
        analysis_timestamp=datetime.now(UTC).isoformat(),
    )


# ── Synchronous Wrapper ─────────────────────────────────────────


def analyze(token_address: str, chain: str = "solana", **kwargs: Any) -> dict[str, Any]:
    """
    Synchronous entry point for rug pull prediction.

    Args:
        token_address: Token contract/mint address
        chain: Blockchain name
        **kwargs: Additional options (deployer, weights)

    Returns:
        Dict with full analysis results
    """
    result = asyncio.run(predict_rug_pull(token_address, chain, **kwargs))
    return result.to_dict()


# ── CLI Entry Point ─────────────────────────────────────────────

if __name__ == "__main__":
    import sys

    logging.basicConfig(level=logging.INFO)

    if len(sys.argv) < 2:
        print("Usage: python3 rug_pull_predictor.py <TOKEN_ADDRESS> [chain] [deployer]")
        sys.exit(1)

    addr = sys.argv[1]
    chain = sys.argv[2] if len(sys.argv) > 2 else "solana"
    deployer = sys.argv[3] if len(sys.argv) > 3 else None

    result = analyze(addr, chain, deployer=deployer)
    print(json.dumps(result, indent=2))
