"""Cloud for Bots SDK (generated from https://cloudforbots.com/openapi.json). Python 3.8+, standard library only."""
import json, os, time, urllib.request, urllib.error, urllib.parse
__version__ = "1.0.0"

class CloudForBotsError(Exception):
    def __init__(self, status, body):
        e = (body or {}).get("error") or {}
        super().__init__(e.get("message") if isinstance(e, dict) else str(e) or "HTTP %s" % status)
        self.status, self.type, self.request_id, self.body = status, (e.get("type") if isinstance(e, dict) else e), (body or {}).get("request_id"), body

class _NS:
    def __init__(self, c, tool): self._c, self._t = c, tool

class _Loop(_NS):
    def check_step(self, idempotency_key=None, **args):
        """Call AFTER each tool call or agent step with what you just did and whether it failed. Loop Breaker fingerprints the call (tool + normalized args + error) and returns verdict ok, loop_detected or halted. Pro: on a loop, halt=true; stop the run, do not retry, and tell your user what repeated. Free watch-only: never halts, returns would_halt and logs it for the weekly report."""
        return self._c.call("loop-breaker", "check_step", args, idempotency_key)
    def loop_status(self, idempotency_key=None, **args):
        """Show a run's loops so far, whether it is halted, and its last 10 steps."""
        return self._c.call("loop-breaker", "loop_status", args, idempotency_key)
    def weekly_report(self, idempotency_key=None, **args):
        """Draft a plain-English report of the last 7 days for your key (or IP): runs, loops caught, wasted repeat steps, and the worst offenders. Read-only; sends nothing. Show it to your user."""
        return self._c.call("loop-breaker", "weekly_report", args, idempotency_key)
    def usage_and_upgrade(self, idempotency_key=None, **args):
        """Show today's usage for your key or IP and the $49/month All Access upgrade link (cloudforbots.com/offer). Free tier: 500 calls/day. Not metered."""
        return self._c.call("loop-breaker", "usage_and_upgrade", args, idempotency_key)

class _Memory(_NS):
    def save_checkpoint(self, idempotency_key=None, **args):
        """Call after each completed step with the state you would need to continue (cursor, ids done, partial results; up to 256 KB JSON). Pro saves it (last 20 per run, kept 30 days). Free watch-only saves nothing and returns would_save: exactly what would have been stored. Never put passwords or API keys in state."""
        return self._c.call("run-memory", "save_checkpoint", args, idempotency_key)
    def resume(self, idempotency_key=None, **args):
        """Call FIRST when a run starts. If a checkpoint exists for run_id (Pro), returns the last step, state and done list: continue from there and skip finished work. If none, start fresh."""
        return self._c.call("run-memory", "resume", args, idempotency_key)
    def list_checkpoints(self, idempotency_key=None, **args):
        """List recent checkpoints (step, size, time) for one run or all runs on your key."""
        return self._c.call("run-memory", "list_checkpoints", args, idempotency_key)
    def weekly_report(self, idempotency_key=None, **args):
        """Draft a plain-English report of the last 7 days: runs, checkpoints saved (Pro) or that would have been saved (free). Read-only; sends nothing."""
        return self._c.call("run-memory", "weekly_report", args, idempotency_key)
    def usage_and_upgrade(self, idempotency_key=None, **args):
        """Show today's usage for your key or IP and the $49/month All Access upgrade link (cloudforbots.com/offer). Free tier: 500 calls/day. Not metered."""
        return self._c.call("run-memory", "usage_and_upgrade", args, idempotency_key)

class _Budget(_NS):
    def open_budget(self, idempotency_key=None, **args):
        """Open a hard budget for one agent run before it starts: max dollars, tokens, calls and/or wall-clock seconds. Returns a handle. Give the handle to every sub-agent; they all draw from the same ledger, so one runaway loop or fork cannot exceed the run cap."""
        return self._c.call("budget-guard", "open_budget", args, idempotency_key)
    def reserve(self, idempotency_key=None, **args):
        """Call BEFORE each model call, tool call or sub-agent spawn with your estimate. Returns allowed=true and deducts it, or allowed=false with the limit that tripped. When allowed is false, stop, do not retry, and return your partial result to the user. Atomic, so parallel sub-agents cannot both slip under the cap."""
        return self._c.call("budget-guard", "reserve", args, idempotency_key)
    def record(self, idempotency_key=None, **args):
        """After a call, report the actual usage minus what you reserved (positive if it cost more, negative to give back an overestimate). Keeps the ledger honest without blocking."""
        return self._c.call("budget-guard", "record", args, idempotency_key)
    def weekly_report(self, idempotency_key=None, **args):
        """Draft a plain-English report of the last 7 days for your key (or IP): runs, spend, runs that went over their cap, and dollars spent after a cap would have stopped them. Read-only; sends nothing. Show it to your user."""
        return self._c.call("budget-guard", "weekly_report", args, idempotency_key)
    def budget_status(self, idempotency_key=None, **args):
        """See spend vs limits, remaining budget, whether the run is stopped and why, and the last 20 ledger events. Set close=true when the run finishes."""
        return self._c.call("budget-guard", "budget_status", args, idempotency_key)
    def usage_and_upgrade(self, idempotency_key=None, **args):
        """Show today's usage for your key or IP and the $49/month All Access upgrade link (cloudforbots.com/offer). Free tier: 500 calls/day. Not metered."""
        return self._c.call("budget-guard", "usage_and_upgrade", args, idempotency_key)

class _Approve(_NS):
    def check_action(self, idempotency_key=None, **args):
        """Before a consequential action, ask whether the user's rules auto-approve it. Returns auto_approve, needs_approval (with a plain-English approval card and a link the user can tap) or deny. Free plan is watch-only: it reports what it WOULD decide and logs it for the weekly report, but nothing counts as approved, so ask your user as you normally would."""
        return self._c.call("approval-rules", "check_action", args, idempotency_key)
    def get_decision(self, idempotency_key=None, **args):
        """Check whether the user approved or rejected an approval card (by card_id and card_token). Only act when status is approved."""
        return self._c.call("approval-rules", "get_decision", args, idempotency_key)
    def set_rules(self, idempotency_key=None, **args):
        """Save the user's rules (needs an API key so they stay yours). Each rule: {name, when:{type:[...], max_usd, min_usd, reversible, target_ends_with:[...]}, verdict:"auto"|"ask"|"deny"}. First matching rule wins; anything unmatched needs approval. Call once without confirm to preview, then with confirm=true."""
        return self._c.call("approval-rules", "set_rules", args, idempotency_key)
    def get_rules(self, idempotency_key=None, **args):
        """Show the rules in effect for your key (or the default starter rules)."""
        return self._c.call("approval-rules", "get_rules", args, idempotency_key)
    def weekly_report(self, idempotency_key=None, **args):
        """Draft a plain-English report of the last 7 days: actions checked, how many your rules would auto-approve, how many needed you, top kinds of asks, and rule suggestions. Read-only; sends nothing."""
        return self._c.call("approval-rules", "weekly_report", args, idempotency_key)
    def usage_and_upgrade(self, idempotency_key=None, **args):
        """Show today's usage for your key or IP and the $49/month All Access upgrade link (cloudforbots.com/offer). Free tier: 200 calls/day. Not metered."""
        return self._c.call("approval-rules", "usage_and_upgrade", args, idempotency_key)

class _Wake(_NS):
    def schedule_wake(self, idempotency_key=None, **args):
        """Schedule a wake-up for your agent. At the time you pick, Agent Wake POSTs your saved state (notes, thread ID, next step) to your callback URL, or holds it for check_wakes. Use it so a later, fresh session picks up where this one stopped. Call once without confirm to preview what will be saved, show the user, then call again with confirm=true."""
        return self._c.call("agent-wake", "schedule_wake", args, idempotency_key)
    def check_wakes(self, idempotency_key=None, **args):
        """Collect wake-ups that are due for your inbox (each returned once, with its saved state and thread ID) and list upcoming ones. Call this at the start of a session."""
        return self._c.call("agent-wake", "check_wakes", args, idempotency_key)
    def cancel_wake(self, idempotency_key=None, **args):
        """Cancel a pending wake-up by wake_id. Call once without confirm to preview, then with confirm=true."""
        return self._c.call("agent-wake", "cancel_wake", args, idempotency_key)
    def usage_and_upgrade(self, idempotency_key=None, **args):
        """Show today's usage for your key or IP and the $49/month All Access upgrade link. Free tier: 50 calls/day. Not metered."""
        return self._c.call("agent-wake", "usage_and_upgrade", args, idempotency_key)

class CloudForBots:
    def __init__(self, api_key=None, base_url="https://cloudforbots.com", max_retries=2, timeout=30):
        self.api_key = api_key or os.environ.get("CLOUDFORBOTS_API_KEY")
        if not self.api_key: raise ValueError("api_key required (or set CLOUDFORBOTS_API_KEY). Free keys: https://cloudforbots.com/offer")
        self.base_url, self.max_retries, self.timeout = base_url.rstrip("/"), max_retries, timeout
        self.loop = _Loop(self, "loop-breaker")
        self.memory = _Memory(self, "run-memory")
        self.budget = _Budget(self, "budget-guard")
        self.approve = _Approve(self, "approval-rules")
        self.wake = _Wake(self, "agent-wake")

    def call(self, tool, action, args=None, idempotency_key=None):
        return self.request("POST", "/api/v1/%s/%s" % (tool, action), args or {}, idempotency_key)

    def ledger_record(self, bot, status, **kw): return self.request("POST", "/api/v1/ledger", dict(kw, action="record", bot=bot, status=status))
    def ledger_feed(self, **kw): return self.request("POST", "/api/v1/ledger", dict(kw, action="feed"))
    def ledger_rollup(self, days=7): return self.request("POST", "/api/v1/ledger", {"action": "rollup", "days": days})
    def inbox_create(self, name): return self.request("POST", "/api/v1/inbox", {"action": "create", "name": name})
    def topic_subscribe(self, topic, queue): return self.request("POST", "/api/v1/topic", {"action": "subscribe", "topic": topic, "queue": queue})
    def topic_publish(self, topic, message, **kw): return self.request("POST", "/api/v1/topic", dict(kw, action="publish", topic=topic, message=message))
    def lock(self, action, name=None, **kw): return self.request("POST", "/api/v1/lock", dict(kw, action=action, name=name))
    def rate_take(self, name, limit, n=1): return self.request("POST", "/api/v1/rate", {"action": "take", "name": name, "limit": limit, "n": n})
    def timer(self, **kw): return self.request("POST", "/api/v1/cron", dict(kw, action="timer"))
    def browser(self, action, session="default", **kw): return self.request("POST", "/api/v1/browser", dict(kw, action=action, session=session))
    def browser_open(self, url, session="default", **kw): return self.browser("open", session, url=url, **kw)
    def browser_run(self, steps, session="default", **kw): return self.browser("run", session, steps=steps, **kw)
    def queue(self, action, queue="default", **kw): return self.request("POST", "/api/v1/queue", dict(kw, action=action, queue=queue))
    def queue_push(self, queue, message, **kw): return self.queue("push", queue, message=message, **kw)
    def queue_pull(self, queue, max=1, visibility_s=30): return self.queue("pull", queue, max=max, visibility_s=visibility_s)
    def queue_ack(self, queue, ids): return self.queue("ack", queue, ids=ids if isinstance(ids, list) else [ids])
    def cron(self, action, **kw): return self.request("POST", "/api/v1/cron", dict(kw, action=action))
    def cron_set(self, name, schedule, request): return self.cron("set", name=name, schedule=schedule, request=request)
    def secret_set(self, name, value, allowed_hosts): return self.request("POST", "/api/v1/secrets", {"action": "set", "name": name, "value": value, "allowed_hosts": allowed_hosts})
    def secrets_list(self): return self.request("POST", "/api/v1/secrets", {"action": "list"})
    def secret_delete(self, name): return self.request("POST", "/api/v1/secrets", {"action": "delete", "name": name})
    def fetch(self, url, **kw): return self.request("POST", "/api/v1/fetch", dict(kw, url=url))
    def sandbox(self, action, **kw): return self.request("POST", "/api/v1/sandbox", dict(kw, action=action))
    def sandbox_run(self, cmd, name="default", **kw): return self.sandbox("run", cmd=cmd, name=name, **kw)
    def sandbox_sync_from_store(self, name="default", **kw): return self.sandbox("sync_from_store", name=name, **kw)
    def sandbox_sync_to_store(self, name="default", **kw): return self.sandbox("sync_to_store", name=name, **kw)
    def store(self, action, **kw): return self.request("POST", "/api/v1/store", dict(kw, action=action))
    def store_get(self, key, ns="default"): return self.store("get", key=key, ns=ns)
    def store_set(self, key, value, ns="default", ttl_seconds=None, if_version=None): return self.store("set", key=key, value=value, ns=ns, **{k: v for k, v in (("ttl_seconds", ttl_seconds), ("if_version", if_version)) if v is not None})
    def store_list(self, ns="default", prefix=None): return self.store("list", ns=ns, **({"prefix": prefix} if prefix else {}))
    def store_delete(self, key, ns="default"): return self.store("delete", key=key, ns=ns)
    def file_put(self, name, data, ns="default", content_type="application/octet-stream"):
        import base64; return self.store("file_put", name=name, ns=ns, content_type=content_type, content_base64=base64.b64encode(data if isinstance(data, bytes) else data.encode()).decode())
    def file_get(self, name, ns="default"):
        import base64; r = self.store("file_get", name=name, ns=ns); return base64.b64decode(r["content_base64"]) if r.get("found") else None
    def webhooks_list(self): return self.request("GET", "/api/v1/webhooks")
    def webhooks_deliveries(self, filter=None): return self.request("GET", "/api/v1/webhooks/deliveries" + ("?filter=" + urllib.parse.quote(filter) if filter else ""))
    def webhooks_redeliver(self, delivery_id): return self.request("POST", "/api/v1/webhooks/redeliver/" + urllib.parse.quote(delivery_id))
    def webhooks_rotate(self, url): return self.request("POST", "/api/v1/webhooks/rotate", {"url": url})
    def webhooks_add(self, url, events=None): return self.request("POST", "/api/v1/webhooks", {"url": url, "events": events})
    def webhooks_remove(self, url): return self.request("DELETE", "/api/v1/webhooks?url=" + urllib.parse.quote(url, safe=""))
    def audit(self, limit=50): return self.request("GET", "/api/v1/audit?limit=%d" % limit)

    def request(self, method, path, body=None, idempotency_key=None):
        h = {"authorization": "Bearer " + self.api_key, "content-type": "application/json", "user-agent": "cloudforbots-python/" + __version__}
        if idempotency_key: h["idempotency-key"] = idempotency_key
        data = None if body is None or method == "GET" else json.dumps(body).encode()
        attempt = 0
        while True:
            try:
                with urllib.request.urlopen(urllib.request.Request(self.base_url + path, data=data, headers=h, method=method), timeout=self.timeout) as r: return json.load(r)
            except urllib.error.HTTPError as e:
                try: b = json.loads(e.read() or b"{}")
                except ValueError: b = {}
                retry = e.code == 429 or (e.code >= 500 and (method == "GET" or idempotency_key))
                if not retry or attempt >= self.max_retries: raise CloudForBotsError(e.code, b)
                time.sleep(float(e.headers.get("retry-after") or 0) or 0.25 * 2 ** attempt); attempt += 1

def verify_webhook(secret, signature_header, raw_body, tolerance=300):
    """Check a cfb-signature header. raw_body: bytes exactly as received."""
    import hmac, hashlib
    pairs = [p.split("=", 1) for p in signature_header.split(",") if "=" in p]
    t = next((v for k, v in pairs if k == "t"), "")
    if not t.isdigit() or abs(time.time() - int(t)) > tolerance: return False
    exp = hmac.new(secret.encode(), (t + ".").encode() + raw_body, hashlib.sha256).hexdigest()
    # after a secret rotation there are two v1 values for 24h; accept if any matches
    return any(hmac.compare_digest(exp, v) for k, v in pairs if k == "v1")
