# pow client. just needs ur key, everything else is optional.
#
#   from client import PowClient          # this file, saved as client.py
#   c = PowClient("pow_YOURKEY")
#   print(c.solve(a=1039200696199, n=N, t=400000), "credits left:", c.last_balance)
#
# if u got curl_cffi installed its faster (reuses the connection + looks like a
# normal browser to cloudflare). no curl_cffi? it still works, just makes a new
# connection each solve. threads are safe.
#
# billing is all server side so edit this file as much as u want, u cant cheat it.
# failed/busy/ratelimited solves dont cost anything, only good ones charge.
#
# balance page (works in a browser too):
#   https://powstepowita.com/key/ur_key_here?format=json

import json
import random
import time
import urllib.error
import urllib.request

try:
    from curl_cffi import requests as _curl
except ImportError:
    _curl = None

DEFAULT_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
              "(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")


class PowError(Exception):
    pass


class OutOfCredits(PowError):
    # 402, key is empty or expired
    pass


class AuthError(PowError):
    # 401/403, key is wrong or revoked
    pass


class WorkerBusy(PowError):
    # 503, everythings saturated atm. retry, u werent charged
    pass


class RateLimited(PowError):
    # 429, too many reqs from ur ip. wait a few secs. not charged
    pass


class BadChallenge(PowError):
    # 400. usually the giant challenge (t over 10m) which we skip instantly.
    # just get a new challenge from the game. not charged
    pass


class PowClient:
    def __init__(self, api_key, base_url="https://powstepowita.com", timeout=120,
                 impersonate="chrome", user_agent=None, use_curl=True):
        if not api_key:
            raise ValueError("api_key is required")
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.user_agent = user_agent or DEFAULT_UA
        self.last_balance = None
        self._session = None
        if use_curl and _curl is not None:
            try:
                self._session = _curl.Session(impersonate=impersonate)
            except Exception:
                self._session = _curl.Session()

    def solve(self, a, n, t, retries=3):
        # solves a^(2^t) mod n, returns the answer as a string
        # 503/429 get retried automaticly (those never charge so its safe).
        # timeouts are NOT retried for u, the solve mightve gone thru and
        # ud pay twice. set retries=0 if u wanna handle it yourself
        payload = json.dumps({"a": str(a), "n": str(n), "t": int(t)}, separators=(",", ":"))
        for attempt in range(retries + 1):
            try:
                body = self._post_json("/v1/solve", payload)
            except (WorkerBusy, RateLimited):
                if attempt >= retries:
                    raise
                time.sleep(1.0 + attempt * 1.5 + random.random())
                continue
            if "result" not in body:
                raise PowError("weird response: %r" % (body,))
            self.last_balance = body.get("balance")
            return body["result"]

    def status(self):
        # same numbers as the balance page in the browser
        data = self._get_json("/key/" + self.api_key + "?format=json")
        self.last_balance = data.get("remaining")
        return data

    def _headers(self):
        return {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "X-Api-Key": self.api_key,
            "User-Agent": self.user_agent,
        }

    def _post_json(self, path, payload):
        url = self.base_url + path
        if self._session is not None:
            try:
                r = self._session.post(url, data=payload, headers=self._headers(), timeout=self.timeout)
            except Exception as e:
                raise PowError("connection failed: %s" % e)
            if r.status_code != 200:
                self._raise_status(r.status_code, r.text)
            return json.loads(r.text)
        req = urllib.request.Request(url, data=payload.encode("utf-8"),
                                     headers=self._headers(), method="POST")
        return self._urlopen(req)

    def _get_json(self, path):
        url = self.base_url + path
        if self._session is not None:
            try:
                r = self._session.get(url, headers=self._headers(), timeout=self.timeout)
            except Exception as e:
                raise PowError("connection failed: %s" % e)
            if r.status_code != 200:
                self._raise_status(r.status_code, r.text)
            return json.loads(r.text)
        req = urllib.request.Request(url, headers=self._headers(), method="GET")
        return self._urlopen(req)

    def _urlopen(self, req):
        try:
            with urllib.request.urlopen(req, timeout=self.timeout) as resp:
                return json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            detail = ""
            try:
                detail = e.read().decode("utf-8", "replace")
            except Exception:
                pass
            self._raise_status(e.code, detail)
        except urllib.error.URLError as e:
            raise PowError("connection failed: %s" % e.reason)

    @staticmethod
    def _raise_status(code, detail):
        detail = (detail or "")[:200]
        if code == 402:
            raise OutOfCredits("out of credits: " + detail)
        if code in (401, 403):
            raise AuthError("auth error (%d): %s" % (code, detail))
        if code == 503:
            raise WorkerBusy("workers busy (503): " + detail)
        if code == 429:
            raise RateLimited("rate limited (429): " + detail)
        if code == 400:
            raise BadChallenge("rejected (400): " + detail)
        raise PowError("HTTP %d: %s" % (code, detail))


if __name__ == "__main__":
    # put ur key here
    KEY = "pow_PASTE_YOUR_KEY_HERE"
    N = ("129393417108456164283921585331104445462431307741900082138691105331844855398554"
         "212458573231441604200465703439129706813254672848942347895477880876226203301248"
         "662188451297356271622235307002901121795899470611092395284818871859461098838476"
         "106386493078209233182532969052940256173016778609912307123487305344038163351")
    c = PowClient(KEY)
    print(c.solve(a=1039200696199, n=N, t=400000), "| credits left:", c.last_balance)
