"""Wallet intelligence: scoring, adversarial analysis, and bot detection (Polymarket-only -- requires on-chain wallet data).

Provides three capabilities built on Polymarket public Data API:

1. **Wallet Scoring** - Evaluate wallet performance (win rate, Sharpe, PnL)
   and auto-fade bad wallets via reverse copy.
2. **Adversarial Analysis** - Detect exploitable patterns in a wallet's
   trading behavior (timing regularity, sizing, directional bias, etc.)
3. **Bot Detection** - Scan a market to classify wallets as bots vs humans
   with strategy type identification and confidence scores.

Usage:
    score = hz.score_wallet("0x1234...")
    report = hz.analyze_wallet("0x1234...")
    bots = hz.scan_bots("condition_id_here")
    hz.reverse_copy(wallet="0x1234...", threshold=-0.3)
"""

from __future__ import annotations

import logging
import math
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Any, Callable

from horizon.flow import (
    Trade,
    WalletPosition,
    get_market_trades,
    get_wallet_positions,
    get_wallet_trades,
)

logger = logging.getLogger("horizon.wallet_intel")


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class WalletScore:
    """Performance score for a wallet."""

    wallet: str
    win_rate: float
    avg_pnl_pct: float
    sharpe: float
    total_pnl: float
    trade_count: int
    position_count: int
    composite_score: float  # -1 (terrible) to +1 (excellent)


@dataclass(frozen=True)
class WalletPattern:
    """A detected behavioral pattern in a wallet's trading."""

    name: str
    description: str
    value: float
    exploitable: bool
    counter_strategy: str


@dataclass(frozen=True)
class WalletAnalysis:
    """Adversarial analysis of a wallet's trading behavior."""

    wallet: str
    patterns: list[WalletPattern]
    vulnerability_score: float  # 0 (hard to exploit) to 1 (very exploitable)
    counter_strategies: list[str]
    trade_count: int
    analysis_period_hours: int


@dataclass(frozen=True)
class BotDetection:
    """Bot detection result for a single wallet in a market."""

    wallet: str
    is_bot: bool
    confidence: float
    strategy_type: str  # market_maker, momentum, mean_reversion, copy_bot, grid_bot, sniper, human
    evidence: list[str]
    trade_count: int
    avg_interval_seconds: float
    size_regularity: float  # 0=random, 1=identical sizes


# ---------------------------------------------------------------------------
# Wallet scoring
# ---------------------------------------------------------------------------


def score_wallet(address: str, limit: int = 500) -> WalletScore:
    """Score a wallet's trading performance.

    Fetches positions and computes win rate, average PnL, Sharpe estimate,
    and a composite score from -1 (terrible) to +1 (excellent).

    Args:
        address: Wallet address (0x...).
        limit: Max positions to analyze.

    Returns:
        WalletScore with performance metrics.
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    positions = get_wallet_positions(address, limit=limit)

    if not positions:
        return WalletScore(
            wallet=address,
            win_rate=0.0,
            avg_pnl_pct=0.0,
            sharpe=0.0,
            total_pnl=0.0,
            trade_count=0,
            position_count=0,
            composite_score=0.0,
        )

    pnl_pcts = [p.pnl_percent for p in positions]
    total_pnl = sum(p.pnl for p in positions)
    wins = sum(1 for p in pnl_pcts if p > 0)
    win_rate = wins / len(pnl_pcts) if pnl_pcts else 0.0
    avg_pnl_pct = sum(pnl_pcts) / len(pnl_pcts) if pnl_pcts else 0.0

    # Sharpe estimate: mean / stddev of PnL percentages
    if len(pnl_pcts) > 1:
        mean = avg_pnl_pct
        variance = sum((x - mean) ** 2 for x in pnl_pcts) / len(pnl_pcts)
        stddev = math.sqrt(variance)
        sharpe = mean / stddev if stddev > 0 else 0.0
    else:
        sharpe = 0.0

    # Composite score: tanh-normalized blend
    norm_winrate = (win_rate - 0.5) * 4  # maps 0.5->0, 0.75->1, 0.25->-1
    pnl_component = math.copysign(1, total_pnl) * min(1.0, abs(total_pnl) / 1000.0)
    raw = 0.3 * sharpe + 0.3 * norm_winrate + 0.4 * pnl_component
    composite = math.tanh(raw)

    return WalletScore(
        wallet=address,
        win_rate=round(win_rate, 4),
        avg_pnl_pct=round(avg_pnl_pct, 4),
        sharpe=round(sharpe, 4),
        total_pnl=round(total_pnl, 2),
        trade_count=sum(1 for _ in positions),  # positions as proxy
        position_count=len(positions),
        composite_score=round(composite, 4),
    )


# ---------------------------------------------------------------------------
# Adversarial analysis
# ---------------------------------------------------------------------------


def _cv(values: list[float]) -> float:
    """Coefficient of variation (stddev / mean). Returns inf if mean is 0."""
    if len(values) < 2:
        return float("inf")
    mean = sum(values) / len(values)
    if mean == 0:
        return float("inf")
    variance = sum((x - mean) ** 2 for x in values) / len(values)
    return math.sqrt(variance) / abs(mean)


def analyze_wallet(address: str, trade_limit: int = 500) -> WalletAnalysis:
    """Analyze a wallet for exploitable trading patterns.

    Runs 6 pattern detectors on the wallet's trade history:
    timing regularity, sizing predictability, directional bias,
    reaction speed, market concentration, and stop levels.

    Args:
        address: Wallet address (0x...).
        trade_limit: Max trades to analyze.

    Returns:
        WalletAnalysis with detected patterns and vulnerability score.
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    trades = get_wallet_trades(address, limit=trade_limit)

    if not trades:
        return WalletAnalysis(
            wallet=address,
            patterns=[],
            vulnerability_score=0.0,
            counter_strategies=[],
            trade_count=0,
            analysis_period_hours=0,
        )

    patterns: list[WalletPattern] = []

    # 1. Timing regularity
    timestamps = sorted(t.timestamp for t in trades if t.timestamp > 0)
    if len(timestamps) >= 3:
        intervals = [
            timestamps[i + 1] - timestamps[i]
            for i in range(len(timestamps) - 1)
            if timestamps[i + 1] > timestamps[i]
        ]
        if intervals:
            timing_cv = _cv(intervals)
            exploitable = timing_cv < 0.3
            patterns.append(WalletPattern(
                name="timing_regularity",
                description=f"Inter-trade interval CV={timing_cv:.2f}. "
                + ("Highly regular timing - predictable trade schedule." if exploitable
                   else "Irregular timing - hard to predict."),
                value=round(timing_cv, 4),
                exploitable=exploitable,
                counter_strategy="Front-run by placing orders just before expected trade times"
                if exploitable else "No timing exploit available",
            ))

    # 2. Sizing predictability
    sizes = [t.usdc_size for t in trades if t.usdc_size > 0]
    if len(sizes) >= 3:
        size_cv = _cv(sizes)
        exploitable = size_cv < 0.3
        patterns.append(WalletPattern(
            name="sizing_predictability",
            description=f"Trade size CV={size_cv:.2f}. "
            + ("Constant sizing - predictable order size." if exploitable
               else "Varied sizing - hard to predict."),
            value=round(size_cv, 4),
            exploitable=exploitable,
            counter_strategy="Size your counter-trades to match expected order size"
            if exploitable else "No sizing exploit available",
        ))

    # 3. Directional bias
    buys = sum(1 for t in trades if t.side.upper() == "BUY")
    total = len(trades)
    if total > 0:
        buy_ratio = buys / total
        bias = abs(buy_ratio - 0.5)
        exploitable = bias > 0.25
        direction = "buyer" if buy_ratio > 0.5 else "seller"
        patterns.append(WalletPattern(
            name="directional_bias",
            description=f"Buy ratio={buy_ratio:.2f}. "
            + (f"Strong {direction} bias - fade the dominant direction." if exploitable
               else "Balanced trading - no directional exploit."),
            value=round(buy_ratio, 4),
            exploitable=exploitable,
            counter_strategy=f"Fade the {direction} bias - take the opposite side"
            if exploitable else "No directional exploit available",
        ))

    # 4. Reaction speed
    # Group trades by market and measure time between consecutive trades
    by_market: dict[str, list[int]] = defaultdict(list)
    for t in trades:
        if t.timestamp > 0 and t.condition_id:
            by_market[t.condition_id].append(t.timestamp)
    reaction_times: list[float] = []
    for mkt_ts in by_market.values():
        sorted_ts = sorted(mkt_ts)
        if len(sorted_ts) >= 2:
            for i in range(1, len(sorted_ts)):
                dt = sorted_ts[i] - sorted_ts[0]
                if dt > 0:
                    reaction_times.append(dt)
                    break
    if reaction_times:
        median_reaction = sorted(reaction_times)[len(reaction_times) // 2]
        exploitable = median_reaction < 60  # less than 60 seconds
        patterns.append(WalletPattern(
            name="reaction_speed",
            description=f"Median reaction time={median_reaction:.0f}s. "
            + ("Very fast reactor - likely automated." if exploitable
               else "Slow reactor - likely manual."),
            value=round(median_reaction, 2),
            exploitable=exploitable,
            counter_strategy="Pre-position before triggering events"
            if exploitable else "No reaction speed exploit available",
        ))

    # 5. Market concentration
    market_counts: dict[str, int] = defaultdict(int)
    for t in trades:
        if t.condition_id:
            market_counts[t.condition_id] += 1
    if market_counts:
        shares = [c / total for c in market_counts.values()]
        herfindahl = sum(s ** 2 for s in shares)
        exploitable = herfindahl > 0.3
        patterns.append(WalletPattern(
            name="concentration",
            description=f"Herfindahl index={herfindahl:.3f} across {len(market_counts)} markets. "
            + ("Highly concentrated - exploitable in those markets." if exploitable
               else "Well diversified - hard to target."),
            value=round(herfindahl, 4),
            exploitable=exploitable,
            counter_strategy="Focus counter-trading on their concentrated markets"
            if exploitable else "No concentration exploit available",
        ))

    # 6. Stop levels
    sell_trades = [t for t in trades if t.side.upper() == "SELL" and t.usdc_size > 0]
    if len(sell_trades) >= 5:
        # Use price as proxy for stop level (cluster sell prices)
        sell_prices = sorted(t.price for t in sell_trades)
        # Simple clustering: find the most common price bucket (width 0.05)
        buckets: dict[float, int] = defaultdict(int)
        for p in sell_prices:
            bucket = round(p * 20) / 20  # round to nearest 0.05
            buckets[bucket] += 1
        if buckets:
            top_bucket, top_count = max(buckets.items(), key=lambda x: x[1])
            density = top_count / len(sell_trades)
            exploitable = density > 0.3
            patterns.append(WalletPattern(
                name="stop_levels",
                description=f"Most common exit price bucket={top_bucket:.2f} "
                f"({density:.0%} of sells). "
                + ("Predictable stop levels detected." if exploitable
                   else "No clear stop levels."),
                value=round(density, 4),
                exploitable=exploitable,
                counter_strategy=f"Hunt stops near {top_bucket:.2f} price level"
                if exploitable else "No stop level exploit available",
            ))

    # Vulnerability score = mean of exploitable pattern values (mapped to 0-1)
    exploitable_patterns = [p for p in patterns if p.exploitable]
    if exploitable_patterns:
        vulnerability_score = len(exploitable_patterns) / len(patterns) if patterns else 0.0
    else:
        vulnerability_score = 0.0

    counter_strategies = [p.counter_strategy for p in patterns if p.exploitable]

    # Analysis period
    if timestamps and len(timestamps) >= 2:
        period_hours = max(1, (timestamps[-1] - timestamps[0]) // 3600)
    else:
        period_hours = 0

    return WalletAnalysis(
        wallet=address,
        patterns=patterns,
        vulnerability_score=round(vulnerability_score, 4),
        counter_strategies=counter_strategies,
        trade_count=len(trades),
        analysis_period_hours=period_hours,
    )


# ---------------------------------------------------------------------------
# Bot detection
# ---------------------------------------------------------------------------


def scan_bots(
    condition_id: str,
    trade_limit: int = 1000,
    min_trades: int = 5,
) -> list[BotDetection]:
    """Scan a market to detect bot wallets.

    Fetches recent trades, groups by wallet, and classifies each wallet
    as bot or human with strategy type and confidence score.

    Args:
        condition_id: Market condition ID (0x-prefixed hex).
        trade_limit: Max trades to analyze.
        min_trades: Min trades required per wallet to classify.

    Returns:
        List of BotDetection sorted by confidence descending.
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    trades = get_market_trades(condition_id, limit=trade_limit)

    if not trades:
        return []

    # Group by wallet
    by_wallet: dict[str, list[Trade]] = defaultdict(list)
    for t in trades:
        if t.wallet:
            by_wallet[t.wallet].append(t)

    results: list[BotDetection] = []

    for wallet, wallet_trades in by_wallet.items():
        if len(wallet_trades) < min_trades:
            continue

        # Sort by timestamp
        sorted_trades = sorted(wallet_trades, key=lambda t: t.timestamp)

        # Feature extraction
        timestamps = [t.timestamp for t in sorted_trades if t.timestamp > 0]
        intervals = [
            timestamps[i + 1] - timestamps[i]
            for i in range(len(timestamps) - 1)
            if timestamps[i + 1] >= timestamps[i]
        ]
        sizes = [t.usdc_size for t in sorted_trades if t.usdc_size > 0]

        # Timing CV
        timing_cv = _cv(intervals) if intervals else float("inf")

        # Size CV
        size_cv = _cv(sizes) if sizes else float("inf")

        # Trade frequency (trades per hour)
        if timestamps and len(timestamps) >= 2:
            span_hours = max((timestamps[-1] - timestamps[0]) / 3600, 0.001)
            frequency = len(timestamps) / span_hours
        else:
            frequency = 0.0

        # Buy/sell balance
        buys = sum(1 for t in sorted_trades if t.side.upper() == "BUY")
        sells = len(sorted_trades) - buys
        total = len(sorted_trades)
        balance = abs(buys - sells) / total if total > 0 else 0.0

        # Sub-second trades (intervals < 2 seconds)
        sub_second = sum(1 for i in intervals if i < 2) / len(intervals) if intervals else 0.0

        # Average interval
        avg_interval = sum(intervals) / len(intervals) if intervals else 0.0

        # Size regularity (1 - normalized CV, clamped to [0, 1])
        if size_cv == float("inf") or size_cv != size_cv:
            size_regularity = 0.0
        else:
            size_regularity = max(0.0, min(1.0, 1.0 - size_cv))

        # Strategy classification
        evidence: list[str] = []
        confidence = 0.0
        strategy_type = "human"

        # Market maker: balanced, high frequency, regular sizing
        mm_score = 0.0
        if balance < 0.15:
            mm_score += 0.3
            evidence.append(f"Balanced buy/sell ratio ({balance:.2f})")
        if frequency > 10:
            mm_score += 0.3
            evidence.append(f"High frequency ({frequency:.1f} trades/hr)")
        if size_regularity > 0.5:
            mm_score += 0.2
            evidence.append(f"Regular sizing (regularity={size_regularity:.2f})")
        if timing_cv < 0.5 and timing_cv != float("inf"):
            mm_score += 0.2
            evidence.append(f"Regular timing (CV={timing_cv:.2f})")

        # Grid bot: evenly spaced sizes, moderate frequency
        grid_score = 0.0
        if size_regularity > 0.7:
            grid_score += 0.4
        if balance < 0.2:
            grid_score += 0.2
        if 2 < frequency < 50:
            grid_score += 0.2
        if timing_cv < 0.5 and timing_cv != float("inf"):
            grid_score += 0.2

        # Momentum: high directional bias
        momentum_score = 0.0
        if balance > 0.6:
            momentum_score += 0.4
        if frequency > 5:
            momentum_score += 0.3
        if timing_cv < 0.8 and timing_cv != float("inf"):
            momentum_score += 0.3

        # Sniper: few fast large trades
        sniper_score = 0.0
        if sub_second > 0.3:
            sniper_score += 0.4
        if len(sorted_trades) < 20:
            sniper_score += 0.3
        if sizes:
            avg_size = sum(sizes) / len(sizes)
            if avg_size > 100:
                sniper_score += 0.4

        # Pick best match
        scores = {
            "market_maker": mm_score,
            "grid_bot": grid_score,
            "momentum": momentum_score,
            "sniper": sniper_score,
        }
        best_type = max(scores, key=lambda k: scores[k])
        best_score = scores[best_type]

        # Human heuristics: high timing CV, high size CV, low frequency
        human_score = 0.0
        if timing_cv > 1.0 or timing_cv == float("inf"):
            human_score += 0.3
        if size_cv > 0.5 or size_cv == float("inf"):
            human_score += 0.3
        if frequency < 3:
            human_score += 0.4

        if human_score > best_score:
            strategy_type = "human"
            confidence = min(1.0, human_score)
            evidence = []
            if timing_cv > 1.0 or timing_cv == float("inf"):
                evidence.append(f"Irregular timing (CV={timing_cv:.2f})")
            if size_cv > 0.5 or size_cv == float("inf"):
                evidence.append(f"Varied sizing (CV={size_cv:.2f})")
            if frequency < 3:
                evidence.append(f"Low frequency ({frequency:.1f} trades/hr)")
        else:
            strategy_type = best_type
            confidence = min(1.0, best_score)
            # Evidence was already built for mm; rebuild for others
            if strategy_type != "market_maker":
                evidence = []
                if strategy_type == "grid_bot":
                    if size_regularity > 0.7:
                        evidence.append(f"Very regular sizing (regularity={size_regularity:.2f})")
                    if balance < 0.2:
                        evidence.append(f"Balanced trading ({balance:.2f})")
                    if timing_cv < 0.5 and timing_cv != float("inf"):
                        evidence.append(f"Regular timing (CV={timing_cv:.2f})")
                elif strategy_type == "momentum":
                    if balance > 0.6:
                        evidence.append(f"Strong directional bias ({balance:.2f})")
                    if frequency > 5:
                        evidence.append(f"High frequency ({frequency:.1f} trades/hr)")
                elif strategy_type == "sniper":
                    if sub_second > 0.3:
                        evidence.append(f"Sub-second trades ({sub_second:.0%})")
                    if sizes:
                        evidence.append(f"Large avg size (${sum(sizes)/len(sizes):.0f})")

        is_bot = strategy_type != "human" and confidence > 0.6

        results.append(BotDetection(
            wallet=wallet,
            is_bot=is_bot,
            confidence=round(confidence, 4),
            strategy_type=strategy_type,
            evidence=evidence,
            trade_count=len(sorted_trades),
            avg_interval_seconds=round(avg_interval, 2),
            size_regularity=round(size_regularity, 4),
        ))

    # Sort by confidence descending
    results.sort(key=lambda x: x.confidence, reverse=True)
    return results


# ---------------------------------------------------------------------------
# Reverse copy (standalone)
# ---------------------------------------------------------------------------


def reverse_copy(
    wallet: str,
    threshold: float = -0.3,
    size_scale: float = 0.5,
    max_position: float = 1000.0,
    poll_interval: float = 30.0,
    exchange: str = "paper",
    dry_run: bool = False,
    api_key: str | None = None,
) -> None:
    """Auto-fade a bad wallet by reverse-copying its trades.

    Scores the wallet first. If composite_score < threshold, starts
    an inverse copy-trade loop. If the wallet isn't bad enough, logs
    a message and returns.

    Args:
        wallet: Wallet address to evaluate and potentially fade.
        threshold: Composite score threshold (default -0.3).
        size_scale: Size multiplier for faded trades.
        max_position: Max USDC exposure per market.
        poll_interval: Seconds between polls.
        exchange: Exchange backend ("paper" or "polymarket").
        dry_run: Log without submitting orders.
        api_key: Horizon API key.
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    if not logging.root.handlers:
        logging.basicConfig(level=logging.INFO, format="%(message)s")

    score = score_wallet(wallet)
    logger.info(
        "[reverse_copy] Wallet %s score: %.4f (threshold: %.2f)",
        wallet[:10], score.composite_score, threshold,
    )

    if score.composite_score >= threshold:
        logger.info(
            "[reverse_copy] Wallet not bad enough to fade (score %.4f >= %.2f)",
            score.composite_score, threshold,
        )
        return

    logger.info(
        "[reverse_copy] Fading wallet %s (score %.4f < %.2f)",
        wallet[:10], score.composite_score, threshold,
    )

    from horizon.copy_trader import copy_trades
    copy_trades(
        wallet=wallet,
        size_scale=size_scale,
        max_position=max_position,
        poll_interval=poll_interval,
        exchange=exchange,
        inverse=True,
        dry_run=dry_run,
        api_key=api_key,
    )


# ---------------------------------------------------------------------------
# Pipeline factories
# ---------------------------------------------------------------------------


def reverse_copier(
    wallet: str,
    threshold: float = -0.3,
    size_scale: float = 0.5,
    max_position: float = 1000.0,
    min_trade_usdc: float = 1.0,
    max_slippage: float = 0.02,
    dry_run: bool = False,
) -> Callable[..., None]:
    """Create a pipeline function that reverse-copies a bad wallet.

    Scores the wallet on first call. If the score is below threshold,
    wraps copy_trader with inverse=True. Otherwise returns None each cycle.

    Args:
        wallet: Wallet address to evaluate.
        threshold: Composite score threshold.
        size_scale: Size multiplier.
        max_position: Max USDC per market.
        min_trade_usdc: Min trade size.
        max_slippage: Max slippage.
        dry_run: Dry run mode.

    Returns:
        Pipeline function: (Context) -> None
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    inner_fn: Callable[..., None] | None = None
    evaluated = False

    def _reverse_copy(ctx: Any) -> None:
        nonlocal inner_fn, evaluated

        if not evaluated:
            evaluated = True
            sc = score_wallet(wallet)
            logger.info(
                "[reverse_copier] Wallet %s score: %.4f (threshold: %.2f)",
                wallet[:10], sc.composite_score, threshold,
            )
            if sc.composite_score < threshold:
                from horizon.copy_trader import copy_trader
                inner_fn = copy_trader(
                    wallet=wallet,
                    size_scale=size_scale,
                    max_position=max_position,
                    min_trade_usdc=min_trade_usdc,
                    inverse=True,
                    max_slippage=max_slippage,
                    dry_run=dry_run,
                )
                logger.info("[reverse_copier] Fading wallet %s", wallet[:10])
            else:
                logger.info("[reverse_copier] Wallet not bad enough to fade")

        if inner_fn is not None:
            inner_fn(ctx)

    _reverse_copy.__name__ = "reverse_copier"
    return _reverse_copy


def bot_scanner(
    min_trades: int = 5,
) -> Callable[..., None]:
    """Create a pipeline function that scans markets for bots.

    Scans each market in ctx.market_ids and stores results in
    ctx.params["bot_scan_results"].

    Args:
        min_trades: Min trades per wallet to classify.

    Returns:
        Pipeline function: (Context) -> None
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    def _scan(ctx: Any) -> None:
        market_ids = getattr(ctx, "market_ids", [])
        all_results: list[BotDetection] = []
        for mkt in market_ids:
            try:
                detections = scan_bots(mkt, min_trades=min_trades)
                all_results.extend(detections)
            except Exception as e:
                logger.warning("[bot_scanner] Failed to scan %s: %s", mkt, e)
        ctx.params["bot_scan_results"] = all_results

    _scan.__name__ = "bot_scanner"
    return _scan
