"""
Free-form chat with GPT-4o about the current cockpit state.

The only action the model can trigger is `propose_trade` — even then, it never
touches the real portfolio: the tool call just writes a 'pending' row to
ai_trade_proposals. The user has to explicitly confirm from the Trade Ideas UI
(POST /api/ai-chat/trade-proposals/{id}/confirm) before anything becomes a real
position. No other tool is wired up, so nothing else can ever be triggered from
here (no cycle, no data mutation, no close/edit of existing positions).
"""
import json
import re
import time
from typing import Dict, List, Optional

from services.ai_analyzer import get_client
from services.ai_chat_context import assemble_context, CONTEXT_BLOCKS

SYSTEM_PROMPT_HEADER = """Tu es l'assistant IA integre au cockpit de trading OpenFin Intelligence.
Tu as acces ci-dessous a un instantane en lecture seule de la situation actuelle (portefeuille, risque geopolitique, regime macro, patterns, options/IV, indicateurs techniques, signaux ondelettes, watchlist, calendrier economique, rapports institutionnels, lecons accumulees, VaR/risque) - le meme niveau de detail que celui utilise par le cycle d'analyse automatique pour generer ses recommandations.

REGLES IMPORTANTES :
- Quand on te demande une idee ou un conseil de trade, PROPOSE quelque chose de concret (biais directionnel, instrument, montage d'options avec strikes/echeance si pertinent, niveaux techniques, justification tiree du contexte) - exactement comme le ferait le cycle automatique dans ses recommandations. Ne te contente pas d'observations vagues ni de renvoyer la question : prends position a partir du contexte fourni.
- Tu disposes de l'outil propose_trade pour enregistrer une idee concrete. Utilise-le UNIQUEMENT quand l'utilisateur demande explicitement un conseil de trade ou valide clairement une idee que tu viens de suggerer - jamais de maniere systematique a chaque message. Chaque appel cree une proposition EN ATTENTE dans Trade Ideas ; rien n'est jamais execute automatiquement.
- La seule limite reelle est que tu ne peux EXECUTER aucune action toi-meme (aucun trade n'est passe directement, aucun cycle n'est declenche, aucune position existante n'est modifiee) - tes idees sont des suggestions que l'utilisateur doit valider lui-meme. Ne le precise que si l'utilisateur semble croire que tu peux agir directement (ex. "achete X pour moi").
- Reponds en francais, de facon concise et directe, en t'appuyant sur le contexte fourni. Si une donnee demandee n'est pas dans le contexte ci-dessous, dis-le plutot que d'inventer.

=== CONTEXTE ACTUEL ===
{context}
=== FIN DU CONTEXTE ===
"""

TRADE_PROPOSAL_TOOL = {
    "type": "function",
    "function": {
        "name": "propose_trade",
        "description": (
            "Enregistre une idee de trade concrete EN ATTENTE dans Trade Ideas. "
            "N'execute RIEN et n'ouvre AUCUNE position — l'utilisateur doit explicitement "
            "confirmer depuis l'interface pour que ca devienne un trade reel dans le portefeuille. "
            "N'appelle cet outil que lorsque l'utilisateur demande explicitement un conseil de trade "
            "ou valide clairement une idee que tu as suggeree — jamais de maniere systematique."
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "title": {"type": "string", "description": "Titre court de l'idee"},
                "underlying": {"type": "string", "description": "Ticker Yahoo Finance du sous-jacent (ex: EURUSD=X, CL=F, SPY, GC=F)"},
                "strategy": {"type": "string", "description": "Nom de la strategie (ex: long call, put spread, straddle, short strangle, directionnel spot)"},
                "asset_class": {"type": "string", "enum": ["indices", "forex", "commodities", "rates", "crypto", "equities"]},
                "expiry_days": {"type": "integer", "description": "Horizon en jours jusqu'a l'echeance"},
                "capital_invested": {"type": "number", "description": "Capital alloue en EUR"},
                "legs": {
                    "type": "array",
                    "description": "Legs optionnelles du montage (liste vide pour un trade directionnel simple sans options)",
                    "items": {
                        "type": "object",
                        "properties": {
                            "strike": {"type": "number"},
                            "option_type": {"type": "string", "enum": ["call", "put"]},
                            "quantity": {"type": "integer"},
                            "position": {"type": "string", "enum": ["long", "short"]},
                        },
                        "required": ["strike", "option_type", "quantity", "position"],
                    },
                },
                "geo_trigger": {"type": "string", "description": "Evenement/catalyseur declencheur, si pertinent"},
                "rationale": {"type": "string", "description": "Justification concise, appuyee sur le contexte fourni"},
            },
            "required": ["title", "underlying", "strategy", "capital_invested", "rationale"],
        },
    },
}


def _chat_messages(system: str, messages: List[Dict], model: str = "gpt-4o", max_tokens: int = 1200, tools: Optional[List[Dict]] = None):
    """Multi-turn variant of ai_analyzer._chat() — accepts a full message history
    instead of a single system+user pair, and optionally OpenAI tool schemas.
    Returns the raw SDK message object (not just its text) so callers can
    inspect tool_calls. Same client/retry/backoff logic, kept independent so it
    never risks the well-tested cycle-facing _chat()."""
    client = get_client()
    if not client:
        raise RuntimeError("OpenAI API key not configured")

    kwargs = {
        "model": model,
        "messages": [{"role": "system", "content": system}] + messages,
        "temperature": 0.4,
        "max_tokens": max_tokens,
    }
    if tools:
        kwargs["tools"] = tools

    last_exc: Optional[Exception] = None
    for attempt in range(4):
        try:
            resp = client.chat.completions.create(**kwargs)
            return resp.choices[0].message
        except Exception as e:
            last_exc = e
            err_str = str(e)
            if "429" in err_str or "rate_limit" in err_str:
                m = re.search(r"try again in ([\d.]+)s", err_str)
                wait = float(m.group(1)) + 1.0 if m else 2 ** (attempt + 1) * 5.0
                time.sleep(min(wait, 60.0))
                continue
            raise
    raise last_exc  # type: ignore[misc]


def _handle_tool_call(tc, session_id: str) -> tuple:
    """Executes one tool call. Returns (tool_result_text, trade_proposal_or_None)."""
    from services.database import save_ai_trade_proposal

    if tc.function.name != "propose_trade":
        return "Outil inconnu.", None

    try:
        args = json.loads(tc.function.arguments or "{}")
        proposal_id = save_ai_trade_proposal({**args, "session_id": session_id})
        trade_proposal = {
            "id": proposal_id,
            "title": args.get("title"),
            "underlying": args.get("underlying"),
            "strategy": args.get("strategy"),
        }
        return f"Proposition enregistree (id={proposal_id}), EN ATTENTE dans Trade Ideas. Rien n'a ete execute.", trade_proposal
    except Exception as e:
        return f"Erreur lors de l'enregistrement de la proposition: {e}", None


def send_chat_message(
    session_id: str,
    message: str,
    enabled_blocks: Optional[List[str]] = None,
    refresh_context: bool = False,
) -> Dict:
    from services.database import save_chat_message, get_chat_messages

    enabled = enabled_blocks or CONTEXT_BLOCKS
    blocks = assemble_context(enabled, session_id, refresh=refresh_context)
    context_text = "\n\n".join(blocks.values())
    system = SYSTEM_PROMPT_HEADER.format(context=context_text)

    history = get_chat_messages(session_id)
    messages = [{"role": h["role"], "content": h["content"]} for h in history]
    messages.append({"role": "user", "content": message})

    save_chat_message(session_id, "user", message)

    reply_msg = _chat_messages(system, messages, tools=[TRADE_PROPOSAL_TOOL])
    trade_proposal = None

    if reply_msg.tool_calls:
        messages.append({
            "role": "assistant",
            "content": reply_msg.content,
            "tool_calls": [
                {"id": tc.id, "type": "function", "function": {"name": tc.function.name, "arguments": tc.function.arguments}}
                for tc in reply_msg.tool_calls
            ],
        })
        for tc in reply_msg.tool_calls:
            tool_result, proposal = _handle_tool_call(tc, session_id)
            trade_proposal = proposal or trade_proposal
            messages.append({"role": "tool", "tool_call_id": tc.id, "content": tool_result})

        final_msg = _chat_messages(system, messages)  # no tools this round — forces a final text reply
        reply_text = final_msg.content or ""
    else:
        reply_text = reply_msg.content or ""

    save_chat_message(session_id, "assistant", reply_text)

    return {"reply": reply_text, "blocks_included": list(blocks.keys()), "trade_proposal": trade_proposal}
