"""Composite arbitrage scanner: meta-scanner running all configured methods.

Scores opportunities across methods, routes capital to the best ones.
Ultra tier only.
"""

from __future__ import annotations

import logging
import time
from dataclasses import dataclass, field
from typing import Callable

from horizon.context import Context

from ._types import CompositeArbResult

logger = logging.getLogger(__name__)


@dataclass
class ArbMethodConfig:
    """Configuration for a single arbitrage method in the composite scanner."""

    method: str  # "parity"|"cross_exchange"|"event"|"spread"|"stat"|"mm"|"latency"
    weight: float = 1.0
    max_capital: float = 500.0
    enabled: bool = True
    kwargs: dict = field(default_factory=dict)


def composite_arb(
    methods: list[ArbMethodConfig],
    total_capital: float = 1000.0,
    max_concurrent: int = 3,
    rebalance_interval: float = 60.0,
    auto_execute: bool = False,
    cooldown: float = 5.0,
) -> Callable[[Context], None]:
    """Create a meta-scanner that runs all configured arb methods.

    Each cycle, the composite scanner checks all enabled methods,
    scores opportunities, and routes capital to the best ones.

    Scoring: score = (net_edge / cost) * confidence * weight * (1 - utilization)

    Args:
        methods: List of ArbMethodConfig for each method.
        total_capital: Total capital budget.
        max_concurrent: Maximum concurrent active arb positions.
        rebalance_interval: Seconds between rebalancing.
        auto_execute: If True, execute top opportunities. NOTE: composite
            execution is not yet implemented — individual sub-strategy scanners
            handle execution. This flag currently only tracks capital allocation.
        cooldown: Seconds between executions.

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

    last_rebalance = 0.0
    last_exec_time = 0.0
    active_positions: list[str] = []
    capital_allocated: dict[str, float] = {}

    def _scanner(ctx: Context) -> None:
        nonlocal last_rebalance, last_exec_time

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

        now = time.monotonic()

        # Only rebalance at interval
        if now - last_rebalance < rebalance_interval:
            return None
        last_rebalance = now

        # Collect opportunities from all methods
        opportunities: list[CompositeArbResult] = []

        for mc in methods:
            if not mc.enabled:
                continue

            result = _scan_method(ctx, engine, mc)
            if result is not None:
                opportunities.append(result)

        if not opportunities:
            ctx.params["last_composite_arb"] = []
            return None

        # Sort by score (highest first)
        opportunities.sort(key=lambda x: x.score, reverse=True)

        # Limit to max_concurrent
        top = opportunities[:max_concurrent]

        ctx.params["last_composite_arb"] = top

        if auto_execute and now - last_exec_time >= cooldown:
            remaining_capital = total_capital - sum(capital_allocated.values())
            for opp in top:
                if opp.capital_needed <= remaining_capital:
                    # CompositeArbResult aggregates scored opportunities from
                    # sub-strategies but does not carry full order parameters
                    # (market_id, side, price, size). Actual execution must be
                    # handled by the individual arb scanners (parity, cross_exchange,
                    # event, etc.) that produced the underlying opportunities.
                    logger.warning(
                        "Composite arb: auto_execute is not yet implemented for "
                        "composite opportunities — individual sub-strategy scanners "
                        "should handle execution. method=%s score=%.4f edge=%.4f",
                        opp.method, opp.score, opp.net_edge,
                    )
                    capital_allocated[opp.method] = opp.capital_needed
                    remaining_capital -= opp.capital_needed
            last_exec_time = now

        return None

    _scanner.__name__ = "composite_arb"
    return _scanner


def _scan_method(ctx: Context, engine, mc: ArbMethodConfig) -> CompositeArbResult | None:
    """Scan a single method and return a scored result."""
    method = mc.method
    params = ctx.params

    # Check for recent results from individual scanners
    if method == "parity":
        opp = params.get("last_parity_arb")
        if opp is not None and hasattr(opp, "net_edge") and opp.net_edge > 0:
            cost = getattr(opp, "total_cost", 1.0)
            score = (opp.net_edge / max(cost, 0.01)) * mc.weight
            return CompositeArbResult(
                method="parity", score=score, net_edge=opp.net_edge,
                capital_needed=cost * getattr(opp, "max_size", 50.0),
                details={"market_id": getattr(opp, "market_id", "")},
            )

    elif method == "cross_exchange":
        opp = params.get("last_arb")
        if opp is not None and hasattr(opp, "net_edge") and opp.net_edge > 0:
            cost = getattr(opp, "buy_price", 0.5)
            score = (opp.net_edge / max(cost, 0.01)) * mc.weight
            return CompositeArbResult(
                method="cross_exchange", score=score, net_edge=opp.net_edge,
                capital_needed=cost * getattr(opp, "size", 50.0),
                details={"market_id": getattr(opp, "market_id", "")},
            )

    elif method == "event":
        opp = params.get("last_event_arb")
        if opp is not None and hasattr(opp, "net_edge") and opp.net_edge > 0:
            score = opp.net_edge * mc.weight
            return CompositeArbResult(
                method="event", score=score, net_edge=opp.net_edge,
                capital_needed=getattr(opp, "price_sum", 1.0) * mc.kwargs.get("size", 50.0),
                details={"event_id": getattr(opp, "event_id", "")},
            )

    elif method == "spread":
        sig = params.get("last_spread_signal")
        if sig is not None and hasattr(sig, "signal") and sig.signal not in ("hold",):
            edge = abs(sig.zscore) * sig.spread_std if sig.spread_std > 0 else 0.0
            score = (edge / max(abs(sig.spread), 0.01)) * mc.weight * 0.5
            return CompositeArbResult(
                method="spread", score=score, net_edge=edge,
                capital_needed=mc.kwargs.get("size", 10.0) * 2,
                details={"zscore": sig.zscore, "signal": sig.signal},
            )

    elif method == "stat":
        result = params.get("last_stat_arb")
        if result is not None and hasattr(result, "signal") and result.signal not in ("hold",):
            edge = abs(result.zscore) * 0.01  # Approximate edge
            confidence = min(abs(result.adf_stat) / 3.0, 1.0) if result.adf_stat != 0 else 0.5
            score = edge * confidence * mc.weight
            return CompositeArbResult(
                method="stat", score=score, net_edge=edge,
                capital_needed=mc.kwargs.get("size", 10.0) * (1 + abs(result.hedge_ratio)),
                details={"zscore": result.zscore, "signal": result.signal, "adf": result.adf_stat},
            )

    elif method == "latency":
        opp = params.get("last_latency_arb")
        if opp is not None and hasattr(opp, "edge") and opp.edge > 0:
            score = opp.edge * mc.weight * 2.0  # Higher weight for latency
            return CompositeArbResult(
                method="latency", score=score, net_edge=opp.edge,
                capital_needed=opp.market_price * mc.kwargs.get("size", 20.0),
                details={"direction": opp.direction, "staleness_ms": opp.staleness_ms},
            )

    return None
