"""Client for interacting with the Fewsats API"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.

# %% auto 0
__all__ = ['Fewsats', 'FewsatsWebhookEvent', 'Offer', 'L402Offers', 'get_response', 'wrap_with_response']

# %% ../nbs/00_core.ipynb 3
from fastcore.utils import *
import os
import hashlib
import hmac
import httpx
import time
import json
from dataclasses import dataclass
from fastcore.basics import BasicRepr
from fastcore.utils import store_attr
from typing import List, Dict, Any
from functools import wraps


# %% ../nbs/00_core.ipynb 8
class Fewsats:

    WEBHOOK_VERSION = "v1"
    WEBHOOK_SIGNATURE_HEADER = "Fewsats-Signature"

    "Client for interacting with the Fewsats API"
    def __init__(self,
                 api_key: str = None, # The API key for the Fewsats account
                 base_url: str = "https://api.fewsats.com"): # The Fewsats API base URL
        self.api_key = api_key or os.environ.get("FEWSATS_API_KEY")
        if not self.api_key:
            raise ValueError("The api_key client option must be set either by passing api_key to the client or by setting the FEWSATS_API_KEY environment variable")
        self.base_url = base_url
        self._httpx_client = httpx.Client()
        self._httpx_client.headers.update({"Authorization": f"Token {self.api_key}"})

    def _request(self,
                method: str, # The HTTP method to use
                path: str, # The path to request
                timeout: int = 10, # Timeout for the request in s
                **kwargs) -> Dict[str, Any]:
        "Makes an authenticated request to Fewsats API"
        url = f"{self.base_url}/{path}"
        return  self._httpx_client.request(method, url, timeout=timeout, **kwargs)


# %% ../nbs/00_core.ipynb 12
@patch
def me(self: Fewsats):
    "Retrieve the user's info."
    return self._request("GET", "v0/users/me")


# %% ../nbs/00_core.ipynb 14
@patch
def billing_info(self: Fewsats):
    "Retrieve the user's billing info."
    return self._request("GET", "v0/users/me/billing-info")

# %% ../nbs/00_core.ipynb 17
@patch
def balance(self: Fewsats):
    "Retrieve the balance of the user's wallet. Amounts are always in USD cents."
    return self._request("GET", "v0/wallets")


# %% ../nbs/00_core.ipynb 20
@patch
def payment_methods(self: Fewsats) -> List[Dict[str, Any]]:
    "Retrieve the user's payment methods, raises an exception for error status codes."
    return self._request("GET", "v0/stripe/payment-methods")


# %% ../nbs/00_core.ipynb 24
@patch
def _preview_payment(self: Fewsats,
                    amount: str): # The amount in USD cents
    "Simulates a purchase, raises an exception for error status codes."
    assert amount.isdigit()
    return self._request("POST", "v0/l402/preview/purchase/amount", json={"amount_usd": amount})


# %% ../nbs/00_core.ipynb 27
@patch
def create_offers(self:Fewsats,
                 offers:List[Dict[str,Any]], # List of offer objects following OfferCreateV0 schema
) -> dict:
    "Create offers for L402 payment server"
    return self._request("POST", "v0/l402/offers", json={"offers": offers})

# %% ../nbs/00_core.ipynb 30
@patch
def get_payment_details(self:Fewsats,
                       payment_request_url:str, # The payment request URL
                       offer_id:str, # The offer ID
                       payment_method:str, # The payment method (lightning, credit_card, ...)
                       payment_context_token:str, # The payment context token
                       ) -> dict:
    """Gets payment details for a specific offer. Use this as buyer when you want to make the payment manually."""
    data = {"offer_id": offer_id, "payment_method": payment_method, "payment_context_token": payment_context_token}
    return httpx.post(payment_request_url, json=data)


# %% ../nbs/00_core.ipynb 33
@patch
def get_payment_status(self:Fewsats, 
                       payment_context_token:str, # The payment context token
                       ) -> dict:
    """Gets the status of a submitted payment. 
    Vendors should use this to check if anyone has paid for their offer associated with the token."""
    return self._request("GET", f"v0/l402/payment-status?payment_context_token={payment_context_token}")

# %% ../nbs/00_core.ipynb 36
@patch
def add_webhook(self:Fewsats,
                       webhook_url:str,
                       ) -> dict:
    """Add a URL to the list of webhooks that will receive notifications when you receive a payment.
    The webhook will be triggered for every successful payment."""
    return self._request("POST", f"v0/users/webhook/add", json={"webhook_url": webhook_url})

# %% ../nbs/00_core.ipynb 39
@dataclass
class FewsatsWebhookEvent:
    offer_id: str
    payment_context_token: str
    amount: int
    currency: str
    status: str
    timestamp: str

@patch(cls_method=True)
def verify_webhook(cls:Fewsats,
                   data: bytes,
                   signature: str,
                   webhook_secret: str,
                   ) -> dict:
    """
    Verify and parse a webhook that comes from Fewsats
    Args:
        data: bytes
        signature: str
        webhook_secret: str
    Returns:
        FewsatsWebhookEvent
    """
    if not isinstance(data, bytes):
        raise TypeError(f"'data' should be bytes, got {type(data)}")

    timestamp_str, signature_str = signature.split(",")
    timestamp = timestamp_str.split("=")[1]
    signature_version, signature = signature_str.split("=")

    payload_str = data.decode("utf-8")

    if signature_version != cls.WEBHOOK_VERSION:
        raise ValueError("Unsupported signature version")

    if timestamp.isdigit():
        timestamp = int(timestamp)
    else:
        raise ValueError("Invalid timestamp")

    if timestamp - time.time() > 300:
        raise ValueError("Timestamp is older than 5 minutes")

    signed_payload = f"{timestamp}.{payload_str}"

    # Generate the signature
    expected_signature = hmac.new(webhook_secret.encode(), signed_payload.encode(), hashlib.sha256).hexdigest()

    if signature.lower() != expected_signature.lower():
        raise ValueError("Webhook message hash does not match signature")

    event = json.loads(payload_str)
    return FewsatsWebhookEvent(**event)


# %% ../nbs/00_core.ipynb 42
@patch
def pay_lightning(self: Fewsats, 
                  invoice: str, # lightning invoice
                  amount: int, # amount in cents
                  currency: str = "usd", # currency
                  description: str = "" ): # description of the payment 
    "Pay for a lightning invoice directly."
    data = {
        "invoice": invoice,
        "amount": amount,
        "currency": currency,
        "description": description
    }
    return self._request("POST", "v0/l402/purchases/lightning", json=data)

# %% ../nbs/00_core.ipynb 45
class Offer(BasicRepr):
    "Represents a single L402 offer"
    def __init__(self, 
                 id: str,
                 amount: int,
                 currency: str,
                 description: str,
                 title: str,
                 payment_methods: List[str] = None,
                 type: str = "one-off"): 
        store_attr()

    def __repr__(self):
        return f"Offer: {self.title}\nID: {self.id}\nAmount: {self.amount/100} {self.currency}\nDescription: {self.description}"
    
    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> 'Offer':
        "Create an Offer from a dictionary"
        return cls(**d)

class L402Offers(BasicRepr):
    "Represents the complete L402 offers schema"
    def __init__(self, 
                 offers: List[Offer],
                 payment_context_token: str,
                 payment_request_url: str,
                 version: str): 
        store_attr()
    
    def __repr__(self):
        offers_str = "\n".join([f"- {o.title} ({o.amount/100} {o.currency})" for o in self.offers])
        return f"L402 Offers:\n{offers_str}\nPayment URL: {self.payment_request_url}\nContext Token: {self.payment_context_token}"
    
    def as_dict(self) -> Dict[str, Any]:
        "Convert to dictionary format for API usage"
        return {
            'offers': [vars(o) for o in self.offers],
            'payment_context_token': self.payment_context_token,
            'payment_request_url': self.payment_request_url,
            'version': self.version
        }
    
    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> 'L402Offers':
        "Create an L402Offers object from a dictionary"
        offers = [Offer.from_dict(o) for o in d['offers']]
        return cls(
            offers=offers,
            payment_context_token=d['payment_context_token'],
            payment_request_url=d['payment_request_url'],
            version=d['version']
        )


# %% ../nbs/00_core.ipynb 49
@patch
def pay_offer(self:Fewsats,
        offer_id : str, # the offer id to pay for
        l402_offer: L402Offers, # a dictionary containing L402 offers
) -> dict: # payment status response
    """Pays an offer_id from the l402_offers. 
    The l402_offer parameter must be a dictionary with this structure:
    {
        'offers': [
            {
                'id': 'test_offer_2',  # String identifier for the offer
                'amount': 1,                 # USD cents
                'currency': 'usd',           # Currency code
                'description': 'Test offer', # Text description
                'title': 'Test Package'      # Title of the package
            }
        ],
        'payment_context_token': 'token',  # Payment context token
        'payment_request_url': 'https://api.fewsats.com/v0/l402/payment-request',  # Payment URL
        'version': '0.2.2'  # API version
    }
    Returns payment status response"""
    if isinstance(l402_offer, dict): l402_offer = L402Offers.from_dict(l402_offer)
    offer_dict = l402_offer.as_dict()
    data = {"offer_id": offer_id, **offer_dict}
    return self._request("POST", "v0/l402/purchases/from-offer", json=data)


# %% ../nbs/00_core.ipynb 52
@patch
def pay_offer_str(self:Fewsats,
        offer_id : str, # the offer id to pay for
        l402_offer: str, # JSON string containing L402 offers
) -> dict: # payment status response
    """Pays an offer_id from the l402_offers.

    The l402_offer parameter must be a JSON string with this structure:
    {
        'offers': [
            {
                'id': 'test_offer_2',  # String identifier for the offer
                'amount': 1,                 # Numeric cost value
                'currency': 'usd',           # Currency code
                'description': 'Test offer', # Text description
                'title': 'Test Package'      # Title of the package
            }
        ],
        'payment_context_token': '60a8e027-8b8b-4ccf-b2b9-380ed0930283',  # Payment context token
        'payment_request_url': 'https://api.fewsats.com/v0/l402/payment-request',  # Payment URL
        'version': '0.2.2'  # API version
    }

    Returns payment status response"""
    # Parse JSON string to dictionary
    try:
        offer_data = json.loads(l402_offer)
        L402Offers.from_dict(offer_data) # we don't care about the return value, just validating the json input
    except json.JSONDecodeError:
        raise ValueError("Invalid JSON string provided for l402_offer")
    
    # Create payload with offer_id
    data = {"offer_id": offer_id, **offer_data}
    
    return self._request("POST", "v0/l402/purchases/from-offer", timeout=20, json=data)

# %% ../nbs/00_core.ipynb 55
@patch
def pay_link(self:Fewsats,
        url: str, # URL to purchase from
        description: str, # Description of the purchase
        price: int, # Price in USD cents
        payment_method: str = "credit_card", # Payment method (credit_card, lightning, etc.)
        title: str = "Purchase from URL" # Title of the purchase
) -> dict: # payment status response
    """Creates a purchase record for an external URL.
    
    Args:
        url: The URL to purchase from
        description: Description of the purchase
        price: Price in USD cents
        payment_method: Payment method to use (default: credit_card)
        title: Title of the purchase (default: "Purchase from URL")
        
    Returns:
        Payment status response containing information about the purchase
    """
    data = {
        "url": url,
        "description": description,
        "price": price,
        "payment_method": payment_method,
        "title": title
    }
    return self._request("POST", "v0/l402/purchases/from-link", json=data)

# %% ../nbs/00_core.ipynb 58
@patch
def payment_info(self:Fewsats,
                  pid:str): # purchase id
    "Retrieve the details of a payment."
    return self._request("GET", f"v0/l402/outgoing-payments/{pid}")

# %% ../nbs/00_core.ipynb 61
def get_response(r): return r.status_code, r.text

def wrap_with_response(method):
    """Wraps a method to return (status_code, text) instead of a Response object"""
    @wraps(method)  # This preserves name, docstring, signature
    def wrapped(*args, **kwargs):
        response = method(*args, **kwargs)
        return get_response(response)
    return wrapped

@patch
def as_tools(self:Fewsats):
    "Return list of available tools for AI agents"
    methods = [self.me, self.balance, self.payment_methods, self.pay_offer_str, self.payment_info]
    return [wrap_with_response(m) for m in methods]
