"""A trading bot over tradefloor's HTTP API, with nothing but httpx.

    python -m tradefloor_serve                  # a local server, in another terminal
    python examples/agents/http_bot.py --days 5

For the hosted service, set TF_URL=https://app.tradefloor.dev and
TF_KEY=tfk_...

Each morning the bot holds the two names that fell most the day before and
sells the rest, then lets the day run. Every write carries an
Idempotency-Key, so resending after a timeout never trades twice, and the
bot waits out 429s and 503s as Retry-After says. The session id is saved to
a file, so if you stop the bot and start it again it carries on where it
was.
"""

from __future__ import annotations

import argparse
import os
import sys
import time
import uuid
from pathlib import Path

import httpx

BASE = os.environ.get("TF_URL", "http://127.0.0.1:8765")
KEY = os.environ.get("TF_KEY")


class Tradefloor:
    def __init__(self) -> None:
        headers = {"Authorization": f"Bearer {KEY}"} if KEY else {}
        self.http = httpx.Client(base_url=BASE, headers=headers, timeout=30)

    def call(self, method: str, path: str, body: dict | None = None, *, key: str | None = None):
        """One request. Writes get an Idempotency-Key, the same one on every try."""
        headers = {}
        if method != "GET":
            headers["Idempotency-Key"] = key or str(uuid.uuid4())
        for attempt in range(6):
            try:
                r = self.http.request(method, path, json=body, headers=headers)
            except httpx.TransportError:
                time.sleep(2 ** attempt)        # the same key makes resending safe
                continue
            if r.is_success:
                return r.json()
            error = r.json()
            wait = error.get("retry_after")
            if wait is not None and attempt < 5:
                time.sleep(max(float(wait), 0.2))
                continue
            sys.exit(f"{error['code']}: {error['message']}\n{error.get('hint', '')}")
        sys.exit(f"{method} {path}: no answer from {BASE} after 6 tries (is the server running?)")


def session(api: Tradefloor, saved: Path) -> dict:
    """The session saved last time, if it is still open; otherwise a new one."""
    if saved.exists():
        sid = saved.read_text().strip()
        info = api.call("GET", f"/v1/sessions/{sid}")
        if info["status"] == "open":
            print(f"resuming {sid} at day {info['clock']['day']}")
            return info
    info = api.call("POST", "/v1/sessions", {"universe_size": 8, "seed": 7, "label": "http_bot"})
    saved.write_text(info["session_id"], newline="")
    print(f"opened {info['session_id']} with {', '.join(info['tickers'])}")
    return info


def trade_one_day(api: Tradefloor, sid: str) -> None:
    obs = api.call("GET", f"/v1/sessions/{sid}/observation")
    day = obs["clock"]["day"]
    moves = {q["ticker"]: q["last"] / q["prev_close"] - 1 for q in obs["quotes"]}
    want = set(sorted(moves, key=moves.get)[:2])
    held = {p["ticker"]: p["quantity"] for p in obs["positions"]}
    budget = obs["account"]["net_worth"] * 0.2
    last = {q["ticker"]: q["last"] for q in obs["quotes"]}
    for ticker, qty in held.items():
        if ticker not in want and qty > 0:
            api.call("POST", f"/v1/sessions/{sid}/orders",
                     {"ticker": ticker, "side": "sell", "quantity": qty,
                      "client_order_id": f"d{day}-sell-{ticker}"}, key=f"{sid}-d{day}-sell-{ticker}")
    for ticker in want - set(held):
        qty = int(budget // last[ticker])
        if qty > 0:
            api.call("POST", f"/v1/sessions/{sid}/orders",
                     {"ticker": ticker, "side": "buy", "quantity": qty,
                      "client_order_id": f"d{day}-buy-{ticker}"}, key=f"{sid}-d{day}-buy-{ticker}")
    # Orders fill at the start of the first step; run the day, then open the next.
    res = api.call("POST", f"/v1/sessions/{sid}/advance", {"until": "next_open"},
                   key=f"{sid}-d{day}-advance")
    worth = res["observation"]["account"]["net_worth"]
    print(f"day {day}: {len(res['fills'])} fills, {len(res['events'])} events, net worth {worth:,.2f}")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--days", type=int, default=5)
    parser.add_argument("--state", default=".tradefloor-http-bot", help="where the session id is kept")
    parser.add_argument("--keep-open", action="store_true", help="leave the session open at the end")
    args = parser.parse_args()
    api = Tradefloor()
    saved = Path(args.state)
    sid = session(api, saved)["session_id"]
    for _ in range(args.days):
        trade_one_day(api, sid)
    if args.keep_open:
        return
    report = api.call("POST", f"/v1/sessions/{sid}/close", key=f"{sid}-close")
    saved.unlink(missing_ok=True)
    print(f"closed after {report['days']} days: net worth {report['account']['net_worth']:,.2f}, "
          f"{report['fills']} fills")
    print("caveats:")
    for caveat in report["caveats"]:
        print(" -", caveat)


if __name__ == "__main__":
    main()
