"""Deep behavioral profiling and auto-profit for Polymarket wallets (Polymarket-only -- requires on-chain wallet data).

Analyzes *how* a wallet trades across four dimensions (temporal, market
selection, order flow, edge) and generates concrete strategies to profit
from its behavior, with auto-execution via ``hunt()`` and ``hunter()``.

Usage:
    profile = hz.profile_wallet("0x1234...")
    hz.hunt("0x1234...")                          # one-shot
    hz.run(pipeline=[hz.hunter("0x1234...")])     # pipeline
"""

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_wallet_trades, get_wallet_positions
from horizon.wallet_intel import WalletScore, WalletAnalysis, score_wallet, analyze_wallet, _cv

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


# ---------------------------------------------------------------------------
# Sub-dataclasses
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class HourDistribution:
    """Trades per hour-of-day (0-23)."""

    counts: dict[int, int]  # hour -> trade count
    peak_hour: int
    entropy: float


@dataclass(frozen=True)
class DayDistribution:
    """Trades per day-of-week (0=Mon, 6=Sun)."""

    counts: dict[int, int]
    peak_day: int
    weekend_ratio: float  # fraction of trades on Sat/Sun


@dataclass(frozen=True)
class SessionClassification:
    """Dominant trading session."""

    session: str  # "us", "asia", "europe", "round_the_clock"
    us_pct: float
    asia_pct: float
    europe_pct: float


@dataclass(frozen=True)
class BurstAnalysis:
    """Trade burstiness detection."""

    burstiness: float  # B = (sigma - mu) / (sigma + mu), range [-1, 1]
    is_bursty: bool  # B > 0.2
    avg_cluster_size: float


@dataclass(frozen=True)
class MarketCategory:
    """Performance in a market category."""

    name: str
    trade_count: int
    win_rate: float


@dataclass(frozen=True)
class PositionBuildingStyle:
    """How the wallet enters positions."""

    style: str  # "single_entry", "scaling_in", "dca"
    avg_entries_per_market: float


@dataclass(frozen=True)
class ExitStyle:
    """How the wallet exits positions."""

    style: str  # "single_exit", "scaling_out"
    avg_exits_per_market: float


# ---------------------------------------------------------------------------
# Profile dataclasses
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class TemporalProfile:
    """When does the wallet trade?"""

    hour_distribution: HourDistribution
    day_distribution: DayDistribution
    session: SessionClassification
    burst: BurstAnalysis
    active_days_pct: float  # fraction of days with >= 1 trade


@dataclass(frozen=True)
class MarketSelectionProfile:
    """What does the wallet trade?"""

    categories: list[MarketCategory]
    best_category: str
    worst_category: str
    herfindahl: float  # concentration index
    diversity_score: float  # 1 - herfindahl
    market_count: int
    early_entrant_score: float  # fraction buying near opening prices


@dataclass(frozen=True)
class OrderFlowProfile:
    """How does the wallet enter and exit?"""

    building_style: PositionBuildingStyle
    exit_style: ExitStyle
    avg_hold_hours: float
    hold_cv: float  # coefficient of variation of hold times
    size_trend: str  # "increasing", "decreasing", "stable"
    size_trend_slope: float
    dip_buyer_score: float  # 0-1
    rip_seller_score: float  # 0-1


@dataclass(frozen=True)
class EdgeDetection:
    """Is the wallet good?"""

    information_score: float  # -1 to +1
    timing_quality: float  # avg (current_price - entry) / entry for buys
    win_rate_by_size: dict[str, float]  # "small" / "medium" / "large"
    streak_type: str  # "streaky", "mean_reverting", "random"
    streak_autocorrelation: float  # lag-1 autocorrelation
    edge_bps: float  # estimated edge in basis points
    confidence: float  # based on sample size


# ---------------------------------------------------------------------------
# Strategy dataclass
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class ProfitStrategy:
    """A concrete strategy to profit from a wallet's behavior."""

    name: str
    description: str
    mechanism: str
    expected_edge: str  # "high", "medium", "low"
    risk_level: str  # "high", "medium", "low"
    confidence: float  # 0-1
    impl_type: str  # "copy", "fade", "front_run", "counter_trade", "session_trade"
    params: dict[str, Any]


# ---------------------------------------------------------------------------
# Full profile
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class FullWalletProfile:
    """Complete behavioral profile with actionable strategies."""

    wallet: str
    score: WalletScore
    analysis: WalletAnalysis
    temporal: TemporalProfile
    market_selection: MarketSelectionProfile
    order_flow: OrderFlowProfile
    edge: EdgeDetection
    strategies: list[ProfitStrategy]
    best_strategy: ProfitStrategy | None
    data_quality: str  # "high" (>200 trades), "medium" (50-200), "low" (<50)


# ---------------------------------------------------------------------------
# Category inference
# ---------------------------------------------------------------------------

_CATEGORY_KEYWORDS: dict[str, list[str]] = {
    "politics": [
        "president", "election", "trump", "biden", "senate", "congress",
        "democrat", "republican", "governor", "vote", "poll", "primary",
        "party", "political", "inaugur", "impeach", "cabinet",
    ],
    "crypto": [
        "bitcoin", "btc", "ethereum", "eth", "crypto", "token", "defi",
        "nft", "blockchain", "solana", "sol", "binance", "coinbase",
        "altcoin", "memecoin", "halving",
    ],
    "sports": [
        "nfl", "nba", "mlb", "nhl", "soccer", "football", "basketball",
        "baseball", "hockey", "tennis", "ufc", "mma", "championship",
        "playoff", "super bowl", "world cup", "olympics", "mvp",
    ],
    "economics": [
        "fed", "interest rate", "inflation", "gdp", "recession",
        "unemployment", "cpi", "fomc", "treasury", "tariff", "trade war",
        "debt ceiling", "s&p", "dow", "nasdaq", "stock",
    ],
    "entertainment": [
        "oscar", "grammy", "emmy", "movie", "film", "celebrity",
        "tv show", "netflix", "spotify", "album", "box office",
        "streaming", "award",
    ],
    "science": [
        "ai", "artificial intelligence", "climate", "space", "nasa",
        "vaccine", "covid", "fda", "drug", "trial", "research",
        "technology", "patent", "quantum",
    ],
}


def _categorize_market(title: str) -> str:
    """Infer category from market title via keyword matching."""
    lower = title.lower()
    scores: dict[str, int] = defaultdict(int)
    for cat, keywords in _CATEGORY_KEYWORDS.items():
        for kw in keywords:
            if kw in lower:
                scores[cat] += 1
    if not scores:
        return "other"
    return max(scores, key=lambda k: scores[k])


# ---------------------------------------------------------------------------
# Shannon entropy
# ---------------------------------------------------------------------------


def _shannon_entropy(counts: dict[Any, int]) -> float:
    """Compute Shannon entropy of a count distribution."""
    total = sum(counts.values())
    if total == 0:
        return 0.0
    entropy = 0.0
    for c in counts.values():
        if c > 0:
            p = c / total
            entropy -= p * math.log2(p)
    return entropy


# ---------------------------------------------------------------------------
# Linear regression slope (simple)
# ---------------------------------------------------------------------------


def _linreg_slope(values: list[float]) -> float:
    """Compute OLS slope for y=values, x=0..n-1."""
    n = len(values)
    if n < 2:
        return 0.0
    x_mean = (n - 1) / 2.0
    y_mean = sum(values) / n
    num = 0.0
    den = 0.0
    for i, y in enumerate(values):
        dx = i - x_mean
        num += dx * (y - y_mean)
        den += dx * dx
    if den == 0:
        return 0.0
    return num / den


# ---------------------------------------------------------------------------
# Lag-1 autocorrelation
# ---------------------------------------------------------------------------


def _lag1_autocorrelation(values: list[float]) -> float:
    """Compute lag-1 autocorrelation of a binary win/loss sequence."""
    n = len(values)
    if n < 3:
        return 0.0
    mean = sum(values) / n
    var = sum((v - mean) ** 2 for v in values) / n
    if var == 0:
        return 0.0
    cov = sum((values[i] - mean) * (values[i + 1] - mean) for i in range(n - 1)) / (n - 1)
    return cov / var


# ---------------------------------------------------------------------------
# Temporal analysis
# ---------------------------------------------------------------------------


def _analyze_temporal(trades: list[Trade]) -> TemporalProfile:
    """Analyze when the wallet trades."""
    import datetime

    hour_counts: dict[int, int] = defaultdict(int)
    day_counts: dict[int, int] = defaultdict(int)
    unique_days: set[str] = set()

    for t in trades:
        if t.timestamp <= 0:
            continue
        dt = datetime.datetime.fromtimestamp(t.timestamp, tz=datetime.timezone.utc)
        hour_counts[dt.hour] += 1
        day_counts[dt.weekday()] += 1
        unique_days.add(dt.strftime("%Y-%m-%d"))

    # Hour distribution
    peak_hour = max(hour_counts, key=lambda h: hour_counts[h]) if hour_counts else 0
    hour_entropy = _shannon_entropy(hour_counts)
    hour_dist = HourDistribution(counts=dict(hour_counts), peak_hour=peak_hour, entropy=hour_entropy)

    # Day distribution
    peak_day = max(day_counts, key=lambda d: day_counts[d]) if day_counts else 0
    total_trades = sum(day_counts.values()) or 1
    weekend_trades = day_counts.get(5, 0) + day_counts.get(6, 0)
    weekend_ratio = weekend_trades / total_trades
    day_dist = DayDistribution(counts=dict(day_counts), peak_day=peak_day, weekend_ratio=round(weekend_ratio, 4))

    # Session classification (UTC hours)
    # US: 13-21, Asia: 0-8, Europe: 7-16
    us_count = sum(hour_counts.get(h, 0) for h in range(13, 22))
    asia_count = sum(hour_counts.get(h, 0) for h in range(0, 9))
    europe_count = sum(hour_counts.get(h, 0) for h in range(7, 17))
    total_h = sum(hour_counts.values()) or 1
    us_pct = us_count / total_h
    asia_pct = asia_count / total_h
    europe_pct = europe_count / total_h

    dominant = "round_the_clock"
    if us_pct > 0.6:
        dominant = "us"
    elif asia_pct > 0.6:
        dominant = "asia"
    elif europe_pct > 0.6:
        dominant = "europe"
    session = SessionClassification(
        session=dominant,
        us_pct=round(us_pct, 4),
        asia_pct=round(asia_pct, 4),
        europe_pct=round(europe_pct, 4),
    )

    # Burst analysis
    timestamps = sorted(t.timestamp for t in trades if t.timestamp > 0)
    intervals = [timestamps[i + 1] - timestamps[i] for i in range(len(timestamps) - 1) if timestamps[i + 1] > timestamps[i]]
    if intervals:
        mu = sum(intervals) / len(intervals)
        sigma = math.sqrt(sum((x - mu) ** 2 for x in intervals) / len(intervals))
        denom = sigma + mu
        burstiness = (sigma - mu) / denom if denom > 0 else 0.0
    else:
        burstiness = 0.0

    # Avg cluster size: count trades within 60s of each other
    cluster_sizes: list[int] = []
    if timestamps:
        current_cluster = 1
        for i in range(1, len(timestamps)):
            if timestamps[i] - timestamps[i - 1] < 60:
                current_cluster += 1
            else:
                cluster_sizes.append(current_cluster)
                current_cluster = 1
        cluster_sizes.append(current_cluster)
    avg_cluster = sum(cluster_sizes) / len(cluster_sizes) if cluster_sizes else 1.0

    burst = BurstAnalysis(
        burstiness=round(burstiness, 4),
        is_bursty=burstiness > 0.2,
        avg_cluster_size=round(avg_cluster, 2),
    )

    # Active days percentage
    if timestamps and len(timestamps) >= 2:
        span_days = max(1, (timestamps[-1] - timestamps[0]) / 86400)
        active_pct = len(unique_days) / span_days
    else:
        active_pct = 1.0 if unique_days else 0.0

    return TemporalProfile(
        hour_distribution=hour_dist,
        day_distribution=day_dist,
        session=session,
        burst=burst,
        active_days_pct=round(min(1.0, active_pct), 4),
    )


# ---------------------------------------------------------------------------
# Market selection analysis
# ---------------------------------------------------------------------------


def _analyze_market_selection(
    trades: list[Trade],
    positions: list[WalletPosition],
) -> MarketSelectionProfile:
    """Analyze what the wallet trades."""
    # Categorize trades
    market_categories: dict[str, str] = {}
    for t in trades:
        if t.condition_id not in market_categories:
            market_categories[t.condition_id] = _categorize_market(t.market_title)

    # Count trades per category
    cat_trades: dict[str, list[Trade]] = defaultdict(list)
    for t in trades:
        cat = market_categories.get(t.condition_id, "other")
        cat_trades[cat].append(t)

    # Win rates per category (use positions)
    pos_by_market: dict[str, WalletPosition] = {}
    for p in positions:
        pos_by_market[p.condition_id] = p

    cat_wins: dict[str, int] = defaultdict(int)
    cat_total: dict[str, int] = defaultdict(int)
    for cid, cat in market_categories.items():
        if cid in pos_by_market:
            cat_total[cat] += 1
            if pos_by_market[cid].pnl > 0:
                cat_wins[cat] += 1

    categories: list[MarketCategory] = []
    for cat, trade_list in cat_trades.items():
        total = cat_total.get(cat, 0)
        wins = cat_wins.get(cat, 0)
        wr = wins / total if total > 0 else 0.0
        categories.append(MarketCategory(name=cat, trade_count=len(trade_list), win_rate=round(wr, 4)))

    categories.sort(key=lambda c: c.trade_count, reverse=True)

    # Best / worst by win rate (min 3 positions)
    with_data = [c for c in categories if cat_total.get(c.name, 0) >= 3]
    best_cat = max(with_data, key=lambda c: c.win_rate).name if with_data else (categories[0].name if categories else "other")
    worst_cat = min(with_data, key=lambda c: c.win_rate).name if with_data else (categories[-1].name if categories else "other")

    # Concentration (Herfindahl by market)
    market_counts: dict[str, int] = defaultdict(int)
    for t in trades:
        market_counts[t.condition_id] += 1
    total_t = len(trades) or 1
    shares = [c / total_t for c in market_counts.values()]
    herfindahl = sum(s ** 2 for s in shares) if shares else 0.0

    # Early entrant: buy price near 0.5 (opening) - fraction of buys at 0.45-0.55
    buys_near_open = sum(1 for t in trades if t.side.upper() == "BUY" and 0.45 <= t.price <= 0.55)
    total_buys = sum(1 for t in trades if t.side.upper() == "BUY") or 1
    early_score = buys_near_open / total_buys

    return MarketSelectionProfile(
        categories=categories,
        best_category=best_cat,
        worst_category=worst_cat,
        herfindahl=round(herfindahl, 4),
        diversity_score=round(1.0 - herfindahl, 4),
        market_count=len(market_counts),
        early_entrant_score=round(early_score, 4),
    )


# ---------------------------------------------------------------------------
# Order flow analysis
# ---------------------------------------------------------------------------


def _analyze_order_flow(trades: list[Trade]) -> OrderFlowProfile:
    """Analyze how the wallet enters and exits."""
    # Group by market
    by_market: dict[str, list[Trade]] = defaultdict(list)
    for t in trades:
        by_market[t.condition_id].append(t)

    # Building style: count buys per market
    buy_counts: list[int] = []
    sell_counts: list[int] = []
    hold_times: list[float] = []

    for cid, mkt_trades in by_market.items():
        sorted_t = sorted(mkt_trades, key=lambda x: x.timestamp)
        buys = [t for t in sorted_t if t.side.upper() == "BUY"]
        sells = [t for t in sorted_t if t.side.upper() == "SELL"]
        if buys:
            buy_counts.append(len(buys))
        if sells:
            sell_counts.append(len(sells))
        # Hold time estimate: first buy to first sell
        if buys and sells and sells[0].timestamp > buys[0].timestamp:
            hold_hours = (sells[0].timestamp - buys[0].timestamp) / 3600
            hold_times.append(hold_hours)

    avg_buys = sum(buy_counts) / len(buy_counts) if buy_counts else 1.0
    if avg_buys <= 1.2:
        build_style = "single_entry"
    elif avg_buys <= 3.0:
        build_style = "scaling_in"
    else:
        build_style = "dca"

    avg_sells = sum(sell_counts) / len(sell_counts) if sell_counts else 1.0
    exit_s = "single_exit" if avg_sells <= 1.5 else "scaling_out"

    avg_hold = sum(hold_times) / len(hold_times) if hold_times else 0.0
    hold_cv_val = _cv(hold_times) if hold_times else float("inf")
    if hold_cv_val == float("inf") or math.isnan(hold_cv_val):
        hold_cv_val = 0.0

    # Size trend: linear regression on chronological trade sizes
    sizes = [t.usdc_size for t in sorted(trades, key=lambda x: x.timestamp) if t.usdc_size > 0]
    slope = _linreg_slope(sizes) if len(sizes) >= 3 else 0.0
    avg_size = sum(sizes) / len(sizes) if sizes else 1.0
    norm_slope = slope / avg_size if avg_size > 0 else 0.0
    if norm_slope > 0.01:
        size_trend = "increasing"
    elif norm_slope < -0.01:
        size_trend = "decreasing"
    else:
        size_trend = "stable"

    # Dip buyer score: fraction of buys where price < avg market price
    buy_trades = [t for t in trades if t.side.upper() == "BUY" and t.price > 0]
    if buy_trades:
        avg_buy_price_by_market: dict[str, float] = {}
        for cid, mkt_trades in by_market.items():
            prices = [t.price for t in mkt_trades if t.price > 0]
            if prices:
                avg_buy_price_by_market[cid] = sum(prices) / len(prices)
        dip_buys = sum(
            1 for t in buy_trades
            if t.condition_id in avg_buy_price_by_market
            and t.price < avg_buy_price_by_market[t.condition_id]
        )
        dip_score = dip_buys / len(buy_trades)
    else:
        dip_score = 0.0

    # Rip seller score: fraction of sells where price > avg market price
    sell_trades = [t for t in trades if t.side.upper() == "SELL" and t.price > 0]
    if sell_trades:
        avg_price_by_market: dict[str, float] = {}
        for cid, mkt_trades in by_market.items():
            prices = [t.price for t in mkt_trades if t.price > 0]
            if prices:
                avg_price_by_market[cid] = sum(prices) / len(prices)
        rip_sells = sum(
            1 for t in sell_trades
            if t.condition_id in avg_price_by_market
            and t.price > avg_price_by_market[t.condition_id]
        )
        rip_score = rip_sells / len(sell_trades)
    else:
        rip_score = 0.0

    return OrderFlowProfile(
        building_style=PositionBuildingStyle(style=build_style, avg_entries_per_market=round(avg_buys, 2)),
        exit_style=ExitStyle(style=exit_s, avg_exits_per_market=round(avg_sells, 2)),
        avg_hold_hours=round(avg_hold, 2),
        hold_cv=round(hold_cv_val, 4),
        size_trend=size_trend,
        size_trend_slope=round(norm_slope, 6),
        dip_buyer_score=round(dip_score, 4),
        rip_seller_score=round(rip_score, 4),
    )


# ---------------------------------------------------------------------------
# Edge detection
# ---------------------------------------------------------------------------


def _detect_edge(
    trades: list[Trade],
    positions: list[WalletPosition],
    score: WalletScore,
) -> EdgeDetection:
    """Detect if the wallet has an exploitable edge."""
    # Information score: for each buy, did price move up after?
    # Proxy: use positions - if current_price > avg_price for buys, that's informative
    info_scores: list[float] = []
    for p in positions:
        if p.avg_price > 0 and p.current_price > 0:
            if p.outcome.lower() == "yes":
                move = (p.current_price - p.avg_price) / p.avg_price
            else:
                move = (p.avg_price - p.current_price) / p.avg_price
            info_scores.append(move)

    if info_scores:
        raw_info = sum(info_scores) / len(info_scores)
        information_score = math.tanh(raw_info * 5)  # scale and bound
    else:
        information_score = 0.0

    # Timing quality: average (current - entry) / entry for winning positions
    quality_vals: list[float] = []
    for p in positions:
        if p.avg_price > 0:
            quality_vals.append((p.current_price - p.avg_price) / p.avg_price)
    timing_quality = sum(quality_vals) / len(quality_vals) if quality_vals else 0.0

    # Win rate by size bucket
    sizes = [t.usdc_size for t in trades if t.usdc_size > 0]
    if sizes:
        sorted_sizes = sorted(sizes)
        p33 = sorted_sizes[len(sorted_sizes) // 3] if len(sorted_sizes) >= 3 else sorted_sizes[0]
        p66 = sorted_sizes[2 * len(sorted_sizes) // 3] if len(sorted_sizes) >= 3 else sorted_sizes[-1]
    else:
        p33, p66 = 10.0, 100.0

    # Map trades to positions for win/loss
    pos_pnl: dict[str, float] = {p.condition_id: p.pnl for p in positions}
    bucket_wins: dict[str, int] = {"small": 0, "medium": 0, "large": 0}
    bucket_total: dict[str, int] = {"small": 0, "medium": 0, "large": 0}
    for t in trades:
        if t.side.upper() != "BUY":
            continue
        if t.usdc_size <= p33:
            bucket = "small"
        elif t.usdc_size <= p66:
            bucket = "medium"
        else:
            bucket = "large"
        bucket_total[bucket] += 1
        if pos_pnl.get(t.condition_id, 0) > 0:
            bucket_wins[bucket] += 1

    win_rate_by_size = {}
    for bucket in ("small", "medium", "large"):
        if bucket_total[bucket] > 0:
            win_rate_by_size[bucket] = round(bucket_wins[bucket] / bucket_total[bucket], 4)
        else:
            win_rate_by_size[bucket] = 0.0

    # Streak analysis: lag-1 autocorrelation of win/loss sequence
    outcomes: list[float] = []
    for p in positions:
        outcomes.append(1.0 if p.pnl > 0 else 0.0)
    autocorr = _lag1_autocorrelation(outcomes) if len(outcomes) >= 3 else 0.0
    if autocorr > 0.15:
        streak_type = "streaky"
    elif autocorr < -0.15:
        streak_type = "mean_reverting"
    else:
        streak_type = "random"

    # Edge in bps
    edge_bps = score.avg_pnl_pct * 100  # pnl_pct is a fraction, * 100 = bps

    # Confidence based on sample size
    n = score.position_count
    if n >= 200:
        confidence = 0.9
    elif n >= 100:
        confidence = 0.7
    elif n >= 50:
        confidence = 0.5
    elif n >= 20:
        confidence = 0.3
    else:
        confidence = 0.1

    return EdgeDetection(
        information_score=round(information_score, 4),
        timing_quality=round(timing_quality, 4),
        win_rate_by_size=win_rate_by_size,
        streak_type=streak_type,
        streak_autocorrelation=round(autocorr, 4),
        edge_bps=round(edge_bps, 2),
        confidence=confidence,
    )


# ---------------------------------------------------------------------------
# Strategy generation
# ---------------------------------------------------------------------------


def _generate_strategies(
    score: WalletScore,
    analysis: WalletAnalysis,
    temporal: TemporalProfile,
    market_selection: MarketSelectionProfile,
    order_flow: OrderFlowProfile,
    edge: EdgeDetection,
) -> list[ProfitStrategy]:
    """Generate profit strategies based on the profile."""
    strategies: list[ProfitStrategy] = []
    composite = score.composite_score

    # 1. fade_loser
    if composite < -0.3:
        strategies.append(ProfitStrategy(
            name="fade_loser",
            description="Inverse-copy this losing wallet's trades.",
            mechanism="Take the opposite side of every trade this wallet makes.",
            expected_edge="high",
            risk_level="medium",
            confidence=min(1.0, abs(composite)),
            impl_type="fade",
            params={"inverse": True, "size_scale": 0.5, "poll_interval": 30.0},
        ))

    # 2. copy_winner
    if composite > 0.3 and edge.information_score > 0.15:
        strategies.append(ProfitStrategy(
            name="copy_winner",
            description="Direct-copy this winning wallet's trades.",
            mechanism="Mirror every trade with matching side and sizing.",
            expected_edge="high",
            risk_level="low",
            confidence=min(1.0, edge.confidence),
            impl_type="copy",
            params={"inverse": False, "size_scale": 0.5, "poll_interval": 15.0},
        ))

    # 3. session_pre_position
    if temporal.burst.is_bursty and temporal.session.session != "round_the_clock":
        sess = temporal.session
        dominant_pct = max(sess.us_pct, sess.asia_pct, sess.europe_pct)
        if dominant_pct > 0.6:
            strategies.append(ProfitStrategy(
                name="session_pre_position",
                description=f"Pre-position before the wallet's {temporal.session.session} trading session.",
                mechanism="Enter positions 1-2 hours before the wallet's peak trading hours.",
                expected_edge="medium",
                risk_level="medium",
                confidence=round(dominant_pct * 0.8, 4),
                impl_type="session_trade",
                params={
                    "session": temporal.session.session,
                    "peak_hour": temporal.hour_distribution.peak_hour,
                    "pre_hours": 2,
                },
            ))

    # 4. front_run_timing
    timing_pattern = next((p for p in analysis.patterns if p.name == "timing_regularity"), None)
    if timing_pattern and timing_pattern.value < 0.3 and timing_pattern.value != float("inf"):
        strategies.append(ProfitStrategy(
            name="front_run_timing",
            description="Exploit predictable trade timing with tight polling.",
            mechanism="Poll aggressively and copy with minimal delay.",
            expected_edge="medium",
            risk_level="high",
            confidence=round(1.0 - timing_pattern.value, 4),
            impl_type="front_run",
            params={"inverse": False, "size_scale": 0.3, "poll_interval": 5.0},
        ))

    # 5. stop_hunt
    stop_pattern = next((p for p in analysis.patterns if p.name == "stop_levels"), None)
    if stop_pattern and stop_pattern.exploitable:
        strategies.append(ProfitStrategy(
            name="stop_hunt",
            description="Counter-trade near the wallet's predictable stop levels.",
            mechanism="Place orders near detected stop clusters to profit from forced exits.",
            expected_edge="medium",
            risk_level="high",
            confidence=round(stop_pattern.value, 4),
            impl_type="counter_trade",
            params={"stop_density": stop_pattern.value},
        ))

    # 6. fade_direction
    dir_pattern = next((p for p in analysis.patterns if p.name == "directional_bias"), None)
    if dir_pattern and dir_pattern.exploitable and composite < 0:
        direction = "buyer" if dir_pattern.value > 0.5 else "seller"
        strategies.append(ProfitStrategy(
            name="fade_direction",
            description=f"Fade this wallet's {direction} bias.",
            mechanism=f"Take the opposite side when they trade in their dominant direction.",
            expected_edge="medium",
            risk_level="medium",
            confidence=round(abs(dir_pattern.value - 0.5) * 2, 4),
            impl_type="fade",
            params={"inverse": True, "direction_bias": dir_pattern.value},
        ))

    # 7. copy_best_category
    best_cat_data = next((c for c in market_selection.categories if c.name == market_selection.best_category), None)
    if best_cat_data and best_cat_data.win_rate > 0.6 and best_cat_data.trade_count >= 5:
        strategies.append(ProfitStrategy(
            name="copy_best_category",
            description=f"Copy only in their best category: {market_selection.best_category}.",
            mechanism=f"Mirror trades only in {market_selection.best_category} markets where they have a {best_cat_data.win_rate:.0%} win rate.",
            expected_edge="high",
            risk_level="low",
            confidence=round(min(1.0, best_cat_data.win_rate), 4),
            impl_type="copy",
            params={
                "category_filter": market_selection.best_category,
                "inverse": False,
                "size_scale": 0.5,
            },
        ))

    # 8. concentration_exploit
    if market_selection.herfindahl > 0.3 and composite < 0:
        strategies.append(ProfitStrategy(
            name="concentration_exploit",
            description="Fade in their concentrated markets.",
            mechanism="Take opposite positions in the markets where they're most concentrated.",
            expected_edge="medium",
            risk_level="medium",
            confidence=round(market_selection.herfindahl * abs(composite), 4),
            impl_type="fade",
            params={"inverse": True, "herfindahl": market_selection.herfindahl},
        ))

    # 9. copy_dip_buyer
    if order_flow.dip_buyer_score > 0.6 and edge.edge_bps > 0:
        strategies.append(ProfitStrategy(
            name="copy_dip_buyer",
            description="Copy their dip-buying behavior.",
            mechanism="Mirror their buy trades that occur after price drops.",
            expected_edge="medium",
            risk_level="low",
            confidence=round(order_flow.dip_buyer_score * edge.confidence, 4),
            impl_type="copy",
            params={"inverse": False, "dip_buyer_score": order_flow.dip_buyer_score},
        ))

    # 10. timing_exploit
    if temporal.hour_distribution.entropy < 2.0:
        strategies.append(ProfitStrategy(
            name="timing_exploit",
            description="Pre-position before their predictable peak hours.",
            mechanism=f"Enter markets 1 hour before hour {temporal.hour_distribution.peak_hour} UTC.",
            expected_edge="low",
            risk_level="low",
            confidence=round(max(0.1, 1.0 - temporal.hour_distribution.entropy / 4.0), 4),
            impl_type="session_trade",
            params={
                "peak_hour": temporal.hour_distribution.peak_hour,
                "hour_entropy": temporal.hour_distribution.entropy,
            },
        ))

    return strategies


# ---------------------------------------------------------------------------
# Main profiler
# ---------------------------------------------------------------------------


def profile_wallet(address: str, trade_limit: int = 500) -> FullWalletProfile:
    """Generate a complete behavioral profile for a wallet.

    Args:
        address: Wallet address (0x...).
        trade_limit: Max trades to fetch for analysis.

    Returns:
        FullWalletProfile with all analysis dimensions and strategies.
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    # Fetch data
    trades = get_wallet_trades(address, limit=trade_limit)
    positions = get_wallet_positions(address, limit=500)
    score = score_wallet(address, limit=500)
    analysis = analyze_wallet(address, trade_limit=trade_limit)

    # Data quality
    n_trades = len(trades)
    if n_trades >= 200:
        data_quality = "high"
    elif n_trades >= 50:
        data_quality = "medium"
    else:
        data_quality = "low"

    # Run analysis pipelines
    temporal = _analyze_temporal(trades)
    market_sel = _analyze_market_selection(trades, positions)
    order_fl = _analyze_order_flow(trades)
    edge_det = _detect_edge(trades, positions, score)

    # Generate strategies
    strategies = _generate_strategies(score, analysis, temporal, market_sel, order_fl, edge_det)

    # Pick best strategy (highest confidence)
    best = max(strategies, key=lambda s: s.confidence) if strategies else None

    return FullWalletProfile(
        wallet=address,
        score=score,
        analysis=analysis,
        temporal=temporal,
        market_selection=market_sel,
        order_flow=order_fl,
        edge=edge_det,
        strategies=strategies,
        best_strategy=best,
        data_quality=data_quality,
    )


# ---------------------------------------------------------------------------
# Auto-execution: hunt (standalone)
# ---------------------------------------------------------------------------


def hunt(
    wallet: str,
    trade_limit: int = 500,
    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:
    """Profile a wallet and auto-execute the best strategy.

    Blocking function: profiles the wallet, picks the best strategy,
    then launches the appropriate copy/fade loop.

    Args:
        wallet: Wallet address (0x...).
        trade_limit: Max trades for profiling.
        size_scale: Base size multiplier.
        max_position: Max USDC per market.
        poll_interval: Default poll interval.
        exchange: Exchange backend.
        dry_run: Log without submitting.
        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")

    logger.info("[hunt] Profiling wallet %s...", wallet[:10])
    profile = profile_wallet(wallet, trade_limit=trade_limit)

    logger.info("[hunt] Score: %.4f | Data quality: %s | Strategies: %d",
                profile.score.composite_score, profile.data_quality, len(profile.strategies))

    if profile.best_strategy is None:
        logger.info("[hunt] No actionable strategy found for %s", wallet[:10])
        return

    strat = profile.best_strategy
    logger.info("[hunt] Best strategy: %s (confidence: %.2f, edge: %s)",
                strat.name, strat.confidence, strat.expected_edge)

    # Determine params
    inverse = strat.params.get("inverse", False)
    s_scale = strat.params.get("size_scale", size_scale)
    s_poll = strat.params.get("poll_interval", poll_interval)

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


# ---------------------------------------------------------------------------
# Auto-execution: hunter (pipeline)
# ---------------------------------------------------------------------------


def hunter(
    wallet: str,
    trade_limit: int = 500,
    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 profiles and hunts a wallet.

    Profiles on first call, then delegates to ``copy_trader()`` each
    cycle with appropriate params based on the best strategy.

    Args:
        wallet: Wallet address (0x...).
        trade_limit: Max trades for profiling.
        size_scale: Base 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
    profiled = False

    def _hunt(ctx: Any) -> None:
        nonlocal inner_fn, profiled

        if not profiled:
            profiled = True
            logger.info("[hunter] Profiling wallet %s...", wallet[:10])

            try:
                profile = profile_wallet(wallet, trade_limit=trade_limit)
            except Exception as e:
                logger.warning("[hunter] Profiling failed: %s", e)
                return

            logger.info("[hunter] Score: %.4f | Strategies: %d",
                        profile.score.composite_score, len(profile.strategies))

            strat = profile.best_strategy
            if strat is None:
                logger.info("[hunter] No actionable strategy for %s", wallet[:10])
                return

            logger.info("[hunter] Using strategy: %s (confidence: %.2f)",
                        strat.name, strat.confidence)

            inverse = strat.params.get("inverse", False)
            s_scale = strat.params.get("size_scale", size_scale)

            from horizon.copy_trader import copy_trader
            inner_fn = copy_trader(
                wallet=wallet,
                size_scale=s_scale,
                max_position=max_position,
                min_trade_usdc=min_trade_usdc,
                inverse=inverse,
                max_slippage=max_slippage,
                dry_run=dry_run,
            )

            # Store profile in context for inspection
            ctx.params["wallet_profile"] = {
                "wallet": profile.wallet,
                "score": profile.score.composite_score,
                "best_strategy": strat.name,
                "strategy_confidence": strat.confidence,
                "data_quality": profile.data_quality,
            }

        if inner_fn is not None:
            inner_fn(ctx)

    _hunt.__name__ = "hunter"
    return _hunt
