"""
Akinator 2.0 — Advanced Multi-Agent Investment Prediction Engine
Built with LangGraph StateGraph: 9 interconnected nodes

Graph Architecture:
  START → Router → [conditional] → What-If Engine → Analyst Hub
                                 → Analyst Hub (direct)
  Analyst Hub → News Sentiment → Confidence Scorer → Critique
  → JIT Education → Memo Generator → Format Response → END
"""

import json
import re
import urllib.request
import urllib.parse
from datetime import datetime, timedelta
from typing import TypedDict, Literal, Optional

from langchain_groq import ChatGroq
from langchain_core.messages import HumanMessage, AIMessage
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool

from config import get_settings


# ── In-memory session store ──────────────────────────────────────
_akinator_sessions: dict[str, list] = {}


# ═══════════════════════════════════════════════════════════════════
# GRAPH STATE SCHEMA — persists data across all nodes
# ═══════════════════════════════════════════════════════════════════

class AkinatorState(TypedDict):
    messages: list              # Conversation history
    user_query: str             # Current user message
    analyst_report: str         # Raw output from ReAct analyst
    news_sentiment: dict        # {headlines, score, label, topic}
    confidence_score: float     # 0-100 confidence percentage
    critique_note: str          # Self-correction note
    whatif_result: str           # What-if analysis result
    jargon_definitions: dict    # {term: definition}
    final_memo: str             # Compiled investment memo
    panel_mode: bool            # Panel discussion mode
    is_whatif: bool             # Whether query is a what-if scenario
    final_response: str         # Combined final response to user
    knowledge_base_id: str      # Optional selected KB ID
    rag_used: bool              # Whether the RAG tool was called


# ═══════════════════════════════════════════════════════════════════
# ANALYST TOOLS — called by ReAct agent inside analyst_hub node
# ═══════════════════════════════════════════════════════════════════

@tool
def market_data_analyst(query: str) -> str:
    """
    Market Data Analyst Agent: Fetches real-time and historical stock/asset prices,
    key metrics (PE ratio, market cap, 52-week range), and recent performance data.
    Use this when you need hard numbers: prices, returns, volumes, or financial ratios.
    Input should be a stock symbol like 'AAPL' or asset name like 'Gold' or 'S&P 500'.
    """
    try:
        import yfinance as yf
        symbols_to_try = [query.upper().strip()]
        mappings = {
            "GOLD": "GC=F", "SILVER": "SI=F", "OIL": "CL=F", "CRUDE": "CL=F",
            "S&P": "^GSPC", "S&P 500": "^GSPC", "SP500": "^GSPC",
            "NASDAQ": "^IXIC", "DOW": "^DJI", "BITCOIN": "BTC-USD", "BTC": "BTC-USD",
            "ETHEREUM": "ETH-USD", "ETH": "ETH-USD", "EUR": "EURUSD=X",
            "EGP": "USDEGP=X", "USD/EGP": "USDEGP=X", "EGYPTIAN POUND": "USDEGP=X",
            "EGX30": "^CASE30", "EGX 30": "^CASE30", "EGYPT": "^CASE30",
        }
        mapped = mappings.get(query.upper().strip())
        if mapped:
            symbols_to_try.insert(0, mapped)

        for symbol in symbols_to_try:
            try:
                ticker = yf.Ticker(symbol)
                info = ticker.info
                if not info or info.get("regularMarketPrice") is None:
                    continue
                hist = ticker.history(period="3mo")
                price = info.get("regularMarketPrice") or info.get("currentPrice", "N/A")
                returns_data = {}
                if not hist.empty and len(hist) > 1:
                    current = hist["Close"].iloc[-1]
                    if len(hist) >= 5:
                        returns_data["5d_return"] = f"{((current / hist['Close'].iloc[-5]) - 1) * 100:.2f}%"
                    if len(hist) >= 22:
                        returns_data["1m_return"] = f"{((current / hist['Close'].iloc[-22]) - 1) * 100:.2f}%"
                    if len(hist) >= 63:
                        returns_data["3m_return"] = f"{((current / hist['Close'].iloc[-63]) - 1) * 100:.2f}%"
                result = {
                    "symbol": symbol,
                    "name": info.get("shortName", info.get("longName", symbol)),
                    "price": price, "currency": info.get("currency", "USD"),
                    "market_cap": info.get("marketCap"), "pe_ratio": info.get("trailingPE"),
                    "52w_high": info.get("fiftyTwoWeekHigh"), "52w_low": info.get("fiftyTwoWeekLow"),
                    "day_change": info.get("regularMarketChangePercent"),
                    "volume": info.get("regularMarketVolume"),
                    "sector": info.get("sector"), "industry": info.get("industry"),
                    "dividend_yield": info.get("dividendYield"), "returns": returns_data,
                }
                result = {k: v for k, v in result.items() if v is not None}
                return f"[Market Analyst Report]\n{json.dumps(result, indent=2)}"
            except Exception:
                continue
        return f"[Market Analyst] Could not find market data for '{query}'. Try a specific ticker symbol."
    except Exception as e:
        return f"[Market Analyst Error] {str(e)}"


@tool
def news_sentiment_analyst(query: str) -> str:
    """
    News & Sentiment Analyst Agent: Searches the web for the latest financial news,
    analyst opinions, earnings reports, and market sentiment about a topic.
    Use this when you need current events, news context, or market sentiment.
    """
    settings = get_settings()
    if not settings.SERPAPI_API_KEY:
        return "[News Analyst] SerpAPI key not configured."
    url = f"https://serpapi.com/search.json?q={urllib.parse.quote(query + ' investment analysis')}&api_key={settings.SERPAPI_API_KEY}&num=6"
    try:
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=10) as response:
            data = json.loads(response.read().decode())
            results = []
            if "answer_box" in data:
                ans = data["answer_box"].get("answer") or data["answer_box"].get("snippet") or ""
                if ans:
                    results.append(f"Quick Answer: {ans}")
            if "organic_results" in data:
                for res in data["organic_results"][:5]:
                    results.append(f"• {res.get('title', '')}: {res.get('snippet', '')}")
            if not results:
                return f"[News Analyst] No recent news found for '{query}'"
            return f"[News & Sentiment Analyst Report]\n" + "\n".join(results)
    except Exception as e:
        return f"[News Analyst Error] {str(e)}"


@tool
def risk_assessment_analyst(query: str) -> str:
    """
    Risk Assessment Analyst Agent: Analyzes the volatility, risk metrics, and downside
    potential of a given asset. Calculates historical volatility and drawdown stats.
    """
    try:
        import yfinance as yf
        import numpy as np
        mappings = {
            "GOLD": "GC=F", "BITCOIN": "BTC-USD", "BTC": "BTC-USD",
            "S&P": "^GSPC", "S&P 500": "^GSPC", "EGX30": "^CASE30",
            "EGYPT": "^CASE30", "EGP": "USDEGP=X",
        }
        symbol = mappings.get(query.upper().strip(), query.upper().strip())
        ticker = yf.Ticker(symbol)
        hist = ticker.history(period="1y")
        if hist.empty or len(hist) < 30:
            return f"[Risk Analyst] Insufficient data for '{query}'."
        closes = hist["Close"].values
        returns = np.diff(closes) / closes[:-1]
        daily_vol = np.std(returns)
        annual_vol = daily_vol * np.sqrt(252)
        max_drawdown = 0
        peak = closes[0]
        for p in closes:
            if p > peak:
                peak = p
            dd = (peak - p) / peak
            if dd > max_drawdown:
                max_drawdown = dd
        mean_return = np.mean(returns) * 252
        sharpe = (mean_return - 0.05) / annual_vol if annual_vol > 0 else 0
        if annual_vol > 0.5:
            risk_level = "VERY HIGH"
        elif annual_vol > 0.3:
            risk_level = "HIGH"
        elif annual_vol > 0.15:
            risk_level = "MODERATE"
        else:
            risk_level = "LOW"
        result = {
            "symbol": symbol, "risk_level": risk_level,
            "annual_volatility": f"{annual_vol * 100:.1f}%",
            "max_drawdown": f"{max_drawdown * 100:.1f}%",
            "sharpe_ratio": f"{sharpe:.2f}",
            "annualized_return": f"{mean_return * 100:.1f}%",
            "worst_day": f"{min(returns) * 100:.2f}%",
            "best_day": f"{max(returns) * 100:.2f}%",
            "current_vs_52w_high": f"{((closes[-1] / max(closes)) - 1) * 100:.1f}%",
        }
        return f"[Risk Assessment Report]\n{json.dumps(result, indent=2)}"
    except Exception as e:
        return f"[Risk Analyst Error] {str(e)}"


@tool
def portfolio_strategy_search(query: str) -> str:
    """
    Portfolio Strategy Analyst Agent: Searches for professional portfolio allocation
    strategies, investment frameworks, and expert recommendations relevant to the query.
    """
    settings = get_settings()
    if not settings.SERPAPI_API_KEY:
        return "[Strategy Analyst] SerpAPI key not configured."
    url = f"https://serpapi.com/search.json?q={urllib.parse.quote(query + ' portfolio strategy expert advice')}&api_key={settings.SERPAPI_API_KEY}&num=5"
    try:
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=10) as response:
            data = json.loads(response.read().decode())
            results = []
            if "organic_results" in data:
                for res in data["organic_results"][:4]:
                    results.append(f"• {res.get('title', '')}: {res.get('snippet', '')}")
            if not results:
                return f"[Strategy Analyst] No strategy advice found for '{query}'"
            return f"[Portfolio Strategy Research]\n" + "\n".join(results)
    except Exception as e:
        return f"[Strategy Analyst Error] {str(e)}"


@tool
def fetch_news_headlines(query: str) -> str:
    """NewsAPI Integration: Fetches top news headlines for a stock/topic via SerpAPI News tab.
    Used by the News Sentiment node for headline-based sentiment scoring."""
    settings = get_settings()
    if not settings.SERPAPI_API_KEY:
        return "[NewsAPI] API key not configured."
    url = f"https://serpapi.com/search.json?q={urllib.parse.quote(query)}&api_key={settings.SERPAPI_API_KEY}&tbm=nws&num=8"
    try:
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=10) as response:
            data = json.loads(response.read().decode())
            if "news_results" not in data:
                return "[NewsAPI] No news results found."
            headlines = []
            for n in data["news_results"][:8]:
                headlines.append(f"• {n.get('title', '')} ({n.get('source', {}).get('name', 'Unknown')})")
            return "[News Headlines]\n" + "\n".join(headlines)
    except Exception as e:
        return f"[NewsAPI Error] {str(e)}"


# ═══════════════════════════════════════════════════════════════════
# JIT EDUCATION — Financial jargon dictionary
# ═══════════════════════════════════════════════════════════════════

JARGON_DICT = {
    "P/E Ratio": "Price-to-Earnings Ratio — how much investors pay for each dollar of earnings. High P/E = expensive, Low P/E = cheap.",
    "Market Cap": "Total value of a company's shares. Calculated as Stock Price x Number of Shares.",
    "Volatility": "How much a price swings up and down. High volatility = risky but potentially rewarding.",
    "Sharpe Ratio": "Measures return per unit of risk. Above 1 is good, above 2 is great, below 0 means losing money.",
    "Drawdown": "The peak-to-trough decline. A 20% drawdown means the price fell 20% from its highest point.",
    "Dividend Yield": "Annual dividend payment as a percentage of stock price. Like earning interest on your investment.",
    "RSI": "Relative Strength Index — momentum indicator. Above 70 = overbought (may drop), Below 30 = oversold (may rise).",
    "MACD": "Moving Average Convergence Divergence — shows trend direction and momentum. Crossovers signal buy/sell.",
    "Bollinger Bands": "Price channels around a moving average. Price near upper band = potentially overbought, near lower = oversold.",
    "52-Week Range": "The lowest and highest prices in the past year. Shows how current price compares to recent history.",
    "Beta": "How much a stock moves relative to the market. Beta > 1 = more volatile, Beta < 1 = more stable.",
    "EPS": "Earnings Per Share — company's profit divided by number of shares. Higher is better.",
    "ROI": "Return on Investment — profit as a percentage of what you invested.",
    "Bull Market": "A market that is rising. Prices are going up and optimism is high.",
    "Bear Market": "A market that is falling. Prices are dropping and pessimism grows.",
    "Hedge": "A strategy to reduce risk by taking an offsetting position. Like insurance for your investment.",
    "Liquidity": "How easily you can buy or sell without affecting the price. High liquidity = easy to trade.",
    "Portfolio": "Your collection of investments — stocks, bonds, cash, etc. Diversification spreads risk across assets.",
    "ETF": "Exchange-Traded Fund — a basket of stocks/bonds you can buy as a single investment. Easy diversification.",
    "Stop-Loss": "An automatic sell order that triggers when price drops to a set level. Limits your losses.",
    "Support Level": "A price floor where buying pressure tends to prevent further drops.",
    "Resistance Level": "A price ceiling where selling pressure tends to prevent further rises.",
    "Annualized Return": "Your return scaled to one year. A 5% return in 6 months is roughly 10% annualized.",
    "Blue Chip": "Large, stable, well-established company with a history of reliable performance.",
    "Diversification": "Spreading investments across different assets to reduce risk. Do not put all eggs in one basket.",
    "Short Selling": "Betting that a stock will go DOWN. You borrow shares, sell them, then buy back cheaper (hopefully).",
    "Margin": "Borrowing money from your broker to buy more stocks. Amplifies both gains AND losses.",
    "IPO": "Initial Public Offering — when a company first sells its stock to the public.",
    "Sector": "A segment of the economy grouping similar businesses — like Tech, Healthcare, or Energy.",
}

# ═══════════════════════════════════════════════════════════════════
# SENTIMENT SCORING KEYWORDS
# ═══════════════════════════════════════════════════════════════════

POSITIVE_WORDS = {
    "surge", "jump", "gain", "rally", "rise", "climb", "soar", "boost", "growth", "profit",
    "bullish", "upgrade", "beat", "record", "high", "strong", "outperform", "positive",
    "opportunity", "momentum", "recover", "breakout", "optimistic", "confident",
}
NEGATIVE_WORDS = {
    "crash", "drop", "fall", "decline", "plunge", "sink", "loss", "bearish", "downgrade",
    "miss", "low", "weak", "underperform", "negative", "risk", "fear", "recession",
    "selloff", "bubble", "warning", "cut", "concern", "volatile", "uncertainty",
}


# ═══════════════════════════════════════════════════════════════════
# SYSTEM PROMPTS
# ═══════════════════════════════════════════════════════════════════

AKINATOR_SYSTEM_PROMPT = """You are Akinator 2.0 — an elite Multi-Agent Investment Prediction Engine.

You operate as a panel of 4 expert analysts working together:
1. Market Data Analyst — Fetches real prices, ratios, returns
2. News & Sentiment Analyst — Finds latest news and market sentiment
3. Risk Assessment Analyst — Evaluates volatility and downside risk
4. Portfolio Strategy Advisor — Recommends allocation and strategy

YOUR PROCESS (Follow this EVERY time):

Step 1 — Gather Intelligence: Use ALL FOUR tools to collect data from every angle. DO NOT skip any tool.

Step 2 — Cross-Validate: Compare findings across all 4 analysts. Note agreements and contradictions.

Step 3 — Synthesize & Predict: Deliver your final recommendation using this format:

Akinator 2.0 Prediction Report

Market Analyst Findings:
[Summary of price data, trends, key metrics]

News & Sentiment Analysis:
[Summary of current news, sentiment direction, catalysts]

Risk Assessment:
[Risk level, volatility, max drawdown, risk-reward evaluation]

Strategic Recommendation:
[Clear, actionable advice: what to do, how to allocate, with reasoning]

Confidence Level:
[Rate your confidence: High/Medium/Low with explanation]

Disclaimer: This analysis is AI-generated for educational purposes. Always consult a licensed financial advisor before investing.

RULES:
- Today's date is {today}.
- ALWAYS use ALL 4 tools before giving your answer. This is MANDATORY.
- Give SPECIFIC numbers, prices, and percentages — never be vague.
- If the user asks about Egyptian investments, search for EGX 30, Egyptian stocks, EGP exchange rates, and Egyptian gold prices.
- For currency amounts in EGP, convert and compare opportunities in Egyptian market AND global markets.
- Be bold with predictions but honest about uncertainty.
- Keep responses well-structured and scannable.
- Investment topics ONLY — politely decline non-investment questions.
- IMPORTANT: Do NOT use markdown formatting like ## or ** in your response. Use plain text with clear section headers followed by colons. Use bullet points with dashes (-) for lists. Keep it clean and readable.
"""

PANEL_DISCUSSION_ADDENDUM = """

PANEL DISCUSSION MODE — ACTIVATED

BEFORE giving your final prediction, you MUST simulate a live roundtable discussion between 10 distinct investor personas. They will debate using the REAL DATA gathered by the 4 analyst tools above.

THE PANEL OF 10:

1. Diana "The Shield" Reeves (Risk Manager) — Ultra-conservative. Obsessed with downside protection.
2. Dr. Marcus Chen (The Quant) — Pure data, zero emotion. Speaks in numbers and ratios.
3. Jake "The Shark" Morrison (Aggressive Trader) — High risk, high reward. Loves volatility.
4. Eleanor Whitfield (Value Investor) — Warren Buffett disciple. Thinks in decades.
5. Professor Amir Hassan (Macro Economist) — Central banks, geopolitics, interest rates.
6. Kai Nakamura (Technical Analyst) — Lives and breathes charts. RSI, MACD, Bollinger Bands.
7. Victoria Sterling (Institutional Banker) — Conservative, establishment perspective.
8. Zack "Moon" Rodriguez (Crypto/Fintech) — Digital-first, loves disruption.
9. Dr. Sarah Park (Behavioral Psychologist) — Market psychology, herd behavior, FOMO.
10. Lena Greenwald (ESG Advocate) — Sustainable investing champion.

DISCUSSION RULES:
- Write it as a NATURAL, LIVELY conversation — not a list of opinions
- Personas MUST reference the ACTUAL data from the 4 analyst tools
- They should DISAGREE, interrupt each other, challenge assumptions
- Include 2-3 rounds of back-and-forth debate
- Each persona speaks IN CHARACTER with their distinct voice

After the discussion, present:
1. Panel Vote table (Persona | Verdict | Reason)
2. Panel Consensus summary
3. Debate Summary with key arguments, alliances, disagreements
4. Final Akinator 2.0 Prediction
5. Confidence Level

IMPORTANT: Do NOT use markdown formatting like ## or ** in your response. Use plain structured text.
"""


# ═══════════════════════════════════════════════════════════════════
# NODE 1: ROUTER — Classifies query type
# ═══════════════════════════════════════════════════════════════════

def router_node(state: AkinatorState) -> dict:
    """Classifies whether the user query is a What-If scenario or a regular query."""
    query = state["user_query"].lower()
    whatif_patterns = [
        r"what if", r"what would", r"had i", r"if i had", r"would have",
        r"hypothetically", r"back in", r"in hindsight", r"if i bought",
        r"if i invested", r"if i sold", r"imagine i", r"suppose i",
        r"\d+\s*years?\s*ago", r"\d+\s*months?\s*ago",
    ]
    is_whatif = any(re.search(p, query) for p in whatif_patterns)
    print(f"[Router Node] Query: {query[:60]}... | is_whatif={is_whatif}")
    return {"is_whatif": is_whatif}


# ═══════════════════════════════════════════════════════════════════
# NODE 2: WHAT-IF ENGINE — Historical scenario analysis
# ═══════════════════════════════════════════════════════════════════

def whatif_engine_node(state: AkinatorState) -> dict:
    """Analyzes hypothetical past investment scenarios with real historical data."""
    query = state["user_query"]
    print(f"[What-If Engine] Analyzing: {query[:80]}...")
    try:
        import yfinance as yf
        q_lower = query.lower()

        # Extract ticker
        ticker = None
        common_tickers = {
            "apple": "AAPL", "google": "GOOGL", "amazon": "AMZN", "tesla": "TSLA",
            "microsoft": "MSFT", "nvidia": "NVDA", "meta": "META", "netflix": "NFLX",
            "bitcoin": "BTC-USD", "ethereum": "ETH-USD", "gold": "GC=F", "silver": "SI=F",
            "spy": "SPY", "s&p": "SPY", "nasdaq": "QQQ", "dow": "DIA",
        }
        for name, sym in common_tickers.items():
            if name in q_lower:
                ticker = sym
                break
        if not ticker:
            ticker_match = re.search(r'\b([A-Z]{2,5})\b', query)
            if ticker_match:
                ticker = ticker_match.group(1)
        if not ticker:
            ticker = "SPY"

        # Extract time period
        years_ago = 1
        year_match = re.search(r'(\d+)\s*years?\s*ago', q_lower)
        month_match = re.search(r'(\d+)\s*months?\s*ago', q_lower)
        specific_year = re.search(r'in\s*(20\d{2}|19\d{2})', q_lower)
        if year_match:
            years_ago = int(year_match.group(1))
        elif month_match:
            years_ago = int(month_match.group(1)) / 12
        elif specific_year:
            years_ago = datetime.now().year - int(specific_year.group(1))

        # Extract investment amount
        amount = 10000
        amount_match = re.search(r'[\$]?\s*(\d[\d,]*(?:\.\d{2})?)', query)
        if amount_match:
            parsed_amount = float(amount_match.group(1).replace(',', ''))
            if parsed_amount >= 10:
                amount = parsed_amount

        # Fetch historical data
        start_date = datetime.now() - timedelta(days=int(years_ago * 365))
        data = yf.download(ticker, start=start_date.strftime('%Y-%m-%d'), progress=False)

        if data.empty or len(data) < 2:
            return {"whatif_result": f"Could not fetch historical data for {ticker}."}

        # Handle multi-level columns from yfinance (newer versions)
        close_series = data['Close']
        if hasattr(close_series, 'columns'):
            close_series = close_series.iloc[:, 0]  # DataFrame -> take first column

        buy_price = float(close_series.iloc[0])
        current_price = float(close_series.iloc[-1])
        shares = amount / buy_price
        current_value = shares * current_price
        profit_loss = current_value - amount
        pct_return = ((current_value / amount) - 1) * 100

        # Max drawdown
        closes = close_series.values.flatten()
        peak = float(closes[0])
        max_dd = 0
        for p in closes:
            pf = float(p)
            if pf > peak:
                peak = pf
            dd = (peak - pf) / peak
            if dd > max_dd:
                max_dd = dd

        max_price = float(close_series.max())
        max_value = shares * max_price
        buy_date = data.index[0].strftime('%Y-%m-%d')

        verdict = "This would have been a profitable investment!" if profit_loss > 0 else "This would have resulted in a loss."
        if pct_return > 100:
            verdict += " Your money would have more than doubled!"

        result = (
            f"What-If Analysis: {ticker}\n\n"
            f"Investment Details:\n"
            f"  - Amount Invested: ${amount:,.2f}\n"
            f"  - Buy Date: {buy_date}\n"
            f"  - Buy Price: ${buy_price:,.2f}\n"
            f"  - Shares Purchased: {shares:,.4f}\n\n"
            f"Current Outcome:\n"
            f"  - Current Price: ${current_price:,.2f}\n"
            f"  - Current Value: ${current_value:,.2f}\n"
            f"  - {'Profit' if profit_loss >= 0 else 'Loss'}: ${abs(profit_loss):,.2f}\n"
            f"  - Total Return: {pct_return:+.2f}%\n\n"
            f"Peak Performance:\n"
            f"  - Highest Price: ${max_price:,.2f}\n"
            f"  - Peak Value: ${max_value:,.2f} ({((max_value / amount) - 1) * 100:+.1f}%)\n"
            f"  - Max Drawdown: {max_dd * 100:.1f}%\n\n"
            f"Verdict: {verdict}"
        )
        return {"whatif_result": result}
    except Exception as e:
        print(f"[What-If Engine Error] {e}")
        return {"whatif_result": f"What-If analysis error: {str(e)}"}


# ═══════════════════════════════════════════════════════════════════
# NODE 3: ANALYST HUB — Main ReAct agent with tools
# ═══════════════════════════════════════════════════════════════════

def analyst_hub_node(state: AkinatorState) -> dict:
    """Core analysis node — runs the ReAct agent with all 5 analyst tools."""
    print("[Analyst Hub] Running multi-agent analysis...")
    settings = get_settings()
    llm = ChatGroq(
        api_key=settings.GROQ_API_KEY,
        model_name=settings.GROQ_MODEL,
        temperature=0.6 if state.get("panel_mode") else 0.5,
        max_tokens=12000 if state.get("panel_mode") else 4000,
    )

    today = datetime.now().strftime("%Y-%m-%d")
    prompt = AKINATOR_SYSTEM_PROMPT.replace("{today}", today)
    if state.get("panel_mode"):
        prompt += PANEL_DISCUSSION_ADDENDUM

    from services.rag_engine import search_kb

    @tool
    def search_knowledge_base(query: str) -> str:
        """Searches the user-selected Knowledge Base for financial documents, PDFs, and definitions."""
        kb_id = state.get("knowledge_base_id")
        if not kb_id:
            return "No Knowledge Base selected."
        return search_kb(kb_id, query)

    tools = [market_data_analyst, news_sentiment_analyst, risk_assessment_analyst,
             portfolio_strategy_search, fetch_news_headlines, search_knowledge_base]
    agent = create_react_agent(llm, tools, prompt=prompt)

    # Build messages with optional what-if context
    msgs = list(state.get("messages", []))
    user_msg = state["user_query"]
    if state.get("whatif_result"):
        user_msg += f"\n\n[What-If Analysis Data — incorporate this into your response]:\n{state['whatif_result']}"
    msgs.append(HumanMessage(content=user_msg))

    # Try with tools first; retry without tools if Groq fails to generate a valid tool call
    answer = ""
    for attempt in range(2):
        try:
            if attempt == 0:
                response = agent.invoke({"messages": msgs})
                from langchain_core.messages import ToolMessage
                for msg in response["messages"]:
                    if isinstance(msg, ToolMessage) and msg.name == "search_knowledge_base":
                        state["rag_used"] = True
            else:
                # Retry: direct LLM call without tools (fallback)
                print("[Analyst Hub] Retrying without tools (fallback)...")
                fallback_response = llm.invoke(msgs)
                answer = fallback_response.content if isinstance(fallback_response.content, str) else str(fallback_response.content)
                break

            # Extract the final AI response
            for msg in reversed(response["messages"]):
                from langchain_core.messages import AIMessage
                if isinstance(msg, AIMessage):
                    content = msg.content
                    if isinstance(content, str) and content.strip():
                        answer = content.strip()
                        break
                    elif isinstance(content, list):
                        parts = [b.get("text", "") if isinstance(b, dict) else str(b) for b in content]
                        joined = "".join(parts).strip()
                        if joined:
                            answer = joined
                            break
            if answer:
                break
        except Exception as e:
            err_str = str(e).lower()
            if "tool_use_failed" in err_str or "failed_generation" in err_str or ("400" in err_str and "function" in err_str):
                print(f"[Analyst Hub] Tool call failed (attempt {attempt+1}): {e}")
                if attempt == 0:
                    continue  # retry without tools
            raise  # re-raise other errors

    if not answer:
        answer = "I could not generate a prediction. Please try rephrasing your question."

    print(f"[Analyst Hub] Report generated ({len(answer)} chars)")
    return {"analyst_report": answer}


# ═══════════════════════════════════════════════════════════════════
# NODE 4: NEWS SENTIMENT — Headline fetching + sentiment scoring
# ═══════════════════════════════════════════════════════════════════

def news_sentiment_node(state: AkinatorState) -> dict:
    """Fetches news headlines and calculates sentiment score from keyword analysis."""
    query = state["user_query"]

    # Better topic extraction — keep the meaningful financial terms
    topic = query.lower().strip()
    # Remove question phrasing but keep the subject
    for phrase in ["should i invest in", "should i buy", "should i sell",
                   "what do you think about", "how is", "how about",
                   "tell me about", "analyze", "predict", "is it good to buy",
                   "can i invest in", "what about", "give me analysis"]:
        topic = topic.replace(phrase, "")
    topic = topic.strip().strip("?.,!")
    if len(topic) < 2:
        topic = query[:50]

    print(f"[News Sentiment Node] Topic: '{topic}'")
    settings = get_settings()
    if not settings.SERPAPI_API_KEY:
        return {"news_sentiment": {"headlines": [], "score": 50, "label": "Neutral", "topic": topic}}

    try:
        url = f"https://serpapi.com/search.json?q={urllib.parse.quote(topic + ' stock market news')}&api_key={settings.SERPAPI_API_KEY}&tbm=nws&num=10"
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=10) as response:
            data = json.loads(response.read().decode())

        headlines = []
        if "news_results" in data:
            for n in data["news_results"][:10]:
                headlines.append({
                    "title": n.get("title", ""),
                    "source": n.get("source", {}).get("name", "Unknown"),
                    "date": n.get("date", ""),
                })

        # Keyword-based sentiment scoring with expanded dictionaries
        all_text = " ".join(h["title"].lower() for h in headlines)

        pos_hits = sum(1 for w in POSITIVE_WORDS if w in all_text)
        neg_hits = sum(1 for w in NEGATIVE_WORDS if w in all_text)
        total = pos_hits + neg_hits

        if total == 0 and headlines:
            # Fallback: if no keywords matched but we have headlines, score as Mixed
            score, label = 50, "Mixed"
        elif total == 0:
            score, label = 50, "Neutral"
        else:
            score = int((pos_hits / total) * 100)
            label = "Bullish" if score >= 65 else "Bearish" if score <= 35 else "Mixed"

        print(f"[News Sentiment Node] Score: {score}/100 ({label}) | +{pos_hits} -{neg_hits} | Headlines: {len(headlines)}")
        return {"news_sentiment": {
            "headlines": headlines[:5], "score": score, "label": label,
            "topic": topic, "positive_signals": pos_hits, "negative_signals": neg_hits,
        }}
    except Exception as e:
        print(f"[News Sentiment Error] {e}")
        return {"news_sentiment": {"headlines": [], "score": 50, "label": "Neutral", "topic": topic}}


# ═══════════════════════════════════════════════════════════════════
# NODE 5: CONFIDENCE SCORER — Data-driven confidence calculation
# ═══════════════════════════════════════════════════════════════════

def confidence_scorer_node(state: AkinatorState) -> dict:
    """Calculates a confidence percentage based on data completeness and alignment."""
    score = 40  # base score
    report = state.get("analyst_report", "")

    # Report substance (up to +15)
    if len(report) > 500:
        score += 15
    elif len(report) > 200:
        score += 8

    # News data availability (+15) and direction strength (+5)
    sentiment = state.get("news_sentiment", {})
    if sentiment.get("headlines"):
        score += 15
        if sentiment.get("label") in ("Bullish", "Bearish"):
            score += 5

    # Specific data points in report (up to +15)
    numbers_found = len(re.findall(r'\$[\d,.]+|\d+\.?\d*%', report))
    score += min(15, numbers_found * 3)

    # What-if analysis bonus (+10)
    if state.get("whatif_result"):
        score += 10

    # Tool usage markers (+5 each, up to +20)
    tool_markers = ["Market Analyst", "News", "Risk Assessment", "Portfolio Strategy"]
    tools_used = sum(1 for m in tool_markers if m.lower() in report.lower())
    score += tools_used * 5

    score = min(95, max(10, score))
    print(f"[Confidence Scorer] Score: {score}%")
    return {"confidence_score": score}


# ═══════════════════════════════════════════════════════════════════
# NODE 6: CRITIQUE — Self-correction against news
# ═══════════════════════════════════════════════════════════════════

def critique_node(state: AkinatorState) -> dict:
    """Reviews the prediction against news sentiment for contradictions. Appends a Correction Note if needed."""
    report = state.get("analyst_report", "")
    sentiment = state.get("news_sentiment", {})
    critique = ""

    report_lower = report.lower()
    sentiment_label = sentiment.get("label", "Neutral")

    # Detect prediction direction
    buy_signals = len(re.findall(r'\b(buy|bullish|upside|growth|opportunity|strong buy|accumulate)\b', report_lower))
    sell_signals = len(re.findall(r'\b(sell|bearish|downside|decline|avoid|reduce|caution)\b', report_lower))
    prediction_direction = "bullish" if buy_signals > sell_signals else "bearish" if sell_signals > buy_signals else "neutral"

    # Check for contradiction
    if prediction_direction == "bullish" and sentiment_label == "Bearish":
        critique = ("Correction Note: The prediction leans bullish, but current news sentiment is predominantly bearish. "
                     "Recent negative headlines suggest increased short-term risk. Consider a more cautious entry or wait "
                     "for sentiment improvement before acting.")
    elif prediction_direction == "bearish" and sentiment_label == "Bullish":
        critique = ("Correction Note: The prediction leans bearish, but current news sentiment is actually bullish. "
                     "Positive headlines suggest improving conditions that may not be reflected in the technical analysis. "
                     "The downside risk may be lower than estimated.")

    # Check for high-impact keywords in news
    headlines_text = " ".join(h.get("title", "") for h in sentiment.get("headlines", [])).lower()
    high_impact = ["fed", "interest rate", "recession", "war", "crisis", "crash", "bankruptcy", "default", "regulation", "ban"]
    impacts_found = [w for w in high_impact if w in headlines_text]

    if impacts_found and not critique:
        critique = (f"Correction Note: High-impact events detected in current news: {', '.join(impacts_found).title()}. "
                     "These macro factors could significantly affect this prediction. Monitor closely before committing capital.")

    if critique:
        print(f"[Critique Node] Correction issued")
    else:
        print(f"[Critique Node] No contradictions found — prediction consistent with news")
    return {"critique_note": critique}


# ═══════════════════════════════════════════════════════════════════
# NODE 7: JIT EDUCATION — Jargon detection & definitions
# ═══════════════════════════════════════════════════════════════════

def jit_education_node(state: AkinatorState) -> dict:
    """Scans the analyst report for financial jargon and provides plain-English definitions."""
    report = state.get("analyst_report", "")
    report_lower = report.lower()
    found_terms = {}
    for term, definition in JARGON_DICT.items():
        if term.lower() in report_lower:
            found_terms[term] = definition
    print(f"[JIT Education] Found {len(found_terms)} jargon terms")
    return {"jargon_definitions": found_terms}


# ═══════════════════════════════════════════════════════════════════
# NODE 8: MEMO GENERATOR — Professional investment memo
# ═══════════════════════════════════════════════════════════════════

def memo_generator_node(state: AkinatorState) -> dict:
    """Compiles the entire session into a structured investment memo."""
    now = datetime.now().strftime("%B %d, %Y at %H:%M")
    confidence = state.get("confidence_score", 0)
    sentiment = state.get("news_sentiment", {})
    critique = state.get("critique_note", "")
    whatif = state.get("whatif_result", "")

    conf_label = "High" if confidence >= 75 else "Moderate" if confidence >= 50 else "Low"
    sentiment_label = sentiment.get("label", "N/A")
    sentiment_score = sentiment.get("score", "N/A")

    memo_lines = [
        "INVESTMENT MEMO",
        f"Generated: {now}",
        f"AI Model: {get_settings().GROQ_MODEL}",
        "",
        f"Query: {state.get('user_query', '')}",
        "",
        f"Confidence Score: {confidence}% ({conf_label})",
        f"News Sentiment: {sentiment_label} ({sentiment_score}/100)",
    ]

    if critique:
        memo_lines.append(f"Self-Correction: {critique}")

    if whatif:
        memo_lines.append("What-If Analysis: Included")

    headlines = sentiment.get("headlines", [])
    if headlines:
        memo_lines.append("")
        memo_lines.append("Key Headlines:")
        for h in headlines[:3]:
            memo_lines.append(f"  - {h.get('title', '')} ({h.get('source', '')})")

    jargon = state.get("jargon_definitions", {})
    if jargon:
        memo_lines.append("")
        memo_lines.append(f"Educational Terms Identified: {len(jargon)}")
        for term in list(jargon.keys())[:5]:
            memo_lines.append(f"  - {term}")

    memo_lines.extend([
        "",
        "Data Sources: Market Data (yfinance), News (SerpAPI), Risk Analysis, Portfolio Strategy",
        "Disclaimer: AI-generated for educational purposes only. Not financial advice.",
    ])

    print(f"[Memo Generator] Memo compiled")
    return {"final_memo": "\n".join(memo_lines)}


# ═══════════════════════════════════════════════════════════════════
# NODE 9: FORMAT RESPONSE — Assembles final user-facing output
# ═══════════════════════════════════════════════════════════════════

def format_response_node(state: AkinatorState) -> dict:
    """Combines all analysis data into the final formatted response."""
    parts = [state.get("analyst_report", "")]

    # What-if section
    if state.get("whatif_result"):
        parts.append("\n" + state["whatif_result"])

    print(f"[Format Response] Assembling final output")
    return {"final_response": "\n".join(parts)}


# ═══════════════════════════════════════════════════════════════════
# GRAPH ROUTING LOGIC
# ═══════════════════════════════════════════════════════════════════

def route_after_router(state: AkinatorState) -> str:
    """Conditional edge: routes to what-if engine or directly to analyst hub."""
    if state.get("is_whatif"):
        return "whatif_engine"
    return "analyst_hub"


# ═══════════════════════════════════════════════════════════════════
# BUILD & COMPILE THE GRAPH
# ═══════════════════════════════════════════════════════════════════

def build_akinator_graph():
    """Constructs the LangGraph StateGraph with all 9 nodes and edges."""
    graph = StateGraph(AkinatorState)

    # Register all nodes
    graph.add_node("router", router_node)
    graph.add_node("whatif_engine", whatif_engine_node)
    graph.add_node("analyst_hub", analyst_hub_node)
    graph.add_node("news_sentiment", news_sentiment_node)
    graph.add_node("confidence_scorer", confidence_scorer_node)
    graph.add_node("critique", critique_node)
    graph.add_node("jit_education", jit_education_node)
    graph.add_node("memo_generator", memo_generator_node)
    graph.add_node("format_response", format_response_node)

    # Wire the edges
    graph.add_edge(START, "router")
    graph.add_conditional_edges("router", route_after_router, {
        "whatif_engine": "whatif_engine",
        "analyst_hub": "analyst_hub",
    })
    graph.add_edge("whatif_engine", "analyst_hub")
    graph.add_edge("analyst_hub", "news_sentiment")
    graph.add_edge("news_sentiment", "confidence_scorer")
    graph.add_edge("confidence_scorer", "critique")
    graph.add_edge("critique", "jit_education")
    graph.add_edge("jit_education", "memo_generator")
    graph.add_edge("memo_generator", "format_response")
    graph.add_edge("format_response", END)

    return graph.compile()


# Compile the graph once at module load
akinator_graph = build_akinator_graph()


# ═══════════════════════════════════════════════════════════════════
# PUBLIC API — Called by FastAPI endpoint
# ═══════════════════════════════════════════════════════════════════

def get_akinator_response(session_id: str, user_message: str, panel_mode: bool = False, knowledge_base_id: Optional[str] = None):
    """Run the full Akinator 2.0 LangGraph pipeline and return rich results."""

    if session_id not in _akinator_sessions:
        _akinator_sessions[session_id] = []

    history = _akinator_sessions[session_id]

    try:
        # Build initial graph state
        initial_state = {
            "messages": list(history),
            "user_query": user_message,
            "analyst_report": "",
            "news_sentiment": {},
            "confidence_score": 0,
            "critique_note": "",
            "whatif_result": "",
            "jargon_definitions": {},
            "final_memo": "",
            "panel_mode": panel_mode,
            "is_whatif": False,
            "final_response": "",
            "knowledge_base_id": knowledge_base_id or "",
            "rag_used": False,
        }

        # Run the graph
        print(f"\n{'='*60}")
        print(f"[Akinator 2.0] Starting LangGraph pipeline...")
        print(f"[Akinator 2.0] Query: {user_message[:100]}...")
        print(f"[Akinator 2.0] Panel Mode: {panel_mode}")
        print(f"{'='*60}")

        result = akinator_graph.invoke(initial_state)

        # Update session history
        history.append(HumanMessage(content=user_message))
        history.append(AIMessage(content=result.get("final_response", "")))
        if len(history) > 10:
            _akinator_sessions[session_id] = history[-10:]

        print(f"[Akinator 2.0] Pipeline complete. Confidence: {result.get('confidence_score', 0)}%")

        # Return rich result
        return {
            "reply": result.get("final_response", ""),
            "confidence_score": result.get("confidence_score", 0),
            "news_sentiment": result.get("news_sentiment", {}),
            "jargon_definitions": result.get("jargon_definitions", {}),
            "memo": result.get("final_memo", ""),
            "whatif_result": result.get("whatif_result", ""),
            "critique_note": result.get("critique_note", ""),
            "rag_used": result.get("rag_used", False),
        }
    except Exception as e:
        error_result = _format_rate_limit_error(e)
        if isinstance(error_result, dict) and error_result.get("rate_limited"):
            return error_result
        return {
            "reply": error_result if isinstance(error_result, str) else f"Error: {str(e)}",
            "confidence_score": 0, "news_sentiment": {}, "jargon_definitions": {},
            "memo": "", "whatif_result": "", "critique_note": "", "rag_used": False,
        }


def _format_rate_limit_error(e: Exception):
    """Parse Groq rate limit errors. Returns dict with wait_seconds if rate limited, string otherwise."""
    import re
    err_str = str(e).lower()
    print(f"[Akinator Error] {e}")

    rate_limit_keywords = [
        "rate_limit", "rate limit", "429",
        "tokens per minute", "tokens per day", "requests per minute",
        "requests per day", "token_limit", "quota", "limit exceeded",
        "resource_exhausted", "too many requests",
    ]

    if any(kw in err_str for kw in rate_limit_keywords):
        wait_seconds = None
        match = re.search(r'try again in\s+(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+(?:\.\d+)?)s)?', err_str)
        if match and any(match.group(i) for i in (1, 2, 3)):
            h = int(match.group(1) or 0)
            m = int(match.group(2) or 0)
            s = float(match.group(3) or 0)
            wait_seconds = h * 3600 + m * 60 + int(s) + (1 if s % 1 > 0 else 0)
        if not wait_seconds:
            match = re.search(r'retry.?after\s+(\d+)', err_str)
            if match:
                wait_seconds = int(match.group(1))
        if not wait_seconds:
            match = re.search(r'(\d+)\s*seconds?', err_str)
            if match:
                wait_seconds = int(match.group(1))
        if not wait_seconds:
            wait_seconds = 30
        return {"rate_limited": True, "wait_seconds": wait_seconds}

    return f"Akinator Error: {str(e)}"
