"""GetFixn client shared by the example bots. pip install requests

Reads your key from the GETFIXN_KEY environment variable. Never hard-code it.
GetFixn sells data, not trading advice. Forecasts can be wrong.
"""
import json
import os
import time

import requests

BASE = os.environ.get("GETFIXN_BASE", "https://data.truefixr.com")
HTTP = requests      # tests swap this for a fake
SLEEP = time.sleep   # tests swap this too
GRADES = "ABCDEF"    # A = low risk ... F = extreme


class LowBalance(Exception):
    """HTTP 402: key missing or wrong, or your prepaid balance ran out."""


def _key():
    key = os.environ.get("GETFIXN_KEY")
    if not key:
        raise SystemExit("Set GETFIXN_KEY first: export GETFIXN_KEY=tfx_live_...")
    return key


def post(path, body, tries=5):
    """POST JSON with the key. Backs off on 429, 5xx and network errors."""
    headers = {"Authorization": "Bearer " + _key()}
    for attempt in range(tries):
        try:
            r = HTTP.post(BASE + path, json=body, headers=headers, timeout=30)
        except requests.RequestException:
            r = None
        if r is not None and r.status_code == 200:
            return r.json()
        if r is not None and r.status_code == 402:
            # Paid route, no valid key or no balance. Retrying will not help.
            raise LowBalance("402 payment required: check GETFIXN_KEY and top up your balance")
        if r is not None and r.status_code in (400, 422):
            raise ValueError("bad request: " + r.text[:200])
        if attempt == tries - 1:
            break
        wait = 2 ** attempt  # 1, 2, 4, 8 seconds
        if r is not None and r.status_code == 429:
            wait = max(wait, int(r.headers.get("Retry-After", 0) or 0))
        SLEEP(wait)
    raise RuntimeError("API unavailable after %d tries" % tries)


def load_locations(path):
    """[{"label": "Austin", "lat": 30.27, "lon": -97.74}, ...]"""
    with open(path) as f:
        return json.load(f)


def pick(locs, text):
    """A saved label ("Austin") or raw "lat,lon" typed by a chat user."""
    for loc in locs:
        if loc["label"].lower() == text.strip().lower():
            return loc
    try:
        lat, lon = (float(x) for x in text.split(","))
    except ValueError:
        raise ValueError("Use a saved name or lat,lon like 30.27,-97.74")
    if not (-90 <= lat <= 90 and -180 <= lon <= 180):
        raise ValueError("lat,lon out of range")
    return {"label": text.strip(), "lat": lat, "lon": lon}


def _batch(path, locs, extra=None):
    out = []
    for i in range(0, len(locs), 500):  # API limit: 500 locations per call
        chunk = locs[i:i + 500]
        body = {"locations": [{"lat": l["lat"], "lon": l["lon"], "label": l["label"]} for l in chunk]}
        body.update(extra or {})
        results = post(path, body).get("results", [])
        if len(results) != len(chunk):
            raise RuntimeError("API returned %d results for %d locations" % (len(results), len(chunk)))
        out += [dict(l, **r) for l, r in zip(chunk, results)]
    return out


def grades(locs, window="24h"):
    """county forecast grade per location. $0.05 per location."""
    return _batch("/v1/portfolio/risk", locs, {"window": window})


def weather(locs):
    """Live weather + severe alerts per location. $0.0002 per location."""
    return _batch("/v1/weather/batch", locs)


def check(locs, state_path="state.json", window="24h", alert_at="D"):
    """Fetch grades, compare with the last run, return changes at or above alert_at."""
    try:
        with open(state_path) as f:
            old = json.load(f)
    except (OSError, ValueError):
        old = {}
    new, changes = {}, []
    for r in grades(locs, window):
        g = r.get("grade")
        new[r["label"]] = g
        if g in GRADES and g != old.get(r["label"]) and GRADES.index(g) >= GRADES.index(alert_at):
            changes.append({"label": r["label"], "lat": r["lat"], "lon": r["lon"],
                            "county": r.get("county"), "state": r.get("state"),
                            "was": old.get(r["label"]), "now": g})
    with open(state_path, "w") as f:
        json.dump(new, f)
    return changes


def line(c):
    return "%s (%s, %s): county forecast grade %s -> %s. A forecast, not a certainty." % (
        c["label"], c.get("county"), c.get("state"), c["was"] or "new", c["now"])
