from fastapi import APIRouter, HTTPException
import traceback as tb_mod
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
from datetime import datetime, date, timedelta
from services.database import (
    add_position, get_positions, close_position,
    update_position_notes, compute_ib_fees
)
from services.data_fetcher import get_quote
from services.options_pricer import black_scholes
from services.data_fetcher import compute_historical_iv
import math

router = APIRouter(prefix="/api/portfolio", tags=["portfolio"])


class AddPositionRequest(BaseModel):
    title: str
    underlying: str
    strategy: str
    asset_class: Optional[str] = "indices"
    entry_date: Optional[str] = None
    expiry_date: Optional[str] = None
    expiry_days: Optional[int] = 90
    legs: List[Dict[str, Any]]
    capital_invested: float
    entry_underlying_price: Optional[float] = None
    geo_trigger: Optional[str] = ""
    rationale: Optional[str] = ""
    notes: Optional[str] = ""


class ClosePositionRequest(BaseModel):
    close_value: float


class NotesRequest(BaseModel):
    notes: str


def mark_to_market(pos: Dict[str, Any]) -> Dict[str, Any]:
    """Compute current value of a position — Saxo-first per leg (services.portfolio_pricing)
    when the underlying has a saxo_option_symbol linked in the Watchlist, yfinance
    historical-vol Black-Scholes otherwise or as a fallback on any Saxo failure. Each leg's
    `pricing_source` in the response says plainly which one was actually used."""
    from services.portfolio_pricing import resolve_saxo_chain, price_leg

    underlying = pos["underlying"]

    # Compute days to expiry
    expiry_date = pos.get("expiry_date") or ""
    if expiry_date:
        try:
            exp = datetime.strptime(expiry_date[:10], "%Y-%m-%d").date()
            T = max(0.001, (exp - date.today()).days / 365)
        except Exception:
            T = max(0.001, (pos.get("expiry_days", 90) - 30) / 365)
    else:
        entry = datetime.strptime(pos["entry_date"][:10], "%Y-%m-%d").date()
        days_elapsed = (date.today() - entry).days
        T = max(0.001, (pos.get("expiry_days", 90) - days_elapsed) / 365)
    days_to_expiry = T * 365

    r = 0.05
    chain, surface = resolve_saxo_chain(underlying, target_days=max(int(days_to_expiry), 1))

    # yfinance fallback inputs — only actually fetched if no usable Saxo chain, so a
    # Saxo-linked instrument never pays for a yfinance round-trip it doesn't need.
    fallback_spot = pos.get("entry_underlying_price") or 100.0
    fallback_sigma = 0.20
    if chain is None:
        q = get_quote(underlying)
        fallback_spot = (q.get("price") if q else None) or fallback_spot
        from services.data_fetcher import compute_historical_iv as get_iv
        fallback_sigma = get_iv(underlying)

    S = chain["spot"] if chain else fallback_spot

    legs = pos.get("legs", [])
    if not legs:
        return {**pos, "current_value": pos["capital_invested"], "pnl": 0, "pnl_pct": 0,
                "current_underlying": S, "greeks": {}, "pricing_sources": []}

    # T at entry (full original duration) — used to reprice legs at entry if premium_paid not stored
    S_entry = float(pos.get("entry_underlying_price") or S)
    entry_date_str = pos.get("entry_date", "")
    if expiry_date and entry_date_str:
        try:
            exp_dt = datetime.strptime(expiry_date[:10], "%Y-%m-%d").date()
            entry_dt = datetime.strptime(entry_date_str[:10], "%Y-%m-%d").date()
            days_to_expiry_entry = max(0.001, (exp_dt - entry_dt).days)
        except Exception:
            days_to_expiry_entry = max(0.001, pos.get("expiry_days", 90))
    else:
        days_to_expiry_entry = max(0.001, pos.get("expiry_days", 90))

    total_current_value = 0.0
    total_entry_value = 0.0
    net_delta = 0.0
    net_theta = 0.0
    net_vega = 0.0
    priced_legs = []
    sources_seen: set = set()
    sigmas_seen: List[float] = []

    for leg in legs:
        K = leg.get("strike") or S
        K_entry = leg.get("strike") or S_entry
        opt_type = leg.get("option_type", "call")
        qty = leg.get("quantity", 1)
        sign = 1 if leg.get("position", "long") == "long" else -1

        priced = price_leg(K, opt_type, days_to_expiry, r, chain, surface, expiry_date, fallback_spot, fallback_sigma)
        bs = black_scholes(S, K, T, r, priced["sigma"], opt_type)  # for greeks, at the same sigma just resolved
        sources_seen.add(priced["source"])
        sigmas_seen.append(priced["sigma"])
        leg_value = priced["price"] * qty * 100 * sign
        total_current_value += leg_value
        net_delta += bs["delta"] * qty * sign
        net_theta += bs["theta"] * qty * sign
        net_vega += bs["vega"] * qty * sign
        priced_legs.append({**leg, "current_premium": round(priced["price"], 4), "pricing_source": priced["source"]})

        if leg.get("premium_paid") is not None:
            total_entry_value += leg["premium_paid"] * qty * 100 * sign
        else:
            # No stored premium: reprice at entry conditions for a consistent PnL baseline
            priced_entry = price_leg(K_entry, opt_type, days_to_expiry_entry, r, chain, surface, expiry_date, S_entry, fallback_sigma)
            total_entry_value += priced_entry["price"] * qty * 100 * sign

    # Entry reference: always from legs (either stored premium or repriced at entry conditions)
    ib_entry = pos.get("ib_fees_entry", 0)
    entry_ref = total_entry_value if total_entry_value != 0 else pos["capital_invested"]
    pnl = total_current_value - entry_ref - ib_entry
    pnl_pct = (pnl / max(abs(entry_ref), 1) * 100) if entry_ref else 0

    return {
        **pos,
        "legs": priced_legs,
        "current_underlying": round(S, 4),
        "current_value": round(total_current_value, 2),
        "entry_ref": round(entry_ref, 2),
        "pnl": round(pnl, 2),
        "pnl_pct": round(pnl_pct, 2),
        "days_remaining": max(0, int(T * 365)),
        # Kept for the frontend's existing "σ (hist. IV)" display — now an average across
        # legs since each can carry its own skew-aware sigma when Saxo-priced, rather than
        # one flat value for the whole position like the old yfinance-only path.
        "sigma_used": round(sum(sigmas_seen) / len(sigmas_seen), 4) if sigmas_seen else None,
        "pricing_sources": sorted(sources_seen),
        "pricing_source_summary": next(iter(sources_seen)) if len(sources_seen) == 1 else ("mixed" if sources_seen else "yfinance_bs"),
        "greeks": {
            "net_delta": round(net_delta, 4),
            "net_theta": round(net_theta, 4),
            "net_vega": round(net_vega, 4),
        },
    }


@router.get("/positions")
def list_positions(status: str = "open"):
    positions = get_positions(status)
    if status == "open":
        return [mark_to_market(p) for p in positions]
    return positions


@router.get("/positions/{pos_id}/payoff")
def position_payoff(pos_id: str):
    """P&L-vs-underlying-price payoff diagram for one position — see
    services.portfolio_pricing.compute_payoff for the at-expiry vs. today (current Saxo
    vol held fixed) two-curve methodology."""
    from services.portfolio_pricing import compute_payoff
    pos = next((p for p in get_positions("open") + get_positions("closed") if p["id"] == pos_id), None)
    if not pos:
        raise HTTPException(status_code=404, detail=f"Position '{pos_id}' introuvable")
    return compute_payoff(pos)


@router.get("/scenario-exposure")
def scenario_exposure():
    """Reprices every open position under a handful of named macro scenarios (Risk-Off,
    Risk-On, inflation persistante, dollar fort, baisse des matières premières) to surface
    concentration on a single underlying bet across differently-named positions — see
    services.portfolio_scenarios.compute_scenario_exposure for the methodology."""
    from services.portfolio_scenarios import compute_scenario_exposure
    return compute_scenario_exposure()


@router.get("/summary")
def portfolio_summary():
    open_pos = get_positions("open")
    closed_pos = get_positions("closed")

    marked = [mark_to_market(p) for p in open_pos]
    total_invested = sum(p["capital_invested"] for p in open_pos)
    total_current = sum(p.get("current_value", p["capital_invested"]) for p in marked)
    total_pnl = sum(p.get("pnl", 0) for p in marked)
    total_fees = sum(p.get("ib_fees_entry", 0) for p in open_pos)

    realized_pnl = 0.0
    for p in closed_pos:
        if p.get("close_value") is not None:
            realized_pnl += (p["close_value"] - p["capital_invested"]
                             - p.get("ib_fees_entry", 0) - p.get("ib_fees_exit", 0))

    return {
        "open_positions": len(open_pos),
        "closed_positions": len(closed_pos),
        "total_invested": round(total_invested, 2),
        "total_current_value": round(total_current, 2),
        "unrealized_pnl": round(total_pnl, 2),
        "unrealized_pnl_pct": round(total_pnl / total_invested * 100, 2) if total_invested else 0,
        "realized_pnl": round(realized_pnl, 2),
        "total_fees_paid": round(total_fees, 2),
        "net_pnl": round(total_pnl + realized_pnl, 2),
    }


TICKER_HINTS: Dict[str, str] = {
    # Indices
    "s&p 500": "^GSPC", "sp500": "^GSPC", "s&p500": "^GSPC", "spx": "^GSPC",
    "nasdaq": "^NDX", "nasdaq 100": "^NDX", "ndx": "^NDX", "qqq": "QQQ",
    "dow jones": "^DJI", "djia": "^DJI",
    "vix": "^VIX",
    "euro stoxx": "^STOXX50E", "stoxx50": "^STOXX50E",
    "nikkei": "^N225",
    # Metals
    "gold": "GC=F", "or": "GC=F", "gold futures": "GC=F",
    "silver": "SI=F", "argent": "SI=F",
    "copper": "HG=F", "cuivre": "HG=F",
    "platinum": "PL=F", "platine": "PL=F",
    # Energy
    "wti": "CL=F", "crude oil": "CL=F", "pétrole": "CL=F", "crude": "CL=F",
    "brent": "BZ=F",
    "natural gas": "NG=F", "gaz naturel": "NG=F", "natgas": "NG=F",
    # Agriculture
    "corn": "ZC=F", "maïs": "ZC=F",
    "wheat": "ZW=F", "blé": "ZW=F",
    "soybean": "ZS=F", "soja": "ZS=F",
    "coffee": "KC=F", "café": "KC=F",
    # Forex — yfinance format: {BASE}{QUOTE}=X
    "eurusd": "EURUSD=X", "eur/usd": "EURUSD=X", "euro": "EURUSD=X",
    "usdjpy": "USDJPY=X", "usd/jpy": "USDJPY=X",
    "gbpusd": "GBPUSD=X", "gbp/usd": "GBPUSD=X",
    "usdchf": "USDCHF=X", "usd/chf": "USDCHF=X",
    "usdcnh": "USDCNH=X", "usd/cnh": "USDCNH=X",
    "usdcny": "USDCNY=X", "usd/cny": "USDCNY=X", "cny": "USDCNY=X",
    "usdrub": "USDRUB=X", "usd/rub": "USDRUB=X",
    "usdtry": "USDTRY=X", "usd/try": "USDTRY=X",
    "usdmxn": "USDMXN=X", "usd/mxn": "USDMXN=X",
    "audusd": "AUDUSD=X", "aud/usd": "AUDUSD=X",
    "dxy": "DX-Y.NYB", "dollar index": "DX-Y.NYB",
}


@router.post("/add")
def add_pos(req: AddPositionRequest):
    import traceback
    try:
        from services.portfolio_pricing import resolve_saxo_chain, price_leg

        data = req.model_dump()

        # Normalize common names to yfinance tickers
        raw = req.underlying.strip()
        normalized = TICKER_HINTS.get(raw.lower(), raw)
        data["underlying"] = normalized

        # Auto-fill entry date and expiry (needed before pricing, to size the Saxo chain fetch)
        if not data.get("entry_date"):
            data["entry_date"] = datetime.utcnow().isoformat()[:10]
        if not data.get("expiry_date") and data.get("expiry_days"):
            data["expiry_date"] = (date.today() + timedelta(days=data["expiry_days"])).isoformat()

        # Underlying price — Saxo option chain's own spot first (real, and consistent with
        # whatever prices the legs below), yfinance only if this underlying has no
        # saxo_option_symbol link at all (Config -> Instruments Watchlist -> "Option").
        chain, surface = resolve_saxo_chain(normalized, target_days=data.get("expiry_days", 90))
        S = chain["spot"] if chain else None
        sigma = None
        if S is None:
            q = get_quote(normalized)
            S = q.get("price") if q else None
            if S is not None:
                sigma = compute_historical_iv(req.underlying)
        if not S:
            hint = TICKER_HINTS.get(raw.lower())
            tip = f" Essayez '{hint}'." if hint else " Utilisez le symbole Yahoo Finance (ex: ^GSPC pour S&P 500, GC=F pour Or, CL=F pour WTI), ou liez-le à un option chain Saxo (Config → Instruments Watchlist)."
            raise HTTPException(status_code=422, detail=f"Ticker '{raw}' introuvable sur Yahoo Finance ni lié à un chain Saxo.{tip}")
        if not data.get("entry_underlying_price"):
            data["entry_underlying_price"] = S

        # Auto-price legs that have no premium_paid — Saxo-first (real quote, else the
        # Saxo-fitted vol surface), yfinance-historical-vol Black-Scholes as a last resort.
        # This ensures P&L starts at ~0 on day 1 (tracking change from entry, not vs. budget).
        if S and data.get("legs"):
            r = 0.05
            expiry_days = data.get("expiry_days", 90)
            for leg in data["legs"]:
                if not leg.get("strike"):
                    leg["strike"] = round(S, 2)  # ATM if no explicit strike
                if leg.get("premium_paid") is None:
                    priced = price_leg(
                        leg["strike"], leg.get("option_type", "call"), expiry_days, r,
                        chain, surface, data.get("expiry_date"), S, sigma or 0.20,
                    )
                    leg["premium_paid"] = round(priced["price"], 4)
                    leg["pricing_source"] = priced["source"]

        pos_id = add_position(data)
        return {"id": pos_id, "status": "added"}
    except HTTPException:
        raise
    except Exception as e:
        tb = traceback.format_exc()
        raise HTTPException(status_code=500, detail=f"{str(e)}\n\n{tb}")


@router.post("/close/{pos_id}")
def close_pos(pos_id: str, req: ClosePositionRequest):
    return close_position(pos_id, req.close_value)


@router.delete("/purge-all")
def purge_all_positions():
    """Delete ALL portfolio positions (open + closed) and related trade entry prices."""
    from services.database import get_conn
    conn = get_conn()
    conn.execute("DELETE FROM portfolio")
    conn.execute("DELETE FROM trade_entry_prices")
    n = conn.total_changes
    conn.commit()
    conn.close()
    return {"deleted": n}


@router.delete("/{pos_id}")
def delete_pos(pos_id: str):
    from services.database import get_conn
    conn = get_conn()
    conn.execute("DELETE FROM portfolio WHERE id=?", (pos_id,))
    conn.commit()
    conn.close()
    return {"status": "deleted", "id": pos_id}


@router.patch("/notes/{pos_id}")
def update_notes(pos_id: str, req: NotesRequest):
    update_position_notes(pos_id, req.notes)
    return {"status": "ok"}


@router.get("/pnl-history")
def pnl_history():
    """Equity curve from closed positions."""
    closed = get_positions("closed")
    closed_sorted = sorted(closed, key=lambda p: p.get("close_date", ""))
    curve = []
    cumulative = 0.0
    for p in closed_sorted:
        if p.get("close_value") is not None:
            pnl = (p["close_value"] - p["capital_invested"]
                   - p.get("ib_fees_entry", 0) - p.get("ib_fees_exit", 0))
            cumulative += pnl
            curve.append({
                "date": p.get("close_date", ""),
                "pnl": round(pnl, 2),
                "cumulative": round(cumulative, 2),
                "title": p.get("title", p.get("underlying", "")),
            })
    return curve
