import json
import urllib.parse
from curl_cffi import requests
from wtf import generate_wtf
from datetime import datetime, timezone, timedelta

def getGifts(
    gift_name: str = None,
    model: str = None,
    backdrop: str = None,
    symbol: str = None,
    gift_num: int = None,
    page: int = 1,
    limit: int = 30,
    sort: str = "price_asc",
    price_range: list | int = 0,
    asset: str = "TON",
    user_auth: str = ""
) -> list:
    
    """
    Retrieve a list of gifts from Tonnel Marketplace

    Args:
        gift_name (str): The name of the gift to filter by.
        model (str): The model of the gift to filter by.
        backdrop (str): The backdrop of the gift to filter by.
        symbol (str): The symbol of the gift to filter by.
        gift_num (int): The gift number to filter by.
        page (int): The page number to retrieve. Default is 1.
        limit (int): The number of gifts to retrieve per page. Default = maximum limit = 30.
        sort (str): The sorting method to apply to the results. Available options: "price_asc", "price_desc", "latest", "mint_time", "rarity", "gift_id_asc", "gift_id_desc"
        price_range (list | int): The price range to filter by. If a list is provided, it should contain two integers: the minimum and maximum price. Default is 0 (no filter).
        asset (str): The asset to filter by. Default is "TON". Available options: "TON", "USDT", "TONNEL"
        user_auth (str): The user auth data required for authorization. Optional.
    Returns:
        list: A list of dict objects with gifts details. 
    """

    URL = "https://gifts2.tonnel.network/api/pageGifts"

    HEADERS = {
        "Host": "gifts2.tonnel.network",
        "Accept": "application/json, text/plain, */*",
        "Content-Type": "application/json",
    }

    SORTS = {
        "price_asc": "{\"price\":1,\"gift_id\":-1}",
        "price_desc": "{\"price\":-1,\"gift_id\":-1}",
        "latest": "{\"message_post_time\":-1,\"gift_id\":-1}",
        "mint_time": "{\"export_at\":1,\"gift_id\":-1}",
        "rarity": "{\"rarity\":-1,\"gift_id\":-1}",
        "gift_id_asc": "{\"gift_num\":1,\"gift_id\":-1}",
        "gift_id_desc": "{\"gift_num\":-1,\"gift_id\":-1}",
        "model_rarity": "{\"modelRarity\":1,\"gift_id\":-1}",
        "backdrop_rarity": "{\"backdropRarity\":1,\"gift_id\":-1}",
        "symbol_rarity": "{\"symbolRarity\":1,\"gift_id\":-1}"
    }

    try:
        sort_value = SORTS[sort]
    except KeyError:
        raise Exception("Invalid sort argument. Available sorts: " + str(list(SORTS.keys())))

    filter_dict = {
        "price": {"$exists": True},
        "refunded": {"$ne": True},
        "buyer": {"$exists": False},
        "export_at": {"$exists": True},
        "asset": asset
    }


    if gift_name:
        filter_dict["gift_name"] = gift_name.title()

    if model:
        filter_dict["model"] = {"$regex": f"^{model.title()} \\("}

    if backdrop:
        filter_dict["backdrop"] = {"$regex": f"^{backdrop.title()} \\("}

    if symbol:
        filter_dict["symbol"] = {"$regex": f"^{symbol.title()} \\("}

    if gift_num is not None:
        filter_dict["gift_num"] = str(gift_num)

    payload = {
        "filter": json.dumps(filter_dict),
        "limit": limit,
        "page": page,
        "sort": sort_value,
        "ref": 0,
        "user_auth": user_auth
    }

    if isinstance(price_range, list) and len(price_range) == 2:
        payload["price_range"] = price_range
    else:
        payload["price_range"] = 0

    response = requests.post(URL, headers=HEADERS, json=payload, impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"Request failed with status code {response.status_code}: {response.text}")

    return response.json()

def myGifts(
    listed: bool = True,
    page: int = 1,
    limit: int = 30,
    user_auth: str = ""
) -> list:
    
    """
    [Requires authentication]
    Fetches a list of your gifts on the market. The list can be filtered to
    either show only the gifts that are currently listed, or all the gifts
    that are unlisted.

    Args:
        listed: A boolean indicating whether to show only the listed gifts
            or all the non-listed gifts. Default is True.
        page: An integer indicating which page of the results to fetch.
            Default is 1.
        limit: An integer indicating how many results to fetch per page.
            Default is 30.
        user_auth: The user's auth data required for authorization.

    Returns:
        list: A list of dict objects with gifts details. 
    """
    if not user_auth:
        raise ValueError("user_auth is required")

    URL = "https://gifts2.tonnel.network/api/pageGifts"

    HEADERS = {
        "Host": "gifts2.tonnel.network",
        "Accept": "application/json, text/plain, */*",
        "Content-Type": "application/json",
    }

    parsed = urllib.parse.parse_qs(user_auth)
    user_data_json = parsed.get("user", ["{}"])[0]
    try:
        user_data = json.loads(urllib.parse.unquote(user_data_json))
        user_id = user_data["id"]
    except (json.JSONDecodeError, KeyError):
        raise Exception("Invalid user_auth format — could not extract user ID")

    if listed:
        sort_value = "{\"message_post_time\":-1,\"gift_id\":-1}"
        filter_dict = {
            "seller": user_id,
            "buyer": {"$exists": False},
            "$or": [
                {"price": {"$exists": True}},
                {"auction_id": {"$exists": True}}
            ]
        }
    else:
        sort_value = "{\"gift_num\":1,\"gift_id\":-1}"
        filter_dict = {
            "seller": user_id,
            "buyer": {"$exists": False},
            "refunded": {"$ne": True},
            "price": {"$exists": False}
        }

    payload = {
        "filter": json.dumps(filter_dict),
        "limit": limit,
        "page": page,
        "sort": sort_value,
        "ref": 0,
        "user_auth": user_auth
    }

    response = requests.post(URL, headers=HEADERS, json=payload, impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"Request failed with status code {response.status_code}: {response.text}")

    return response.json()

def listForSale(
    gift_id: int,
    price: int | float,
    user_auth: str
    ) -> dict:
    """
    [Requires authentication]
    Lists a gift for sale on the marketplace.

    Args:
        gift_id (int): The Tonnel Gift ID of the gift (not gift_num / telegram gift number) to be listed for sale. Can be retrieved from myGifts or getGifts.
        price (int | float): The price at which the gift will be listed.
        user_auth (str): The user's auth data required for authorization.

    Returns:
        dict: A dictionary containing the response data from the API. Either success or error.

    Raises:
        ValueError: If user_auth is not provided.
        Exception: If the API request fails, with details about the status code and response text.
    """

    if not user_auth:
        raise ValueError("user_auth is required")

    url = "https://gifts.coffin.meme/api/listForSale"

    headers = {
        "Content-Type": "application/json",
        "Host": "gifts.coffin.meme",
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "keep-alive"
    }

    timestamp, wtf = generate_wtf()

    payload = {
        "authData": user_auth,
        "gift_id": gift_id,
        "price": price,
        "asset": "TON",
        "timestamp": timestamp,
        "wtf": wtf
    }

    response = requests.post(url, headers=headers, json=payload, impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"Request failed: {response.status_code} — {response.text}")

    return response.json()

def cancelSale(
    gift_id: int,
    user_auth: str
    ) -> dict:
    
    """
    [Requires authentication]
    Cancels a gift that is currently listed for sale.

    Args:
        gift_id (int): The Tonnel Gift ID of the gift to cancel sale of (not gift_num / telegram gift number). Can be retrieved from myGifts or getGifts.
        user_auth (str): The user's auth data required for authorization.

    Returns:
        dict: A dictionary containing the response data from the API. Either success or error.

    Raises:
        ValueError: If user_auth is not provided.
        Exception: If the API request fails, with details about the status code and response text.
    """
    if not user_auth:
        raise ValueError("user_auth is required")

    url = "https://gifts.coffin.meme/api/cancelSale"

    headers = {
        "Content-Type": "application/json",
        "Host": "gifts.coffin.meme",
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "keep-alive"
    }

    timestamp, wtf = generate_wtf()

    payload = {
        "authData": user_auth,
        "gift_id": gift_id,
        "timestamp": timestamp,
        "wtf": wtf
    }

    response = requests.post(url, headers=headers, json=payload, impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"Request failed: {response.status_code} — {response.text}")

    return response.json()

def saleHistory(
    authData: str,
    page: int = 1,
    limit: int = 50,
    type: str = "ALL",
    gift_name: str = None,
    model: str = None,
    backdrop: str = None,
    sort: str = "latest"
) -> list:
    
    """
    [Requires authentication]
    Retrieves a list of gifts sold on the marketplace.

    Args:
        authData (str): The user's auth data required for authorization.
        page (int): The page number to retrieve. Default is 1.
        limit (int): The number of results to retrieve per page. Default = 50.
        type (str): The type of gifts to filter by. Available options: "ALL", "SALE", "INTERNAL_SALE", "BID"
        gift_name (str): The name of the gift to filter by.
        model (str): The model of the gift to filter by.
        backdrop (str): The backdrop of the gift to filter by.
        sort (str): The sorting method to apply to the results. Default is "latest". Available options: "latest", "price_asc", "price_desc", "gift_id_asc", "gift_id_desc".

    Returns:
        list: A list of dict objects with gifts details.

    Raises:
        ValueError: If authData is not provided.
        Exception: If the API request fails, with details about the status code and response text.
    """
    
    URL = "https://gifts2.tonnel.network/api/saleHistory"

    HEADERS = {
        "Host": "gifts2.tonnel.network",
        "Accept": "application/json, text/plain, */*",
        "Content-Type": "application/json",
    }

    SORTS = {
        "latest": "{\"timestamp\":-1,\"gift_id\":-1}",
        "price_asc": "{\"price\":1,\"gift_id\":-1}",
        "price_desc": "{\"price\":-1,\"gift_id\":-1}",
        "gift_id_asc": "{\"gift_num\":1,\"gift_id\":-1}",
        "gift_id_desc": "{\"gift_num\":-1,\"gift_id\":-1}"
    }

    try:
        sort_value = SORTS[sort]
    except KeyError:
        raise Exception("Invalid sort argument. Available sorts: " + str(list(SORTS.keys())))

    filter_dict = {}

    if gift_name:
        filter_dict["gift_name"] = gift_name.title()

    if model:
        filter_dict["model"] = {"$regex": f"^{model.title()} \\("}

    if backdrop:
        filter_dict["backdrop"] = {"$regex": f"^{backdrop.title()} \\("}

    payload = {
        "authData": authData,
        "page": page,
        "limit": limit,
        "type": type,
        "filter": filter_dict,
        "sort": json.loads(sort_value)
    }

    response = requests.post(URL, headers=HEADERS, json=payload, impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"Request failed with status code {response.status_code}: {response.text}")

    return response.json()

def getAuctions(
    gift_name: str = None,
    model: str = None,
    backdrop: str = None,
    symbol: str = None,
    gift_num: int = None,
    page: int = 1,
    limit: int = 30,
    sort: str = "ending_soon",
    price_range: list | int = 0,
    asset: str = "TON",
    user_auth: str = ""
) -> list:
    """
    Retrieves a list of auctions on the marketplace.

    Args:
        gift_name: The name of the gift to filter by.
        model: The model of the gift to filter by.
        backdrop: The backdrop of the gift to filter by.
        symbol: The symbol of the gift to filter by.
        gift_num: The gift number to filter by.
        page: An integer indicating which page of the results to fetch.
            Default is 1.
        limit: An integer indicating how many results to fetch per page.
            Default is 30.
        sort: A string indicating how to sort the results. Default is "ending_soon". Available options: "ending_soon", "latest", "highest_bid", "latest_bid".
        price_range: A list or integer indicating the price range to filter by.
            If a list is provided, it should contain two integers: the minimum
            and maximum price. If an integer is provided, it will be used as the
            minimum price and the maximum price will be set to infinity.
            Default is 0 (no filter).
        asset: A string indicating which asset to filter by. Default is "TON". Available options: "TON", "USDT", "TONNEL".
        user_auth: The user auth data required for authorization. Optional.

    Returns:
        list: A list of dict objects with auctions details.
    """
    URL = "https://gifts2.tonnel.network/api/pageGifts"

    HEADERS = {
        "Host": "gifts2.tonnel.network",
        "Accept": "application/json, text/plain, */*",
        "Content-Type": "application/json",
    }

    SORTS = {
        "ending_soon": "{\"auctionEndTime\":1,\"gift_id\":-1}",
        "latest": "{\"auctionStartTime\":-1,\"gift_id\":-1}",
        "highest_bid": "{\"bidHistory.amount\":-1,\"gift_id\":-1}",
        "latest_bid": "{\"bidHistory.timestamp\":-1,\"gift_id\":-1}"
    }

    try:
        sort_value = SORTS[sort]
    except KeyError:
        raise Exception("Invalid sort argument. Available sorts: " + str(list(SORTS.keys())))

    filter_dict = {
        "auction_id": {"$exists": True},
        "status": "active",
        "asset": asset
    }

    if gift_name:
        filter_dict["gift_name"] = gift_name.title()

    if model:
        filter_dict["model"] = {"$regex": f"^{model.title()} \\("}

    if backdrop:
        filter_dict["backdrop"] = {"$regex": f"^{backdrop.title()} \\("}

    if symbol:
        filter_dict["symbol"] = {"$regex": f"^{symbol.title()} \\("}

    if gift_num:
        filter_dict["gift_num"] = str(gift_num)

    payload = {
        "filter": json.dumps(filter_dict),
        "limit": limit,
        "page": page,
        "sort": sort_value,
        "ref": 0,
        "user_auth": user_auth
    }

    if isinstance(price_range, list) and len(price_range) == 2:
        payload["price_range"] = price_range
    else:
        payload["price_range"] = 0

    response = requests.post(URL, headers=HEADERS, json=payload, impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"Request failed with status code {response.status_code}: {response.text}")

    return response.json()

def iso(hours: int) -> str:
    dt = datetime.now(timezone.utc) + timedelta(hours=hours)
    return dt.isoformat(timespec="milliseconds").replace("+00:00", "Z")

def createAuction(
    gift_id: int,
    starting_bid: int | float,
    authData: str,
    duration: int = 1,
) -> dict:
    
    """
    [Requires authentication]
    Creates an auction for a gift on the marketplace.

    Args:
        gift_id (int): The Tonnel Gift ID of the gift (not gift_num / telegram gift number) to be auctioned. Can be retrieved from myGifts or getGifts.
        starting_bid (int | float): The starting bid price of the auction.
        authData (str): The user's auth data required for authorization.
        duration (int): The duration of the auction in hours. Default is 1. Available options: 1, 2, 3, 6, 12, 24

    Returns:
        dict: A dictionary containing the response data from the API. Either success or error.

    Raises:
        ValueError: If authData is not provided.
        ValueError: If duration is not one of the allowed durations.
        Exception: If the API request fails, with details about the status code and response text.
    """
    
    ALLOWED_DURATIONS = {1, 2, 3, 6, 12, 24}
    if not authData:
        raise ValueError("authData is required")

    if duration not in ALLOWED_DURATIONS:
        raise ValueError(
            f"duration_hours must be one of {sorted(ALLOWED_DURATIONS)} "
            f"(got {duration})"
        )

    auction_end_time = iso(duration)

    payload = {
        "authData"     : authData,
        "gift_id"      : gift_id,
        "startingBid"  : starting_bid,
        "auctionEndTime": auction_end_time
    }

    url = "https://gifts.coffin.meme/api/auction/create"
    headers = {
        "Content-Type" : "application/json",
        "Host"         : "gifts.coffin.meme",
        "Accept"       : "*/*",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection"   : "keep-alive"
    }

    response = requests.post(url, headers=headers, json=payload,
                         impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"createAuction failed {response.status_code}: {response.text}")

    return response.json()

def cancelAuction(
    auction_id: str,
    authData: str
    ) -> dict:
    """
    [Requires authentication]
    Cancels an auction on Tonnel Marketplace.

    Args:
        auction_id (str): The ID of the auction to cancel. Can be retrieved from getAuctions or myGifts.
        authData (str): The user's auth data required for authorization.

    Returns:
        dict: A dictionary containing the response data from the API. Either success or error.

    Raises:
        ValueError: If authData is not provided.
        Exception: If the API request fails, with details about the status code and response text.
    """
    if not authData:
        raise ValueError("authData is required")
    
    url = "https://gifts.coffin.meme/api/auction/cancel"
    headers = {
        "Content-Type": "application/json",
        "Host": "gifts.coffin.meme",
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "keep-alive"
    }

    payload = {
        "authData": authData,
        "auction_id": auction_id
    }

    response = requests.post(url, headers=headers, json=payload, impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"cancelAuction failed {response.status_code}: {response.text}")

    return response.json()

def buyGift(
    gift_id: int,
    price: int | float,
    authData: str
    ) -> dict:
    """
    [Requires authentication]
    Purchases a gift from the marketplace.

    Args:
        gift_id (int): The Tonnel Gift ID of the gift to purchase (not gift_num / telegram gift number).
        price (int | float): The price at which the gift is to be purchased.
        authData (str): The user's auth data required for authorization.

    Returns:
        dict: A dictionary containing the response data from the API. Either success or error.

    Raises:
        ValueError: If authData is not provided.
        Exception: If the API request fails, with details about the status code and response text.
    """

    if not authData:
        raise ValueError("authData is required")

    url = f"https://gifts.coffin.meme/api/buyGift/{gift_id}"
    
    headers = {
        "Content-Type": "application/json",
        "Host": "gifts.coffin.meme",
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "keep-alive",
        "User-Agent": "Mozilla/5.0"
    }

    timestamp, wtf = generate_wtf()

    payload = {
        "authData": authData,
        "asset": "TON",
        "price": float(price),
        "timestamp": timestamp,
        "wtf": wtf
    }

    response = requests.post(url, headers=headers, json=payload, impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"buy_gift failed {response.status_code}: {response.text}")

    return response.json()

def info(
    authData: str
    ) -> dict:
    """
    [Requires authentication]
    Retrieves information about the user

    Args:
        authData (str): The user's auth data required for authorization.

    Returns:
        dict: A dictionary containing balances, memo etc.

    Raises:
        ValueError: If authData is not provided.
        Exception: If the API request fails, with details about the status code and response text.
    """
    if not authData:
        raise ValueError("authdata is required")

    url = "https://gifts2.tonnel.network/api/balance/info"

    headers = {
        "Content-Type": "application/json",
        "Host": "gifts2.tonnel.network",
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "keep-alive",
        "User-Agent": "Mozilla/5.0"
    }

    payload = {
        "authData": authData
    }

    response = requests.post(url, headers=headers, json=payload, impersonate="chrome110")

    if response.status_code != 200:
        raise Exception(f"info failed {response.status_code}: {response.text}")

    return response.json()

class Gift:
    """
    A class representing a gift on the Tonnel Marketplace.

    Attributes:
        .gift_num : Telegram gift number
        .gift_id : Tonnel Gift ID
        .name : Name of the gift
        .model : Model of the gift
        .backdrop : Backdrop of the gift
        .symbol : Symbol of the gift
        .price : Price of the gift
        .status : Status of the gift
        .asset : Asset of the gift
        .export_at : Export time of the gift
        .customEmojiId : Custom emoji ID
        .auction : Auction details
    """
    def __init__(self, data: dict):
        self._data = data

    @property
    def gift_id(self) -> int:
        return self._data.get("gift_id")

    @property
    def gift_num(self) -> int:
        return self._data.get("gift_num")

    @property
    def name(self) -> str:
        return self._data.get("name")

    @property
    def model(self) -> str:
        return self._data.get("model")

    @property
    def backdrop(self) -> str:
        return self._data.get("backdrop")

    @property
    def symbol(self) -> str:
        return self._data.get("symbol")

    @property
    def price(self) -> float | None:
        return self._data.get("price")

    @property
    def status(self) -> str:
        return self._data.get("status")

    @property
    def asset(self) -> str:
        return self._data.get("asset")

    @property
    def export_at(self) -> str:
        return self._data.get("export_at")

    def to_dict(self) -> dict:
        """Return the raw data as a dictionary."""
        return self._data.copy()