"""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
    hawkes_intensity - Hawkes self-exciting process intensity per-market
    correlation_estimator - Ledoit-Wolf shrinkage correlation across feeds

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 logging
import math
import time
from collections import deque
from typing import Any, Callable

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

logger = logging.getLogger(__name__)


# ===========================================================================
# 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
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    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
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    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
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    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


def hawkes_intensity(
    feed_name: str | None = None,
    mu: float = 0.1,
    alpha: float = 0.5,
    beta: float = 1.0,
    param_name: str = "hawkes",
) -> Callable[[Context], None]:
    """Create a Hawkes self-exciting process pipeline function.

    Per-market HawkesProcess instances track event arrival intensity.
    Events are triggered by fills and large price jumps (>2%).

    Injected params:
        ctx.params[param_name + "_intensity"] -> float: current intensity
        ctx.params[param_name + "_branching"] -> float: branching ratio
        ctx.params[param_name + "_expected_1m"] -> float: expected events in next 60s

    Args:
        feed_name: Feed name to read price data from. None = first available feed.
        mu: Background intensity (baseline event rate).
        alpha: Excitation magnitude per event.
        beta: Decay rate of excitation.
        param_name: Prefix for keys injected into ctx.params.

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

    processes: dict[str, HawkesProcess] = {}
    prev_prices: dict[str, float] = {}

    def _hawkes_intensity(ctx: Context) -> None:
        market_id = ctx.market.id if ctx.market else "__default__"

        if market_id not in processes:
            processes[market_id] = HawkesProcess(
                mu=mu, alpha=alpha, beta=beta,
            )
            prev_prices[market_id] = 0.0

        process = processes[market_id]
        now = time.time()

        # Check for fills this cycle
        fills_this_cycle = ctx.params.get("fills_this_cycle", 0)
        if fills_this_cycle and fills_this_cycle > 0:
            for _ in range(int(fills_this_cycle)):
                process.add_event(now)

        # Check for price jumps > 2%
        price = _get_feed_price(ctx, feed_name)
        if price > 0.0:
            prev = prev_prices[market_id]
            if prev > 0.0:
                pct_change = abs(price - prev) / prev
                if pct_change > 0.02:
                    process.add_event(now)
            prev_prices[market_id] = price

        # Query intensity slightly after now so events added this tick
        # contribute to the reading (intensity is causal: event at t
        # only excites for t' > t).
        query_t = now + 1e-6
        ctx.params[param_name + "_intensity"] = process.intensity(query_t)
        ctx.params[param_name + "_branching"] = process.branching_ratio()
        ctx.params[param_name + "_expected_1m"] = process.expected_events(query_t, 60.0)

    _hawkes_intensity.__name__ = "hawkes_intensity"
    return _hawkes_intensity


def correlation_estimator(
    feed_names: list[str] | None = None,
    window: int = 100,
    param_name: str = "correlation",
) -> Callable[[Context], None]:
    """Create a Ledoit-Wolf shrinkage correlation estimator pipeline function.

    Collects price returns from multiple feeds and computes a shrunk
    covariance matrix using the Ledoit-Wolf estimator.

    Injected params:
        ctx.params[param_name + "_matrix"] -> list[list[float]]: shrunk covariance matrix
        ctx.params[param_name + "_shrinkage_intensity"] -> float: shrinkage intensity
        ctx.params[param_name + "_updated"] -> bool: whether the matrix was updated this tick

    Args:
        feed_names: List of feed names to track. None = all available feeds.
        window: Number of observations before computing the covariance matrix.
                Updates every ``window`` ticks (not every tick).
        param_name: Prefix for keys injected into ctx.params.

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

    price_histories: dict[str, deque[float]] = {}
    tick_count: list[int] = [0]  # mutable counter via list
    last_matrix: list[list[float]] = []
    last_shrinkage: list[float] = [0.0]

    def _correlation_estimator(ctx: Context) -> None:
        # Determine which feeds to track
        names = feed_names
        if names is None:
            names = list(ctx.feeds.keys()) if ctx.feeds else []

        if not names:
            ctx.params[param_name + "_matrix"] = last_matrix
            ctx.params[param_name + "_shrinkage_intensity"] = last_shrinkage[0]
            ctx.params[param_name + "_updated"] = False
            return

        # Collect current prices
        has_all = True
        for name in names:
            fd = ctx.feeds.get(name)
            if fd is None or fd.price <= 0.0:
                has_all = False
                continue
            if name not in price_histories:
                price_histories[name] = deque(maxlen=window + 1)
            price_histories[name].append(fd.price)

        if not has_all:
            ctx.params[param_name + "_matrix"] = last_matrix
            ctx.params[param_name + "_shrinkage_intensity"] = last_shrinkage[0]
            ctx.params[param_name + "_updated"] = False
            return

        tick_count[0] += 1

        # Only update every `window` ticks
        updated = False
        if tick_count[0] % window == 0:
            # Build returns matrix: each row is an asset's return series
            returns_matrix: list[list[float]] = []
            min_len = min(len(price_histories[n]) for n in names)

            if min_len >= 3:  # need at least 3 prices for 2 returns
                # Build per-asset return series
                asset_returns: list[list[float]] = []
                for name in names:
                    prices = list(price_histories[name])[-min_len:]
                    returns = []
                    for i in range(1, len(prices)):
                        if prices[i - 1] > 0.0:
                            returns.append(
                                (prices[i] - prices[i - 1]) / prices[i - 1]
                            )
                        else:
                            returns.append(0.0)
                    asset_returns.append(returns)

                n_assets = len(asset_returns)
                n_obs = len(asset_returns[0]) if asset_returns else 0

                if n_assets >= 1 and n_obs >= 2:
                    # Transpose: rows=observations, cols=assets
                    # ledoit_wolf_shrinkage expects [[r_t0_a0, r_t0_a1, ...], ...]
                    for t in range(n_obs):
                        returns_matrix.append(
                            [asset_returns[a][t] for a in range(n_assets)]
                        )

                if len(returns_matrix) >= 2 and len(returns_matrix[0]) >= 1:
                    try:
                        matrix, shrinkage = ledoit_wolf_shrinkage(returns_matrix)
                        last_matrix.clear()
                        last_matrix.extend(matrix)
                        last_shrinkage[0] = shrinkage
                        updated = True
                    except (ValueError, RuntimeError) as e:
                        logger.debug(
                            "Ledoit-Wolf shrinkage failed: %s", e,
                        )

        ctx.params[param_name + "_matrix"] = last_matrix
        ctx.params[param_name + "_shrinkage_intensity"] = last_shrinkage[0]
        ctx.params[param_name + "_updated"] = updated

    _correlation_estimator.__name__ = "correlation_estimator"
    return _correlation_estimator


def _get_feed_price(ctx: Context, feed_name: str | None) -> float:
    """Extract price from a named feed or the first available feed."""
    if feed_name is not None:
        fd = ctx.feeds.get(feed_name)
        if fd is not None and fd.price > 0:
            return fd.price
        return 0.0

    # First available feed
    for fd in ctx.feeds.values():
        if fd.price > 0:
            return fd.price
    return 0.0


# ===========================================================================
# 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
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    # 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
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    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
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    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 []
