"""
Portfolio context builder — injects open positions into AI prompts.

Provides:
  - get_open_trades_with_moves(): open trades + 1d/5d price moves via yfinance
  - get_portfolio_concentration(): count per asset_class
  - build_portfolio_context_block(): formatted prompt block for AI injection
"""
import logging
import math
from typing import List, Dict, Optional
from datetime import date, datetime

_log = logging.getLogger("portfolio_context")


def _safe_float(v, ndigits: int = 2) -> Optional[float]:
    """Round float, returning None for NaN/Inf/None — JSON-safe."""
    if v is None:
        return None
    try:
        f = float(v)
        if math.isnan(f) or math.isinf(f):
            return None
        return round(f, ndigits)
    except (TypeError, ValueError):
        return None


def get_open_trades_with_moves() -> List[Dict]:
    """Fetch all open trades and compute recent underlying price moves."""
    from services.database import get_trade_entry_prices, _normalize_asset_class, _asset_class_from_ticker
    import yfinance as yf

    trades = get_trade_entry_prices(days=365)  # all open trades regardless of age

    result = []
    for t in trades:
        sym = t.get("underlying", "")
        entry_price = t.get("entry_price")
        move_1d = move_5d = current_price = None

        try:
            hist = yf.Ticker(sym).history(period="5d", auto_adjust=True)
            if not hist.empty:
                closes = hist["Close"].squeeze().dropna()
                if len(closes) >= 1:
                    current_price = _safe_float(closes.iloc[-1], 4)
                if len(closes) >= 2 and closes.iloc[-2] != 0:
                    move_1d = _safe_float((closes.iloc[-1] / closes.iloc[-2] - 1) * 100)
                if len(closes) >= 5 and closes.iloc[0] != 0:
                    move_5d = _safe_float((closes.iloc[-1] / closes.iloc[0] - 1) * 100)
        except Exception as e:
            _log.debug(f"[PortfolioCtx] yfinance failed for {sym}: {e}")

        # Days held / remaining
        entry_str = t.get("entry_date", "")
        horizon = t.get("horizon_days") or 30
        days_held = 0
        days_remaining = horizon
        try:
            entry_dt = datetime.strptime(entry_str, "%Y-%m-%d").date()
            days_held = (date.today() - entry_dt).days
            days_remaining = max(0, horizon - days_held)
        except Exception:
            pass

        # Canonical asset class
        cls = _normalize_asset_class(t.get("asset_class") or "", sym)

        result.append({
            "id": t.get("id"),
            "underlying": sym,
            "strategy": t.get("strategy") or "?",
            "asset_class": cls,
            "pattern_name": (t.get("pattern_name") or "")[:50],
            "entry_price": _safe_float(entry_price, 4),
            "current_price": current_price,
            "move_1d_pct": move_1d,
            "move_5d_pct": move_5d,
            "score_at_entry": t.get("score_at_entry"),
            "days_held": days_held,
            "days_remaining": days_remaining,
            "horizon_days": horizon,
        })

    return result


def get_portfolio_concentration(open_trades: List[Dict]) -> Dict[str, int]:
    """Count open trades by canonical asset_class."""
    conc: Dict[str, int] = {}
    for t in open_trades:
        cls = t.get("asset_class") or "unknown"
        conc[cls] = conc.get(cls, 0) + 1
    return conc


def build_portfolio_context_block(open_trades: List[Dict], concentration: Dict[str, int]) -> str:
    """Build a prompt section describing current portfolio for injection into AI prompts."""
    if not open_trades:
        return "\n## CURRENT PORTFOLIO\nNo open trades — empty portfolio.\n"

    total = len(open_trades)
    conc_sorted = sorted(concentration.items(), key=lambda x: -x[1])
    conc_str = " | ".join(f"{cls.upper()}: {n}" for cls, n in conc_sorted)

    lines: List[str] = [f"Total: {total} open trade(s) | Concentration: {conc_str}", ""]

    for t in open_trades:
        sym = t["underlying"]
        strat = t["strategy"]
        cls = t.get("asset_class") or "?"
        held = t.get("days_held", "?")
        rem = t.get("days_remaining", "?")
        m1d_str = f"{t['move_1d_pct']:+.1f}%" if t.get("move_1d_pct") is not None else "N/A"
        m5d_str = f"{t['move_5d_pct']:+.1f}%" if t.get("move_5d_pct") is not None else "N/A"
        pat = t.get("pattern_name", "")
        entry = t.get("entry_price")
        cur = t.get("current_price")
        ep_str = f"entry {entry:.2f} → current {cur:.2f}" if entry and cur else ""

        line = f"  • {sym} | {strat} [{cls}] | {held}d held / {rem}d remaining | D-1: {m1d_str} | D-5: {m5d_str}"
        if ep_str:
            line += f" | {ep_str}"
        if pat:
            line += f" | thesis: «{pat}»"
        lines.append(line)

    # Identify overweight classes
    overweight = [cls for cls, n in conc_sorted if n >= 3]
    ow_str = ", ".join(overweight) if overweight else "none"

    block = (
        "\n## CURRENT PORTFOLIO — OPEN POSITIONS\n"
        + "\n".join(lines)
        + f"\n\nOverweight classes (≥3 trades): {ow_str}\n"
        + "\n⚠️ MANDATORY RULES (non-negotiable):\n"
        + "1. DO NOT suggest a new trade on an underlying already in the portfolio — strictly forbidden.\n"
        + "2. Explicitly flag in 'rationale' if a suggestion CONTRADICTS an open position (potential exit signal).\n"
        + "3. Avoid adding to an overweight class (≥3 trades) unless an exceptional justified catalyst exists.\n"
        + "4. A signal opposing an open position = EXIT opportunity to document, not a reverse entry.\n"
    )
    return block
