"""
LLM integration — confirms or rejects setups found by the deterministic
strategy engine. The LLM never generates trades from scratch; it only
reviews pre-classified setups with full orderflow context.

This fixes V1's core problem: LLM generating 80% longs because it has
no structural constraint. V2 constrains the LLM to evaluate specific
setups that already passed signal alignment.
"""
from __future__ import annotations
import json
import logging
import os
import time
from dataclasses import dataclass
from typing import Optional

import requests

from .models import OrderflowSnapshot, Setup, MarketRegime

log = logging.getLogger(__name__)

# === LLM Config ===
DEFAULT_MODEL = "deepseek/deepseek-chat"
DEFAULT_BASE_URL = "https://api.deepseek.com/v1"
DEFAULT_TEMPERATURE = 0.0
DEFAULT_TIMEOUT = 30


@dataclass
class LLMDecision:
    """LLM's verdict on a setup."""
    action: str          # "confirm", "reject", "modify"
    confidence: float    # 0..1, LLM's own confidence
    reason: str          # human-readable explanation
    modified_stop: Optional[float] = None
    modified_target: Optional[float] = None


SYSTEM_PROMPT = """You are a senior orderflow trader reviewing pre-classified trade setups for Hyperliquid perpetual futures.

You receive:
1. A SETUP that was already detected by deterministic strategy scanners
2. Full ORDERFLOW CONTEXT (book, tape, CVD, absorption, regime)
3. SIGNAL ALIGNMENT CHECK showing which conditions pass/fail

Your job is to CONFIRM, REJECT, or MODIFY each setup. You do NOT generate new setups.

## Decision Rules

CONFIRM when:
- Setup type matches the orderflow evidence
- Signal alignment has 5+ of 7 conditions passing
- No absorption contradiction (e.g., long setup but ask absorption active)
- CVD not diverging against the trade direction
- Regime supports the setup (not counter-trend in strong trend)
- Spread is tight enough for the target distance
- Funding rate doesn't indicate crowding on your side

REJECT when:
- Absorption contradicts the setup direction (buyers absorbed on longs, sellers absorbed on shorts)
- CVD divergence against position (bearish divergence on longs, bullish on shorts)
- Book and tape contradict each other (book says bid-heavy but tape says sell aggression)
- Price at VWAP with no clear side (neutral zone, no edge)
- Recent high/low just swept without confirmation (potential stop hunt trap)
- Risk:reward below 1.5:1 after fees (4.5 bps each way)
- RSI exhausted (>70 for longs, <30 for shorts)

MODIFY when:
- Setup is valid but stop is too tight (inside noise) → widen stop
- Target is unrealistic → pull it to structural level (VPOC, VAH/VAL, VWAP)
- Confidence should be adjusted based on signal alignment count

## Symmetry Rule
Apply EXACTLY the same strictness to longs and shorts. Do NOT favor longs.
If you would reject a short, ask yourself if the equivalent long would also be rejected.

## Output Format
Respond with a single JSON object:
{
  "action": "confirm" | "reject" | "modify",
  "confidence": 0.0-1.0,
  "reason": "brief explanation (max 100 chars)",
  "modified_stop": null or price,
  "modified_target": null or price
}
"""


class LLMReviewer:
    """LLM-based setup reviewer."""

    def __init__(
        self,
        api_key: Optional[str] = None,
        model: str = DEFAULT_MODEL,
        base_url: str = DEFAULT_BASE_URL,
        temperature: float = DEFAULT_TEMPERATURE,
        timeout: float = DEFAULT_TIMEOUT,
    ):
        self.api_key = api_key or os.environ.get("DEEPSEEK_API_KEY", "")
        self.model = model
        self.base_url = base_url
        self.temperature = temperature
        self.timeout = timeout
        self.session = requests.Session()

    @property
    def available(self) -> bool:
        return bool(self.api_key)

    def review(
        self,
        setup: Setup,
        of: OrderflowSnapshot,
        regime: MarketRegime,
    ) -> LLMDecision:
        """Ask LLM to review a single setup."""
        if not self.available:
            log.debug("LLM not available (no API key), auto-confirming")
            return LLMDecision(
                action="confirm",
                confidence=setup.confidence,
                reason="LLM unavailable, auto-confirm",
            )

        prompt = self._build_prompt(setup, of, regime)

        try:
            response = self._call(prompt)
            return self._parse_response(response, setup)
        except Exception as e:
            log.warning(f"LLM review failed: {e}, auto-confirming")
            return LLMDecision(
                action="confirm",
                confidence=setup.confidence,
                reason=f"LLM error: {e}",
            )

    def review_batch(
        self,
        setups: list[Setup],
        of: OrderflowSnapshot,
        regime: MarketRegime,
    ) -> list[LLMDecision]:
        """Review multiple setups in one LLM call."""
        if not setups:
            return []
        if not self.available:
            return [
                LLMDecision("confirm", s.confidence, "LLM unavailable")
                for s in setups
            ]

        prompt = self._build_batch_prompt(setups, of, regime)
        try:
            response = self._call(prompt)
            return self._parse_batch_response(response, setups)
        except Exception as e:
            log.warning(f"LLM batch review failed: {e}")
            return [
                LLMDecision("confirm", s.confidence, f"LLM error: {e}")
                for s in setups
            ]

    def _build_prompt(self, setup: Setup, of: OrderflowSnapshot, regime: MarketRegime) -> str:
        alignment = self._signal_alignment(of, setup.side)
        return f"""SETUP TO REVIEW:
- Type: {setup.setup_type}
- Side: {setup.side}
- Coin: {setup.coin}
- Entry: {setup.entry_ref}
- Stop: {setup.invalidation}
- Target: {setup.target}
- R:R: {setup.rr:.2f}
- Confidence: {setup.confidence:.0%}
- Evidence: {', '.join(setup.evidence)}

ORDERFLOW CONTEXT:
- Mid Price: {of.mid}
- Spread: {of.spread_bps:.1f} bps
- Book Imbalance: {of.book_imbalance:+.3f} ({'bid-heavy' if of.book_imbalance > 0 else 'ask-heavy'})
- Wall Skew: {of.wall_skew:+.3f}
- Stacked Bids: {of.stacked_bid_count} / Asks: {of.stacked_ask_count}
- Tape Delta: {of.tape_delta_pct:+.3f} ({'buy aggression' if of.tape_delta_pct > 0 else 'sell aggression'})
- CVD: {of.cvd:.0f} (divergence: {'YES' if of.cvd_divergence else 'no'})
- Bid Absorption: {of.bid_absorption:.2f} / Ask Absorption: {of.ask_absorption:.2f}
- OMNI Score: {of.omni_score:+.2f} (stance: {of.stance})
- RSI(14): {of.rsi_14:.1f}
- MACD Hist: {of.macd_histogram:.6f}
- ATR(14): {of.atr_14:.6f}
- VWAP: {of.vwap:.6f} (price {of.price_vs_vwap:+.3%} vs VWAP)
- VPOC: {of.vpoc:.6f} / VAH: {of.vah:.6f} / VAL: {of.val:.6f}
- Funding: {of.funding_rate:.6f}
- Premium: {of.premium_bps:+.1f} bps

REGIME:
- 15m: {regime.trend_15m} / 1h: {regime.trend_1h} / 4h: {regime.trend_4h}
- Volatility: {regime.vol_regime}
- Overall: {regime.overall}

SIGNAL ALIGNMENT ({setup.side.upper()}):
{alignment}

Review this setup. JSON only."""

    def _build_batch_prompt(self, setups: list[Setup], of: OrderflowSnapshot, regime: MarketRegime) -> str:
        parts = [f"ORDERFLOW CONTEXT for all setups:\n- Mid: {of.mid}, Spread: {of.spread_bps:.1f} bps"]
        parts.append(f"- Book Imbalance: {of.book_imbalance:+.3f}, Tape Delta: {of.tape_delta_pct:+.3f}")
        parts.append(f"- CVD: {of.cvd:.0f}, Divergence: {of.cvd_divergence}")
        parts.append(f"- Absorption: bid={of.bid_absorption:.2f} ask={of.ask_absorption:.2f}")
        parts.append(f"- RSI: {of.rsi_14:.1f}, MACD: {of.macd_histogram:.6f}")
        parts.append(f"- VWAP: {of.vwap:.6f}, VPOC: {of.vpoc:.6f}")
        parts.append(f"- Regime: {regime.overall} (15m={regime.trend_15m}, 1h={regime.trend_1h})")
        parts.append(f"- Funding: {of.funding_rate:.6f}")
        parts.append("")

        for i, s in enumerate(setups):
            alignment = self._signal_alignment(of, s.side)
            parts.append(f"SETUP {i+1}: {s.setup_type} {s.side} {s.coin}")
            parts.append(f"  Entry={s.entry_ref} Stop={s.invalidation} Target={s.target} R:R={s.rr:.2f} Conf={s.confidence:.0%}")
            parts.append(f"  Evidence: {', '.join(s.evidence)}")
            parts.append(f"  Alignment: {alignment}")
            parts.append("")

        parts.append(f"Review all {len(setups)} setups. Return JSON array of {len(setups)} objects.")
        return "\n".join(parts)

    def _signal_alignment(self, of: OrderflowSnapshot, side: str) -> str:
        """Build signal alignment checklist string."""
        if side == "long":
            checks = [
                ("Book bid-heavy", of.book_imbalance > 0.15, f"{of.book_imbalance:+.3f}"),
                ("Tape buy aggression", of.tape_delta_pct > 0.15, f"{of.tape_delta_pct:+.3f}"),
                ("Near/above VWAP", of.price_vs_vwap > -0.005, f"{of.price_vs_vwap:+.4f}"),
                ("MACD bullish", of.macd_histogram >= 0, f"{of.macd_histogram:.6f}"),
                ("RSI not exhausted", 35 <= of.rsi_14 <= 65, f"{of.rsi_14:.1f}"),
                ("No ask absorption", of.ask_absorption < 0.6, f"{of.ask_absorption:.2f}"),
                ("Funding not crowded", of.funding_rate < 0.001, f"{of.funding_rate:.6f}"),
            ]
        else:
            checks = [
                ("Book ask-heavy", of.book_imbalance < -0.15, f"{of.book_imbalance:+.3f}"),
                ("Tape sell aggression", of.tape_delta_pct < -0.15, f"{of.tape_delta_pct:+.3f}"),
                ("Near/below VWAP", of.price_vs_vwap < 0.005, f"{of.price_vs_vwap:+.4f}"),
                ("MACD bearish", of.macd_histogram <= 0, f"{of.macd_histogram:.6f}"),
                ("RSI not exhausted", 35 <= of.rsi_14 <= 65, f"{of.rsi_14:.1f}"),
                ("No bid absorption", of.bid_absorption < 0.6, f"{of.bid_absorption:.2f}"),
                ("Funding not crowded", of.funding_rate > -0.001, f"{of.funding_rate:.6f}"),
            ]

        lines = []
        for label, passed, val in checks:
            mark = "PASS" if passed else "FAIL"
            lines.append(f"  [{mark}] {label}: {val}")

        passing = sum(1 for _, p, _ in checks if p)
        lines.append(f"  Score: {passing}/7")
        return "\n".join(lines)

    def _call(self, prompt: str) -> str:
        """Call LLM API."""
        url = f"{self.base_url}/chat/completions"
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }
        payload = {
            "model": self.model.split("/")[-1],
            "messages": [
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": prompt},
            ],
            "temperature": self.temperature,
            "max_tokens": 500,
            "response_format": {"type": "json_object"},
        }

        r = self.session.post(url, json=payload, headers=headers, timeout=self.timeout)
        r.raise_for_status()
        data = r.json()
        return data["choices"][0]["message"]["content"]

    def _parse_response(self, raw: str, setup: Setup) -> LLMDecision:
        """Parse single LLM response."""
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            return LLMDecision("confirm", setup.confidence, "LLM response not JSON")

        return LLMDecision(
            action=data.get("action", "confirm"),
            confidence=max(0, min(1, float(data.get("confidence", setup.confidence)))),
            reason=data.get("reason", "")[:100],
            modified_stop=data.get("modified_stop"),
            modified_target=data.get("modified_target"),
        )

    def _parse_batch_response(self, raw: str, setups: list[Setup]) -> list[LLMDecision]:
        """Parse batch LLM response."""
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            return [LLMDecision("confirm", s.confidence, "LLM parse error") for s in setups]

        if isinstance(data, list):
            decisions = []
            for i, s in enumerate(setups):
                if i < len(data):
                    d = data[i]
                    decisions.append(LLMDecision(
                        action=d.get("action", "confirm"),
                        confidence=max(0, min(1, float(d.get("confidence", s.confidence)))),
                        reason=d.get("reason", "")[:100],
                        modified_stop=d.get("modified_stop"),
                        modified_target=d.get("modified_target"),
                    ))
                else:
                    decisions.append(LLMDecision("confirm", s.confidence, "LLM missing response"))
            return decisions

        # Single object for all
        return [self._parse_response(raw, s) for s in setups]


# === Simulated LLM for Backtesting (no API calls) ===

class SimulatedLLMReviewer:
    """
    Simulates LLM review using the same signal alignment matrix.
    Used for backtesting without API costs.
    """

    def review(
        self,
        setup: Setup,
        of: OrderflowSnapshot,
        regime: MarketRegime,
    ) -> LLMDecision:
        """Simulate LLM review using deterministic checks."""
        side = setup.side
        reasons = []
        fails = 0

        if side == "long":
            # Absorption contradiction
            if of.ask_absorption > 0.6:
                fails += 2
                reasons.append("ask_absorption contradicts long")

            # CVD divergence against
            if of.cvd_divergence and of.cvd < 0:
                fails += 2
                reasons.append("bearish CVD divergence on long")

            # Book/tape contradiction
            if of.book_imbalance > 0.15 and of.tape_delta_pct < -0.2:
                fails += 1
                reasons.append("book bid-heavy but tape selling")

            # RSI exhausted
            if of.rsi_14 > 70:
                fails += 1
                reasons.append(f"RSI {of.rsi_14:.0f} overbought")

            # Against strong trend
            if regime.trend_1h == "down" and regime.trend_15m == "down":
                fails += 1
                reasons.append("counter-trend in downtrend")

        else:  # short
            if of.bid_absorption > 0.6:
                fails += 2
                reasons.append("bid_absorption contradicts short")

            if of.cvd_divergence and of.cvd > 0:
                fails += 2
                reasons.append("bullish CVD divergence on short")

            if of.book_imbalance < -0.15 and of.tape_delta_pct > 0.2:
                fails += 1
                reasons.append("book ask-heavy but tape buying")

            if of.rsi_14 < 30:
                fails += 1
                reasons.append(f"RSI {of.rsi_14:.0f} oversold")

            if regime.trend_1h == "up" and regime.trend_15m == "up":
                fails += 1
                reasons.append("counter-trend in uptrend")

        # Common checks
        if setup.rr < 1.5:
            fails += 1
            reasons.append(f"R:R {setup.rr:.2f} too low")

        if of.spread_bps > 5.0:
            fails += 1
            reasons.append(f"spread {of.spread_bps:.1f} bps too wide")

        if fails >= 3:
            return LLMDecision("reject", setup.confidence * 0.3, "; ".join(reasons))
        elif fails >= 1:
            return LLMDecision("modify", setup.confidence * 0.7, "; ".join(reasons))
        else:
            return LLMDecision("confirm", setup.confidence, "all checks pass")

    @property
    def available(self) -> bool:
        return True
