"""feature_extractor.py — Transform raw trade records into standardized features.

Phase 1 supports extraction from the data sources already available in the system:
  - decision_ledger.ndjson entries (signal/decision metadata)
  - trading_ledger.ndjson entries (trade outcomes)
  - agent_state.json snapshots (runtime state at trade time)
  - agent_decision_contract.json (macro direction at trade time)

All extraction functions return dicts with standardized keys matching the
trading_brain.db trade_events and agent_judgments column schemas.

Usage::

    from src.learning.feature_extractor import extract_trade_features
    features = extract_trade_features(trade_record, state_snapshot, agent_contract)
    brain.record_trade(features["entry_features"], features["outcome"])
"""

from __future__ import annotations

from typing import Any


# ── Column name constants (keep in sync with trading_brain.py DDL) ──

_ENTRY_COLS = [
    "ticket", "decision_id", "signal_id",
    "opened_at", "strategy_id", "strategy_trust", "side",
    "confidence", "entry_price", "sl_price", "tp_price", "volume_lots",
    "session", "regime", "adx_value", "atr_value", "vol_ratio", "spread",
    "agent_direction", "agent_confidence", "agent_aligned",
    "macro_authority", "direction_quality",
    "strategy_win_rate_20", "strategy_profit_factor", "strategy_loss_streak",
    "risk_level", "cooldown_active", "daily_loss_pct",
]

_OUTCOME_COLS = [
    "closed_at", "pnl", "pnl_atr", "is_win", "sl_hit", "tp_hit",
    "duration_min", "exit_reason", "peak_profit_atr", "max_adverse_atr",
    "capture_ratio",
]

_JUDGMENT_COLS = [
    "judged_at", "direction", "confidence", "probability",
    "reason", "source", "degraded",
    "regime", "adx_value", "vol_ratio", "dxy_correlation", "ema_bias",
    "valid_until", "expired", "disempowered",
]


# ── Main extraction ────────────────────────────────────────────────

def extract_trade_features(
    trade_record: dict[str, Any] | None = None,
    agent_state: dict[str, Any] | None = None,
    agent_contract: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Extract standardized entry features and outcome from raw trade data.

    Args:
        trade_record: Dict from decision_ledger or trading_ledger containing
            the full trade details including signal metadata, execution info,
            and PnL outcome.
        agent_state: Dict from agent_state.json with runtime state at trade time.
            Used for risk context (cooldown, daily loss, consecutive losses).
        agent_contract: Dict from agent_decision_contract.json with macro
            direction and interpreter context.

    Returns:
        {"entry_features": {...}, "outcome": {...}}
        Both dicts use keys matching trading_brain.db column names.
    """
    tr = trade_record or {}
    state = agent_state or {}
    macro = (agent_contract or {}).get("macro", {}) if isinstance(agent_contract, dict) else {}

    entry = _extract_entry(tr, state, macro)
    outcome = _extract_outcome(tr)
    return {"entry_features": entry, "outcome": outcome}


def extract_judgment_features(
    judgment_result: dict[str, Any] | None = None,
    market_snapshot: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Extract standardized features from an AI macro direction judgment.

    Args:
        judgment_result: Dict from commander_client.call_commander() output.
        market_snapshot: Optional dict with current regime, adx, vol_ratio, etc.

    Returns dict with keys matching agent_judgments column names.
    """
    jr = judgment_result or {}
    ms = market_snapshot or {}

    return {
        "direction": str(jr.get("direction", "") or "").upper()[:10],
        "confidence": float(jr.get("confidence", 0.5) or 0.5),
        "probability": float(jr.get("probability", 0.5) or 0.5),
        "reason": str(jr.get("reason", "") or "")[:500],
        "source": str(jr.get("source", "commander") or "commander"),
        "degraded": 1 if jr.get("degraded") else 0,
        "regime": str(ms.get("regime", "") or "").upper()[:30],
        "adx_value": float(ms.get("adx", 0) or 0),
        "vol_ratio": float(ms.get("vol_ratio", 1.0) or 1.0),
        "dxy_correlation": float(ms.get("dxy_correlation", 0.0) or 0.0),
        "ema_bias": str(ms.get("ema_bias", "") or "").upper()[:20],
    }


# ── Internal helpers ───────────────────────────────────────────────

def _extract_entry(
    tr: dict[str, Any],
    state: dict[str, Any],
    macro: dict[str, Any],
) -> dict[str, Any]:
    """Extract entry features from trade record + runtime state."""
    sig = tr.get("payload", tr) if isinstance(tr.get("payload", {}), dict) else {}
    entry_info = sig.get("entry_plan", sig)

    side = str(sig.get("side", tr.get("side", "")) or "").upper()
    agent_dir = str(macro.get("direction", "") or "").upper()

    return {
        "ticket": int(tr.get("ticket", 0) or 0),
        "decision_id": str(tr.get("decision_id", "") or ""),
        "signal_id": str(sig.get("signal_id", tr.get("signal_id", "")) or ""),
        "opened_at": str(tr.get("timestamp", tr.get("opened_at", "")) or ""),
        "strategy_id": str(sig.get("strategy_id", tr.get("strategy_id", "")) or ""),
        "strategy_trust": str(tr.get("trust_level", "") or ""),
        "side": side,
        "confidence": float(sig.get("confidence", tr.get("confidence", 0.0)) or 0.0),
        "entry_price": float(entry_info.get("entry", entry_info.get("entry_price", 0.0)) or 0.0),
        "sl_price": float(entry_info.get("sl", entry_info.get("sl_price", 0.0)) or 0.0),
        "tp_price": float(entry_info.get("tp", entry_info.get("tp_price", 0.0)) or 0.0),
        "volume_lots": float(sig.get("volume_lots", tr.get("volume_lots", 0.0)) or 0.0),
        "session": str(sig.get("session", tr.get("session", "")) or "").upper(),
        "regime": str(sig.get("regime", tr.get("regime", "")) or "").upper(),
        "adx_value": float(sig.get("adx", tr.get("adx", 0.0)) or 0.0),
        "atr_value": float(sig.get("atr", tr.get("atr", 0.0)) or 0.0),
        "vol_ratio": float(state.get("vol_ratio", tr.get("vol_ratio", 1.0)) or 1.0),
        "spread": float(sig.get("spread", tr.get("spread", 0.0)) or 0.0),
        "agent_direction": agent_dir,
        "agent_confidence": float(macro.get("confidence", 0.5) or 0.5),
        "agent_aligned": int(_is_aligned(side, agent_dir)),
        "macro_authority": str(tr.get("macro_authority", "") or "").upper(),
        "direction_quality": str(macro.get("phase", "") or "").upper(),
        "strategy_win_rate_20": float(state.get("win_rate", 0.0) or 0.0),
        "strategy_profit_factor": float(state.get("profit_factor", 0.0) or 0.0),
        "strategy_loss_streak": int(state.get("consecutive_losses", 0) or 0),
        "risk_level": str(state.get("risk_level", "") or ""),
        "cooldown_active": 1 if state.get("_shrink_applied") else 0,
        "daily_loss_pct": float(state.get("daily_loss_pct", 0.0) or 0.0),
    }


def _extract_outcome(tr: dict[str, Any]) -> dict[str, Any]:
    """Extract outcome/label features from trade record."""
    pnl = float(tr.get("pnl", 0.0) or 0.0)
    pnl_atr = float(tr.get("pnl_atr", 0.0) or 0.0)
    # Compute pnl_atr approximation if not directly available
    if pnl_atr == 0.0 and pnl != 0.0:
        entry = float(tr.get("entry_price", tr.get("entry", 0.0)) or 0.0)
        atr = float(tr.get("atr", 1.0) or 1.0)
        if entry > 0 and atr > 0:
            sl = float(tr.get("sl_price", tr.get("sl", 0.0)) or 0.0)
            if sl > 0:
                sl_points = abs(entry - sl)
                pnl_points = abs(pnl) / (100.0 * float(tr.get("volume_lots", 0.01) or 0.01))
                pnl_atr = round(pnl_points / max(0.1, sl_points), 4) if sl_points > 0 else 0.0

    return {
        "closed_at": str(tr.get("closed_at", tr.get("close_time", "")) or ""),
        "pnl": round(pnl, 2),
        "pnl_atr": round(pnl_atr, 4),
        "is_win": 1 if pnl > 0 else 0,
        "sl_hit": 1 if tr.get("sl_hit", False) else 0,
        "tp_hit": 1 if tr.get("tp_hit", False) else 0,
        "duration_min": int(tr.get("duration_min", tr.get("duration", 0)) or 0),
        "exit_reason": str(tr.get("exit_reason", tr.get("close_reason", "")) or "")[:50],
        "exit_price": float(tr.get("exit_price", 0.0) or 0.0),
        "peak_profit_atr": float(tr.get("peak_profit_atr", 0.0) or 0.0),
        "max_adverse_atr": float(tr.get("max_adverse_atr", 0.0) or 0.0),
        "capture_ratio": float(tr.get("capture_ratio", tr.get("profit_capture_ratio", 0.0)) or 0.0),
    }


def _is_aligned(signal_side: str, agent_direction: str) -> bool:
    """A signal is 'aligned' when BUY+LONG or SELL+SHORT; NEUTRAL is not aligned."""
    s = signal_side.upper()
    d = agent_direction.upper()
    if s == "BUY" and d == "LONG":
        return True
    if s == "SELL" and d == "SHORT":
        return True
    return False
