"""FinLab Web — chat Claude (Opus) spécialisé finance, branché sur les outils finlab.

Modèle : claude-opus-4-8 (API Anthropic), thinking adaptatif, streaming, tool-use.
Connexion : ANTHROPIC_API_KEY en direct, ou ANTHROPIC_BASE_URL pour passer par
le proxy LiteLLM du lab (ex: http://192.168.10.1:4000).

Lancement :  .venv/bin/python -m webapp.server   (puis http://localhost:8800)
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

import anthropic
from fastapi import FastAPI
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))  # rend le package finlab importable

from finlab import alerts, data, fundamental, plan as plan_mod, scanner, technical, tracker  # noqa: E402

MODEL = "claude-opus-4-8"
SOUL = (Path(__file__).resolve().parent / "finance_soul.md").read_text(encoding="utf-8")
client = anthropic.Anthropic()  # lit ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL de l'env

# ── Outils exposés à Claude (mappés sur finlab) ───────────────────────────────
TOOLS = [
    {"name": "digest", "description": "Rapport compact tout-en-un : portefeuille + opportunités haussières filtrées + alertes du jour. À privilégier pour une vue d'ensemble.",
     "input_schema": {"type": "object", "properties": {
         "theme": {"type": "string", "description": "thème de watchlist, 'all' ou 'portfolio'"},
         "target_pct": {"type": "number", "description": "cible de perf hebdo en %"}}}},
    {"name": "portfolio_summary", "description": "Valorisation du portefeuille réel : valeur, P&L, poids, exposition sectorielle, concentration.",
     "input_schema": {"type": "object", "properties": {}}},
    {"name": "opportunities", "description": "Scanner d'opportunités d'un thème : capacité de mouvement (volatilité) + biais directionnel. Thèmes : energy_power, chips, cables_optical_network, software_cloud, datacenter_infra, 'all', 'portfolio'.",
     "input_schema": {"type": "object", "properties": {
         "theme": {"type": "string"}, "target_pct": {"type": "number"},
         "bullish_only": {"type": "boolean"}}}},
    {"name": "technical_analysis", "description": "Indicateurs techniques (RSI, MACD, MM50/200, Bollinger, ATR) + signaux pour une liste de tickers.",
     "input_schema": {"type": "object", "properties": {
         "tickers": {"type": "array", "items": {"type": "string"}},
         "period": {"type": "string", "description": "ex: 6mo, 1y"}}, "required": ["tickers"]}},
    {"name": "fundamentals", "description": "Ratios fondamentaux (PER, PEG, marges, ROE, croissance, dette, cible analystes) pour une liste de tickers.",
     "input_schema": {"type": "object", "properties": {
         "tickers": {"type": "array", "items": {"type": "string"}}}, "required": ["tickers"]}},
    {"name": "trade_plan", "description": "Plan de trade chiffré : entrée, stop ATR, objectifs +5/+10%, taille de position et ratio risque/rendement (R:R). Le verdict R:R est central.",
     "input_schema": {"type": "object", "properties": {
         "ticker": {"type": "string"}, "capital": {"type": "number", "description": "capital à déployer (devise du titre)"}},
         "required": ["ticker"]}},
    {"name": "alerts_today", "description": "Événements du jour : croisements MACD, cassures MM50, survente. watch = thème, 'all' ou 'portfolio'.",
     "input_schema": {"type": "object", "properties": {"watch": {"type": "string"}}}},
    {"name": "price", "description": "Dernier cours d'un titre dans sa devise native.",
     "input_schema": {"type": "object", "properties": {"ticker": {"type": "string"}}, "required": ["ticker"]}},
]


def execute(name: str, args: dict) -> str:
    """Exécute un outil finlab et renvoie du texte (jamais d'exception remontée à Claude)."""
    try:
        if name == "digest":
            from finlab import digest as digest_mod
            return digest_mod.build(args.get("theme", "all"), args.get("target_pct", 5.0))
        if name == "portfolio_summary":
            return tracker.report()
        if name == "opportunities":
            df = scanner.scan_theme(args.get("theme", "all"), args.get("target_pct", 5.0),
                                    bullish_only=args.get("bullish_only", False))
            return df.to_string(index=False)
        if name == "technical_analysis":
            return technical.scan(args["tickers"], args.get("period", "1y")).to_string(index=False)
        if name == "fundamentals":
            return fundamental.compare(args["tickers"]).to_string(index=False)
        if name == "trade_plan":
            return plan_mod.render(plan_mod.plan(args["ticker"], args.get("capital", 1427.0)))
        if name == "alerts_today":
            return alerts.render(alerts.run(args.get("watch")))
        if name == "price":
            p, cur = data.last_price(args["ticker"])
            return f"{args['ticker']}: {p:.2f} {cur}"
        return f"Outil inconnu: {name}"
    except Exception as e:
        return f"Erreur outil {name}: {e}"


# ── API ───────────────────────────────────────────────────────────────────────
app = FastAPI(title="FinLab")
STATIC = Path(__file__).resolve().parent / "static"


class ChatRequest(BaseModel):
    messages: list  # [{role, content}]


def _sse(event: str, data) -> str:
    return f"data: {json.dumps({'event': event, 'data': data})}\n\n"


def agent_stream(messages: list):
    """Boucle agentique : streame le texte, exécute les outils, recommence si besoin."""
    system = [{"type": "text", "text": SOUL, "cache_control": {"type": "ephemeral"}}]
    while True:
        with client.messages.stream(
            model=MODEL,
            max_tokens=16000,
            thinking={"type": "adaptive"},
            output_config={"effort": "high"},
            system=system,
            tools=TOOLS,
            messages=messages,
        ) as stream:
            for text in stream.text_stream:
                yield _sse("delta", text)
            final = stream.get_final_message()

        messages.append({"role": "assistant", "content": final.content})
        if final.stop_reason != "tool_use":
            break

        results = []
        for block in final.content:
            if block.type == "tool_use":
                yield _sse("tool", block.name)
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": execute(block.name, block.input)})
        messages.append({"role": "user", "content": results})

    yield _sse("done", None)


@app.post("/api/chat")
def chat(req: ChatRequest):
    return StreamingResponse(agent_stream(req.messages), media_type="text/event-stream")


@app.get("/")
def index():
    return FileResponse(STATIC / "index.html")


app.mount("/static", StaticFiles(directory=STATIC), name="static")


if __name__ == "__main__":
    import uvicorn
    print("FinLab → http://localhost:8800")
    uvicorn.run(app, host="0.0.0.0", port=8800)
