"""Advanced quantitative analytics pipeline functions.

Pipeline functions:
    toxic_flow   — VPIN toxic flow detection per-market
    microstructure — OFI, Kyle's lambda, Amihud, Roll spread, entropy
    change_detector — CUSUM change-point detection per-market

Offline analysis:
    strategy_significance — Deflated Sharpe significance test
    signal_diagnostics    — IC, half-life, Hurst
    market_efficiency     — Variance ratio, Hurst, efficiency flag
"""

from __future__ import annotations

import math
from collections import deque
from typing import Any, Callable

from horizon._horizon import (
    CusumDetector,
    OfiTracker,
    VpinDetector,
    amihud_ratio,
    deflated_sharpe,
    hurst_exponent,
    information_coefficient,
    kyles_lambda,
    roll_spread,
    shannon_entropy,
    signal_half_life,
    variance_ratio,
)
from horizon.context import Context


# ===========================================================================
# Pipeline functions
# ===========================================================================


def toxic_flow(
    feed: str,
    bucket_volume: float = 1000.0,
    n_buckets: int = 50,
    threshold: float = 0.7,
) -> Callable[[Context], dict[str, Any]]:
    """Create a VPIN toxic flow pipeline function.

    Per-market VpinDetector instances track volume-synchronized order
    imbalance and inject toxicity metrics into the pipeline.

    Args:
        feed: Feed name to read trade data from.
        bucket_volume: Volume per VPIN bucket.
        n_buckets: Number of rolling buckets.
        threshold: VPIN threshold for toxic alert.

    Returns:
        Pipeline function: (Context) -> dict with keys:
            vpin, flow_toxicity, toxic_alert
    """
    detectors: dict[str, VpinDetector] = {}

    def _toxic_flow(ctx: Context) -> dict[str, Any]:
        market_id = ctx.market.id if ctx.market else "__default__"
        if market_id not in detectors:
            detectors[market_id] = VpinDetector(bucket_volume, n_buckets)

        detector = detectors[market_id]
        fd = ctx.feeds.get(feed)

        if fd is not None and fd.last_trade_size > 0:
            vpin = detector.update(
                fd.price, fd.last_trade_size, fd.last_trade_is_buy
            )
        else:
            vpin = detector.current_vpin()

        return {
            "vpin": vpin,
            "flow_toxicity": min(1.0, vpin / max(threshold, 1e-10)),
            "toxic_alert": vpin >= threshold,
        }

    _toxic_flow.__name__ = "toxic_flow"
    return _toxic_flow


def microstructure(
    feed: str,
    lookback: int = 100,
) -> Callable[[Context], dict[str, Any]]:
    """Create a microstructure analytics pipeline function.

    Per-market OfiTracker and rolling deques for returns/volumes
    compute real-time microstructure metrics.

    Args:
        feed: Feed name to read data from.
        lookback: Rolling window size for returns/volumes.

    Returns:
        Pipeline function: (Context) -> dict with keys:
            ofi, kyle_lambda, amihud, roll_spread, market_entropy
    """
    ofi_trackers: dict[str, OfiTracker] = {}
    price_histories: dict[str, deque[float]] = {}
    volume_histories: dict[str, deque[float]] = {}

    def _microstructure(ctx: Context) -> dict[str, Any]:
        market_id = ctx.market.id if ctx.market else "__default__"
        fd = ctx.feeds.get(feed)

        # Init per-market state
        if market_id not in ofi_trackers:
            ofi_trackers[market_id] = OfiTracker()
            price_histories[market_id] = deque(maxlen=lookback)
            volume_histories[market_id] = deque(maxlen=lookback)

        tracker = ofi_trackers[market_id]
        prices = price_histories[market_id]
        volumes = volume_histories[market_id]

        ofi_val = 0.0
        kyle_val = 0.0
        amihud_val = 0.0
        roll_val = 0.0
        entropy_val = 0.0

        if fd is not None:
            # OFI from bid/ask quantities
            if fd.bid > 0 and fd.ask > 0:
                bid_qty = fd.bid  # Use price as proxy when qty unavailable
                ask_qty = fd.ask
                ofi_val = tracker.update(bid_qty, ask_qty)

            # Track price/volume history
            price = fd.price if fd.price > 0 else (fd.bid + fd.ask) / 2.0
            if price > 0:
                prices.append(price)
                vol = fd.volume_24h if fd.volume_24h > 0 else 1.0
                volumes.append(vol)

            # Compute metrics when enough data
            if len(prices) >= 3:
                returns_list = []
                signed_vols = []
                for i in range(1, len(prices)):
                    r = (prices[i] - prices[i - 1]) / prices[i - 1]
                    returns_list.append(r)
                    sign = 1.0 if r >= 0 else -1.0
                    signed_vols.append(sign * volumes[i])

                if len(returns_list) >= 2:
                    kyle_val = kyles_lambda(returns_list, signed_vols)
                    amihud_val = amihud_ratio(returns_list, list(volumes)[1:])
                    roll_val = roll_spread(returns_list)

                # Market entropy from price
                if price > 0 and price < 1:
                    entropy_val = shannon_entropy(price)

        return {
            "ofi": ofi_val,
            "kyle_lambda": kyle_val,
            "amihud": amihud_val,
            "roll_spread": roll_val,
            "market_entropy": entropy_val,
        }

    _microstructure.__name__ = "microstructure"
    return _microstructure


def change_detector(
    threshold: float = 5.0,
    drift: float = 0.0,
) -> Callable[[Context], dict[str, Any]]:
    """Create a CUSUM change-point detection pipeline function.

    Per-market CusumDetector instances detect structural breaks
    in price series.

    Args:
        threshold: CUSUM threshold for triggering.
        drift: Allowable drift before accumulation.

    Returns:
        Pipeline function: (Context) -> dict with keys:
            change_detected, cusum_upper, cusum_lower
    """
    detectors: dict[str, CusumDetector] = {}

    def _change_detector(ctx: Context) -> dict[str, Any]:
        market_id = ctx.market.id if ctx.market else "__default__"
        if market_id not in detectors:
            detectors[market_id] = CusumDetector(threshold, drift)

        detector = detectors[market_id]
        price = 0.0

        if ctx.feeds:
            # Use first available feed price
            for fd in ctx.feeds.values():
                if fd.price > 0:
                    price = fd.price
                    break
                if fd.bid > 0 and fd.ask > 0:
                    price = (fd.bid + fd.ask) / 2.0
                    break

        detected = detector.update(price) if price > 0 else False

        return {
            "change_detected": detected,
            "cusum_upper": detector.upper(),
            "cusum_lower": detector.lower(),
        }

    _change_detector.__name__ = "change_detector"
    return _change_detector


# ===========================================================================
# Offline Analysis
# ===========================================================================


def strategy_significance(
    result: Any,
    n_trials: int,
    alpha: float = 0.05,
) -> dict[str, Any]:
    """Test strategy significance using Deflated Sharpe Ratio.

    Extracts Sharpe/skew/kurtosis from an equity curve or backtest result
    and tests whether the performance is statistically significant.

    Args:
        result: Object with ``equity_curve`` (list[float]) or ``sharpe`` attribute,
                or a dict with key "equity_curve" or "returns".
        n_trials: Number of strategy configurations tested.
        alpha: Significance level.

    Returns:
        dict with: deflated_sharpe_pvalue, bonferroni_alpha, is_significant
    """
    # Extract returns
    returns = _extract_returns(result)
    if not returns or len(returns) < 10:
        return {
            "deflated_sharpe_pvalue": 1.0,
            "bonferroni_alpha": alpha / max(n_trials, 1),
            "is_significant": False,
        }

    n_obs = len(returns)
    mean_r = sum(returns) / n_obs
    var_r = sum((r - mean_r) ** 2 for r in returns) / n_obs
    std_r = math.sqrt(max(var_r, 1e-15))
    sharpe = mean_r / std_r if std_r > 1e-15 else 0.0

    # Skewness and excess kurtosis
    skew = sum(((r - mean_r) / std_r) ** 3 for r in returns) / n_obs if std_r > 1e-15 else 0.0
    kurt = (
        sum(((r - mean_r) / std_r) ** 4 for r in returns) / n_obs if std_r > 1e-15 else 3.0
    )

    pvalue = deflated_sharpe(sharpe, n_obs, n_trials, skew, kurt)
    bonf = alpha / max(n_trials, 1)

    return {
        "deflated_sharpe_pvalue": pvalue,
        "bonferroni_alpha": bonf,
        "is_significant": pvalue < alpha,
    }


def signal_diagnostics(
    predictions: list[float],
    outcomes: list[float],
) -> dict[str, float]:
    """Compute signal quality diagnostics.

    Args:
        predictions: Model predictions.
        outcomes: Actual outcomes.

    Returns:
        dict with: ic, half_life, hurst
    """
    ic = information_coefficient(predictions, outcomes) if len(predictions) >= 2 else 0.0
    hl = signal_half_life(predictions) if len(predictions) >= 3 else 0.0
    h = hurst_exponent(predictions) if len(predictions) >= 20 else 0.5

    return {
        "ic": ic,
        "half_life": hl,
        "hurst": h,
    }


def market_efficiency(
    prices: list[float],
) -> dict[str, Any]:
    """Assess market efficiency from a price series.

    Args:
        prices: Time series of prices.

    Returns:
        dict with: variance_ratio, hurst, is_efficient
    """
    if len(prices) < 4:
        return {
            "variance_ratio": 1.0,
            "hurst": 0.5,
            "is_efficient": True,
        }

    # Compute returns
    returns = [(prices[i] - prices[i - 1]) / prices[i - 1] for i in range(1, len(prices)) if prices[i - 1] != 0]

    vr = variance_ratio(returns, 2) if len(returns) >= 3 else 1.0
    h = hurst_exponent(prices) if len(prices) >= 20 else 0.5

    # Market is efficient if VR ≈ 1 and Hurst ≈ 0.5
    is_efficient = abs(vr - 1.0) < 0.3 and abs(h - 0.5) < 0.15

    return {
        "variance_ratio": vr,
        "hurst": h,
        "is_efficient": is_efficient,
    }


# ===========================================================================
# Helpers
# ===========================================================================


def _extract_returns(result: Any) -> list[float]:
    """Extract a returns series from various result formats."""
    # Dict with "returns" key
    if isinstance(result, dict):
        if "returns" in result:
            return list(result["returns"])
        if "equity_curve" in result:
            eq = result["equity_curve"]
            return [(eq[i] - eq[i - 1]) / eq[i - 1] for i in range(1, len(eq)) if eq[i - 1] != 0]

    # Object with returns attribute
    if hasattr(result, "returns"):
        return list(result.returns)

    # Object with equity_curve attribute
    if hasattr(result, "equity_curve"):
        eq = list(result.equity_curve)
        return [(eq[i] - eq[i - 1]) / eq[i - 1] for i in range(1, len(eq)) if eq[i - 1] != 0]

    return []
