"""Position Reconciliation -- compare engine state vs exchange API.

Detects discrepancies between the engine's internal position tracking
and the exchange's reported positions. Flags missing positions, size
mismatches, and side mismatches with appropriate severity levels.

Usage:
    # One-off reconciliation
    report = reconcile(engine, exchange_positions=[
        {"market_id": "btc-100k", "size": 10.0, "side": "yes"},
    ])
    if not report.is_clean:
        for brk in report.breaks:
            print(f"BREAK: {brk.market_id} - {brk.break_type} ({brk.severity})")

    # Pipeline integration (auto-reconcile every 5 minutes)
    hz.run(
        pipeline=[
            hz.auto_reconcile(engine, interval=300.0),
            model,
            quoter,
        ],
        ...
    )
"""

from __future__ import annotations

import logging
import time as _time
from dataclasses import dataclass, field
from typing import Any, Callable

from horizon.context import Context

logger = logging.getLogger("horizon")


@dataclass
class PositionBreak:
    """A single reconciliation discrepancy."""

    market_id: str
    break_type: str  # "missing_engine", "missing_exchange", "size_mismatch", "side_mismatch", "price_mismatch"
    engine_size: float | None = None
    exchange_size: float | None = None
    engine_side: str | None = None
    exchange_side: str | None = None
    severity: str = "info"  # "critical", "warning", "info"


@dataclass
class ReconciliationReport:
    """Result of a reconciliation run."""

    breaks: list[PositionBreak] = field(default_factory=list)
    matched: int = 0
    total_engine: int = 0
    total_exchange: int = 0
    is_clean: bool = True
    timestamp: float = 0.0


def reconcile(
    engine: Any,
    exchange_positions: list[dict[str, Any]] | None = None,
) -> ReconciliationReport:
    """Compare engine positions vs exchange.

    If exchange_positions is None, attempts to fetch from engine's exchange
    (if the engine supports it). Otherwise, compares against the provided list.

    Exchange position dicts should have keys:
        - "market_id" (str): market identifier
        - "size" (float): position size
        - "side" (str): "yes" or "no"

    Size mismatch severity:
        - > 10% difference: "critical"
        - > 1% difference: "warning"
        - Side mismatch: always "critical"
        - Missing from either side: "critical"

    Args:
        engine: Horizon Engine instance.
        exchange_positions: Optional list of position dicts from the exchange.
            If None, tries engine.sync_positions() or returns engine-only report.

    Returns:
        ReconciliationReport with all detected breaks.
    """
    from horizon._horizon import auth_require_pro
    auth_require_pro()

    now = _time.time()
    report = ReconciliationReport(timestamp=now)

    # Get engine positions
    engine_positions = engine.positions()
    report.total_engine = len(engine_positions)

    # Build engine position lookup: market_id -> (total_size, side_str)
    engine_map: dict[str, tuple[float, str]] = {}
    for pos in engine_positions:
        mid = getattr(pos, "market_id", "")
        size = getattr(pos, "size", 0.0)
        side = getattr(pos, "side", None)
        side_str = ""
        if side is not None:
            side_str = str(side).lower()
            # Handle Side.Yes / Side.No enum
            if "yes" in side_str:
                side_str = "yes"
            elif "no" in side_str:
                side_str = "no"
        # Aggregate by market_id (in case of multiple positions per market)
        if mid in engine_map:
            existing_size, existing_side = engine_map[mid]
            engine_map[mid] = (existing_size + size, existing_side)
        else:
            engine_map[mid] = (size, side_str)

    # If no exchange positions provided, try to fetch
    if exchange_positions is None:
        # Try to get positions from the engine's exchange backend
        try:
            # Some engines expose exchange position fetching
            if hasattr(engine, "exchange_positions"):
                exchange_positions = engine.exchange_positions()
            else:
                # Return engine-only report with no exchange data
                report.total_exchange = 0
                report.is_clean = True
                return report
        except Exception as e:
            logger.warning("Could not fetch exchange positions: %s", e)
            report.total_exchange = 0
            report.is_clean = True
            return report

    report.total_exchange = len(exchange_positions)

    # Build exchange position lookup
    exchange_map: dict[str, tuple[float, str]] = {}
    for pos_dict in exchange_positions:
        mid = pos_dict.get("market_id", "")
        size = float(pos_dict.get("size", 0.0))
        side = str(pos_dict.get("side", "")).lower()
        if mid in exchange_map:
            existing_size, existing_side = exchange_map[mid]
            exchange_map[mid] = (existing_size + size, existing_side)
        else:
            exchange_map[mid] = (size, side)

    # Find all market IDs across both sides
    all_markets = set(engine_map.keys()) | set(exchange_map.keys())
    matched = 0

    for mid in sorted(all_markets):
        in_engine = mid in engine_map
        in_exchange = mid in exchange_map

        if in_engine and not in_exchange:
            # Position exists in engine but not on exchange
            eng_size, eng_side = engine_map[mid]
            report.breaks.append(PositionBreak(
                market_id=mid,
                break_type="missing_exchange",
                engine_size=eng_size,
                exchange_size=None,
                engine_side=eng_side,
                exchange_side=None,
                severity="critical",
            ))
        elif in_exchange and not in_engine:
            # Position exists on exchange but not in engine
            exch_size, exch_side = exchange_map[mid]
            report.breaks.append(PositionBreak(
                market_id=mid,
                break_type="missing_engine",
                engine_size=None,
                exchange_size=exch_size,
                engine_side=None,
                exchange_side=exch_side,
                severity="critical",
            ))
        else:
            # Both sides have the position -- compare
            eng_size, eng_side = engine_map[mid]
            exch_size, exch_side = exchange_map[mid]

            # Side mismatch check
            if eng_side and exch_side and eng_side != exch_side:
                report.breaks.append(PositionBreak(
                    market_id=mid,
                    break_type="side_mismatch",
                    engine_size=eng_size,
                    exchange_size=exch_size,
                    engine_side=eng_side,
                    exchange_side=exch_side,
                    severity="critical",
                ))
                continue

            # Size mismatch check
            ref_size = max(abs(eng_size), abs(exch_size))
            if ref_size > 0:
                diff_pct = abs(eng_size - exch_size) / ref_size
            else:
                diff_pct = 0.0

            if diff_pct > 0.10:
                severity = "critical"
            elif diff_pct > 0.01:
                severity = "warning"
            else:
                severity = ""

            if severity:
                report.breaks.append(PositionBreak(
                    market_id=mid,
                    break_type="size_mismatch",
                    engine_size=eng_size,
                    exchange_size=exch_size,
                    engine_side=eng_side,
                    exchange_side=exch_side,
                    severity=severity,
                ))
            else:
                matched += 1

    report.matched = matched
    report.is_clean = len(report.breaks) == 0

    return report


def auto_reconcile(
    engine: Any = None,
    interval: float = 300.0,
) -> Callable[[Context], None]:
    """Pipeline function for hz.run() that runs reconciliation periodically.

    Runs reconciliation every ``interval`` seconds and injects results
    into ctx.params:
        - "recon_clean" (bool): True if no breaks found
        - "recon_breaks" (int): Number of breaks found

    Logs warnings for any breaks detected.

    Args:
        engine: Optional engine override. If None, uses ctx.params["engine"].
        interval: Seconds between reconciliation runs.

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

    _last_run: list[float] = [0.0]  # Mutable container for closure
    _last_report: list[ReconciliationReport | None] = [None]

    def _reconcile(ctx: Context) -> None:
        now = _time.time()

        # Only run at the configured interval
        if now - _last_run[0] < interval:
            # Inject previous results if available
            if _last_report[0] is not None:
                ctx.params["recon_clean"] = _last_report[0].is_clean
                ctx.params["recon_breaks"] = len(_last_report[0].breaks)
            else:
                ctx.params["recon_clean"] = True
                ctx.params["recon_breaks"] = 0
            return

        _last_run[0] = now

        eng = engine if engine is not None else ctx.params.get("engine")
        if eng is None:
            ctx.params["recon_clean"] = True
            ctx.params["recon_breaks"] = 0
            return

        try:
            report = reconcile(eng)
            _last_report[0] = report

            ctx.params["recon_clean"] = report.is_clean
            ctx.params["recon_breaks"] = len(report.breaks)

            if not report.is_clean:
                for brk in report.breaks:
                    if brk.severity == "critical":
                        logger.warning(
                            "RECON BREAK [%s] %s: %s (engine=%s, exchange=%s)",
                            brk.severity.upper(),
                            brk.market_id,
                            brk.break_type,
                            brk.engine_size,
                            brk.exchange_size,
                        )
                    else:
                        logger.info(
                            "RECON BREAK [%s] %s: %s (engine=%s, exchange=%s)",
                            brk.severity,
                            brk.market_id,
                            brk.break_type,
                            brk.engine_size,
                            brk.exchange_size,
                        )
        except Exception as e:
            logger.warning("Reconciliation failed: %s", e)
            ctx.params["recon_clean"] = True
            ctx.params["recon_breaks"] = 0

    _reconcile.__name__ = "auto_reconcile"
    return _reconcile
