"""CalyxCash direct API helper. Python standard library; no hosted model.

Keep CALYXCASH_API_KEY in your server environment. Save the request key before
execution and reuse it after a lost response. This helper never retries paid
work automatically, with one exception: a 429 capacity refusal (nothing ran, nothing
charged) is retried with the same request key after the stated Retry-After, within
a bounded total wait. A 202 response contains the receipt to poll without charge.
"""
import json
import os
import time
import urllib.error
import urllib.request
import uuid


def new_request_key():
    return str(int(time.time()))+'_'+uuid.uuid4().hex


class CalyxCashError(Exception):
    def __init__(self, code, status=None, response=None, request_key=None):
        super().__init__(code)
        self.code, self.status, self.response, self.request_key = code, status, response, request_key


class CalyxCash:
    def __init__(self, api_key=None):
        self._key=api_key or os.environ['CALYXCASH_API_KEY']

    def _request(self, path, body=None, request_key=None):
        headers={'Authorization':'Bearer '+self._key,'Accept':'application/json','User-Agent':'CalyxCashClient/1.0 (+https://calyxcash.com/developers)'}
        if body is not None:headers['Content-Type']='application/json'
        if request_key:headers['Idempotency-Key']=request_key
        request=urllib.request.Request('https://calyxcash.com'+path,
            data=None if body is None else json.dumps(body,allow_nan=False).encode(),headers=headers)
        try:
            with urllib.request.urlopen(request,timeout=105) as response:
                data=json.load(response)
                if request_key:data['request_key']=request_key
                return data
        except urllib.error.HTTPError as error:
            try:data=json.load(error)
            except (ValueError,UnicodeDecodeError):data={}
            raise CalyxCashError(data.get('code',data.get('error','HTTP_ERROR')),error.code,data,request_key) from None
        except (urllib.error.URLError,TimeoutError,OSError):
            raise CalyxCashError('TRANSPORT_UNCERTAIN_REUSE_REQUEST_KEY',request_key=request_key) from None

    def capabilities(self):return self._request('/v1/capabilities')
    def quote(self, tool, arguments):return self._request('/v1/quote',{'tool':tool,'arguments':arguments})
    def usage(self):return self._request('/v1/usage')
    def receipt(self, receipt_id):
        if len(receipt_id)!=64 or any(c not in '0123456789abcdef' for c in receipt_id):raise ValueError('Invalid receipt ID')
        return self._request('/v1/receipts/'+receipt_id)

    def read(self, tool, arguments, *, max_price_micro_usd, request_key, capacity_wait_s=40):
        """Quote then execute under an explicit price cap and saved request key.

        Quotes and calls use the same arguments. A replay preserves its original
        charge even if the current tariff differs. To recover a known receipt
        directly, use receipt(id), which does not re-quote or charge.
        """
        if type(max_price_micro_usd) is not int or max_price_micro_usd<0:raise ValueError('Use integer micro-USD')
        quote=self.quote(tool,arguments)
        if not quote['paid_reads_enabled']:raise CalyxCashError('CALYXCASH_API_NOT_OPEN',request_key=request_key)
        if quote['quote']['price_micro_usd']>max_price_micro_usd:raise CalyxCashError('PRICE_EXCEEDS_CLIENT_LIMIT',response=quote,request_key=request_key)
        body={'tool':tool,'arguments':arguments,'tariff':quote['quote']['tariff'],'max_price_micro_usd':max_price_micro_usd}
        deadline=time.monotonic()+capacity_wait_s
        while True:
            try:return self._request('/v1/query',body,request_key)
            except CalyxCashError as error:
                wait=(error.response or {}).get('retry_after_s')
                wait=wait if type(wait) is int and wait>0 else 2
                # 429 = every reader was busy: nothing ran or was charged; the same key is safe to send again.
                if error.status!=429 or time.monotonic()+wait>deadline:raise
                time.sleep(wait)
