"""Copy-trading module for mirroring Polymarket whale wallets (Polymarket-only -- requires on-chain wallet data).

Provides both standalone mode (``copy_trades()``) and pipeline mode
(``copy_trader()``) for automatically replicating trades from tracked
wallets through the Horizon Engine.

Usage:
    # Standalone (blocking loop):
    hz.copy_trades(wallet="0x1234...", size_scale=0.5, max_position=1000.0)

    # Pipeline mode (inside hz.run):
    hz.run(
        markets=["some-market"],
        pipeline=[hz.copy_trader(wallet="0x...", size_scale=0.5)],
        interval=30.0,
    )
"""

from __future__ import annotations

import logging
import os
import signal
import time
from collections import deque
from dataclasses import dataclass, field
from typing import Any, Callable

from horizon._horizon import Engine, OrderRequest, OrderSide, RiskConfig, Side
from horizon.context import Context
from horizon.exchanges import Polymarket
from horizon.flow import Trade, get_wallet_trades

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

_DEDUP_CAPACITY = 10_000


# ---------------------------------------------------------------------------
# Config & result dataclasses
# ---------------------------------------------------------------------------


@dataclass
class CopyTraderConfig:
    """Configuration for copy-trading."""

    wallets: list[str] = field(default_factory=list)
    size_scale: float = 1.0
    max_position_per_market: float = 1000.0
    min_trade_usdc: float = 1.0
    poll_interval: float = 30.0
    inverse: bool = False
    max_slippage: float = 0.02
    dry_run: bool = False
    dedup_capacity: int = _DEDUP_CAPACITY


@dataclass
class CopyTradeResult:
    """Record of a single copied (or attempted) trade."""

    source_wallet: str
    source_tx_hash: str | None
    market_slug: str
    condition_id: str
    side: str
    order_side: str
    size: float
    price: float
    order_id: str | None = None
    error: str | None = None


# ---------------------------------------------------------------------------
# Internal state
# ---------------------------------------------------------------------------


class _CopyTraderState:
    """Mutable state for dedup, exposure tracking, and watermarks."""

    def __init__(self, capacity: int = _DEDUP_CAPACITY) -> None:
        self._capacity = capacity
        self._seen_deque: deque[str] = deque()
        self._seen_set: set[str] = set()
        self._exposure: dict[str, float] = {}
        self._last_seen_timestamp: dict[str, int] = {}

    # -- dedup ---------------------------------------------------------------

    def _trade_key(self, trade: Trade) -> str:
        if trade.tx_hash:
            return trade.tx_hash
        return f"{trade.wallet}:{trade.condition_id}:{trade.timestamp}:{trade.size}"

    def is_duplicate(self, trade: Trade) -> bool:
        return self._trade_key(trade) in self._seen_set

    def mark_seen(self, trade: Trade) -> None:
        key = self._trade_key(trade)
        if key in self._seen_set:
            return
        self._seen_set.add(key)
        self._seen_deque.append(key)
        while len(self._seen_deque) > self._capacity:
            evicted = self._seen_deque.popleft()
            self._seen_set.discard(evicted)

    # -- position limits -----------------------------------------------------

    def check_position_limit(
        self, condition_id: str, add_usdc: float, max_position: float,
    ) -> float:
        """Return the clamped USDC amount that can be added without exceeding the limit.

        Returns 0.0 if the position is already at or over the limit.
        """
        current = self._exposure.get(condition_id, 0.0)
        remaining = max(0.0, max_position - current)
        return min(add_usdc, remaining)

    def update_exposure(self, condition_id: str, usdc_delta: float) -> None:
        self._exposure[condition_id] = self._exposure.get(condition_id, 0.0) + usdc_delta

    # -- watermark -----------------------------------------------------------

    def is_newer(self, wallet: str, timestamp: int) -> bool:
        return timestamp > self._last_seen_timestamp.get(wallet, 0)

    def update_watermark(self, wallet: str, timestamp: int) -> None:
        prev = self._last_seen_timestamp.get(wallet, 0)
        if timestamp > prev:
            self._last_seen_timestamp[wallet] = timestamp

    # -- seed ----------------------------------------------------------------

    def seed(self, trades: list[Trade]) -> None:
        """Mark existing trades as seen (prevents replaying history)."""
        for t in trades:
            self.mark_seen(t)
            if t.timestamp > 0:
                self.update_watermark(t.wallet, t.timestamp)


# ---------------------------------------------------------------------------
# Side mapping
# ---------------------------------------------------------------------------


def _map_trade_to_order(
    trade: Trade, inverse: bool = False,
) -> tuple[Side, OrderSide]:
    """Map a source trade to (Side, OrderSide).

    | Trade.side | Trade.outcome | -> Side | -> OrderSide |
    |-----------|--------------|--------|-------------|
    | BUY       | Yes          | Yes    | Buy         |
    | BUY       | No           | No     | Buy         |
    | SELL      | Yes          | Yes    | Sell        |
    | SELL      | No           | No     | Sell        |

    Inverse mode flips OrderSide only (Buy<->Sell), Side stays same.
    """
    side = Side.Yes if trade.outcome.lower() == "yes" else Side.No
    order_side = OrderSide.Buy if trade.side.upper() == "BUY" else OrderSide.Sell

    if inverse:
        order_side = OrderSide.Sell if order_side == OrderSide.Buy else OrderSide.Buy

    return side, order_side


# ---------------------------------------------------------------------------
# Core processing
# ---------------------------------------------------------------------------


def _process_trade(
    engine: Engine,
    trade: Trade,
    state: _CopyTraderState,
    config: CopyTraderConfig,
) -> CopyTradeResult | None:
    """Process a single trade: dedup, filter, size, submit.

    Returns a CopyTradeResult if the trade was actionable, None if skipped.
    """
    # Dedup
    if state.is_duplicate(trade):
        return None
    state.mark_seen(trade)

    # Watermark
    if not state.is_newer(trade.wallet, trade.timestamp):
        return None
    state.update_watermark(trade.wallet, trade.timestamp)

    # Filter: min size
    if trade.usdc_size < config.min_trade_usdc:
        return None

    # Filter: valid price
    if trade.price <= 0 or trade.price >= 1:
        return None

    # Side mapping
    side, order_side = _map_trade_to_order(trade, inverse=config.inverse)

    # Size: USDC -> tokens, scaled
    scaled_usdc = trade.usdc_size * config.size_scale

    # Position limit clamping (only for buys / position-increasing)
    if order_side == OrderSide.Buy:
        allowed_usdc = state.check_position_limit(
            trade.condition_id, scaled_usdc, config.max_position_per_market,
        )
        if allowed_usdc <= 0:
            return CopyTradeResult(
                source_wallet=trade.wallet,
                source_tx_hash=trade.tx_hash,
                market_slug=trade.market_slug,
                condition_id=trade.condition_id,
                side="yes" if side == Side.Yes else "no",
                order_side="buy" if order_side == OrderSide.Buy else "sell",
                size=0.0,
                price=trade.price,
                error="position_limit_reached",
            )
        scaled_usdc = allowed_usdc

    token_size = scaled_usdc / trade.price
    if token_size < 0.01:
        return None

    # Price with slippage
    if order_side == OrderSide.Buy:
        price = min(0.99, trade.price + config.max_slippage)
    else:
        price = max(0.01, trade.price - config.max_slippage)

    result = CopyTradeResult(
        source_wallet=trade.wallet,
        source_tx_hash=trade.tx_hash,
        market_slug=trade.market_slug,
        condition_id=trade.condition_id,
        side="yes" if side == Side.Yes else "no",
        order_side="buy" if order_side == OrderSide.Buy else "sell",
        size=round(token_size, 4),
        price=round(price, 4),
    )

    if config.dry_run:
        logger.info(
            "[copy] DRY RUN: %s %s %.2f tokens @ %.4f on %s (source: %s)",
            result.order_side.upper(), result.side, token_size, price,
            trade.market_slug, trade.wallet[:10],
        )
        return result

    # Submit order
    try:
        req = OrderRequest(
            market_id=trade.condition_id,
            side=side,
            order_side=order_side,
            size=round(token_size, 4),
            price=round(price, 4),
            token_id=trade.token_id or "",
        )
        order_id = engine.submit_order(req)
        result.order_id = order_id

        # Track exposure
        if order_side == OrderSide.Buy:
            state.update_exposure(trade.condition_id, scaled_usdc)

        logger.info(
            "[copy] %s %s %.2f tokens @ %.4f on %s -> order %s",
            result.order_side.upper(), result.side, token_size, price,
            trade.market_slug, order_id,
        )
    except Exception as e:
        result.error = str(e)
        logger.warning("[copy] Order failed: %s", e)

    return result


# ---------------------------------------------------------------------------
# Standalone mode
# ---------------------------------------------------------------------------


def copy_trades(
    wallet: str | list[str] | None = None,
    wallets: list[str] | None = None,
    size_scale: float = 1.0,
    max_position: float = 1000.0,
    min_trade_usdc: float = 1.0,
    poll_interval: float = 30.0,
    exchange: str = "paper",
    inverse: bool = False,
    max_slippage: float = 0.02,
    dry_run: bool = False,
    risk: Any = None,
    api_key: str | None = None,
) -> None:
    """Copy trades from Polymarket whale wallet(s).

    Blocking loop that polls wallet trades and submits mirrored orders.

    Args:
        wallet: Single wallet address or list of addresses.
        wallets: List of wallet addresses (alternative to ``wallet``).
        size_scale: Multiplier for trade size (0.5 = half size).
        max_position: Maximum USDC exposure per market.
        min_trade_usdc: Ignore trades smaller than this.
        poll_interval: Seconds between polls.
        exchange: Exchange backend ("paper" or "polymarket").
        inverse: If True, fade the whale (flip buy/sell).
        max_slippage: Max price deviation from source trade.
        dry_run: Log trades without submitting orders.
        risk: Optional Risk or RiskConfig for the engine.
        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")

    # Normalize wallets
    wallet_list: list[str] = []
    if wallets:
        wallet_list = list(wallets)
    elif isinstance(wallet, list):
        wallet_list = wallet
    elif wallet:
        wallet_list = [wallet]
    if not wallet_list:
        raise ValueError("At least one wallet address is required")

    config = CopyTraderConfig(
        wallets=wallet_list,
        size_scale=size_scale,
        max_position_per_market=max_position,
        min_trade_usdc=min_trade_usdc,
        poll_interval=poll_interval,
        inverse=inverse,
        max_slippage=max_slippage,
        dry_run=dry_run,
    )

    # Build risk config
    from horizon.risk import Risk
    risk_config: RiskConfig | None = None
    if isinstance(risk, Risk):
        risk_config = risk.to_config()
    elif isinstance(risk, RiskConfig):
        risk_config = risk

    # Create engine
    horizon_key = api_key or os.environ.get("HORIZON_API_KEY")
    if exchange == "polymarket":
        poly = Polymarket()
        if poly.private_key and not poly.api_key:
            poly.derive_api_credentials()
        engine = Engine(
            risk_config=risk_config,
            exchange_type="polymarket",
            exchange_key=poly.api_key,
            exchange_secret=poly.api_secret,
            exchange_passphrase=poly.api_passphrase,
            clob_url=poly.clob_url,
            private_key=poly.private_key,
            api_key=horizon_key,
        )
    else:
        engine = Engine(risk_config=risk_config, api_key=horizon_key)

    state = _CopyTraderState(capacity=config.dedup_capacity)

    # Seed dedup from existing trades
    logger.info("[copy] Seeding dedup from %d wallet(s)...", len(wallet_list))
    for w in wallet_list:
        try:
            seed_trades = get_wallet_trades(w, limit=100)
            state.seed(seed_trades)
            logger.info("[copy] Seeded %d trades from %s", len(seed_trades), w[:10])
        except Exception as e:
            logger.warning("[copy] Failed to seed from %s: %s", w[:10], e)

    # Signal handling
    running = True

    def handle_signal(sig, frame):
        nonlocal running
        logger.info("[copy] Shutting down...")
        running = False

    signal.signal(signal.SIGINT, handle_signal)
    signal.signal(signal.SIGTERM, handle_signal)

    logger.info("[copy] Starting copy-trader")
    logger.info("[copy] Wallets: %s", [w[:10] + "..." for w in wallet_list])
    logger.info("[copy] Scale: %.2f, Max position: %.0f, Interval: %.0fs",
                size_scale, max_position, poll_interval)
    if inverse:
        logger.info("[copy] INVERSE MODE: fading whale trades")
    if dry_run:
        logger.info("[copy] DRY RUN: no orders will be submitted")

    cycle = 0
    total_copied = 0
    results: list[CopyTradeResult] = []

    while running:
        cycle_start = time.monotonic()
        cycle += 1

        for w in wallet_list:
            try:
                trades = get_wallet_trades(w, limit=50)
            except Exception as e:
                logger.warning("[copy] Poll failed for %s: %s", w[:10], e)
                continue

            for trade in trades:
                result = _process_trade(engine, trade, state, config)
                if result is not None:
                    results.append(result)
                    if result.error is None:
                        total_copied += 1

        # Periodic status
        if cycle % 10 == 0:
            status = engine.status()
            logger.info(
                "[copy] cycle=%d copied=%d orders=%d positions=%d pnl=%.2f",
                cycle, total_copied, status.open_orders,
                status.active_positions, status.total_pnl(),
            )

        elapsed = time.monotonic() - cycle_start
        sleep_time = max(0.0, poll_interval - elapsed)
        if sleep_time > 0 and running:
            time.sleep(sleep_time)

    logger.info("[copy] Stopped. Total trades copied: %d", total_copied)


# ---------------------------------------------------------------------------
# Pipeline mode
# ---------------------------------------------------------------------------


def copy_trader(
    wallet: str | list[str] | None = None,
    wallets: list[str] | None = None,
    size_scale: float = 1.0,
    max_position: float = 1000.0,
    min_trade_usdc: float = 1.0,
    inverse: bool = False,
    max_slippage: float = 0.02,
    dry_run: bool = False,
) -> Callable[[Context], None]:
    """Create a pipeline function that copies trades from tracked wallets.

    Returns a callable for use in ``hz.run(pipeline=[...])``.

    Args:
        wallet: Single wallet address or list of addresses.
        wallets: List of wallet addresses (alternative to ``wallet``).
        size_scale: Multiplier for trade size.
        max_position: Maximum USDC exposure per market.
        min_trade_usdc: Ignore trades smaller than this.
        inverse: If True, fade the whale (flip buy/sell).
        max_slippage: Max price deviation from source trade.
        dry_run: Log trades without submitting orders.

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

    # Normalize wallets
    wallet_list: list[str] = []
    if wallets:
        wallet_list = list(wallets)
    elif isinstance(wallet, list):
        wallet_list = wallet
    elif wallet:
        wallet_list = [wallet]
    if not wallet_list:
        raise ValueError("At least one wallet address is required")

    config = CopyTraderConfig(
        wallets=wallet_list,
        size_scale=size_scale,
        max_position_per_market=max_position,
        min_trade_usdc=min_trade_usdc,
        inverse=inverse,
        max_slippage=max_slippage,
        dry_run=dry_run,
    )

    state = _CopyTraderState(capacity=config.dedup_capacity)
    seeded = False

    def _copy(ctx: Context) -> None:
        nonlocal seeded

        engine = ctx.params.get("engine")
        if engine is None:
            return

        # Seed on first call
        if not seeded:
            for w in wallet_list:
                try:
                    seed_trades = get_wallet_trades(w, limit=100)
                    state.seed(seed_trades)
                    logger.info("[copy] Seeded %d trades from %s", len(seed_trades), w[:10])
                except Exception as e:
                    logger.warning("[copy] Failed to seed from %s: %s", w[:10], e)
            seeded = True

        # Poll and process
        results: list[CopyTradeResult] = ctx.params.get("copy_trade_results", [])

        for w in wallet_list:
            try:
                trades = get_wallet_trades(w, limit=50)
            except Exception as e:
                logger.warning("[copy] Poll failed for %s: %s", w[:10], e)
                continue

            for trade in trades:
                result = _process_trade(engine, trade, state, config)
                if result is not None:
                    results.append(result)

        ctx.params["copy_trade_results"] = results

    _copy.__name__ = "copy_trader"
    return _copy
