"""Utility functions for working with x402 payments on Stacks."""

from __future__ import annotations

import asyncio
import math
import os
import re
import time
from datetime import datetime, timezone
from typing import Any, Awaitable, Callable, Optional, TypeVar

from .types import NetworkType, TokenContract, TokenType
from .types_v2 import STACKS_NETWORKS

T = TypeVar("T")


# ── Currency Conversion ──────────────────────────────────────────────


def micro_stx_to_stx(micro_stx: int | str) -> str:
    amount = int(micro_stx)
    return f"{amount / 1_000_000:.6f}"


def stx_to_micro_stx(stx: float | str) -> int:
    amount = float(stx)
    return int(math.floor(amount * 1_000_000))


def sats_to_btc(sats: int | str) -> str:
    amount = int(sats)
    return f"{amount / 100_000_000:.8f}"


def btc_to_sats(btc: float | str) -> int:
    amount = float(btc)
    return int(math.floor(amount * 100_000_000))


def usdcx_to_micro_usdcx(usdcx: float | str) -> int:
    amount = float(usdcx)
    return int(math.floor(amount * 1_000_000))


def micro_usdcx_to_usdcx(micro_usdcx: int | str) -> str:
    amount = int(micro_usdcx)
    return f"{amount / 1_000_000:.6f}"


# ── Address Validation ───────────────────────────────────────────────


_MAINNET_RE = re.compile(r"^SP[0-9A-Z]{25,41}$")
_TESTNET_RE = re.compile(r"^ST[0-9A-Z]{25,41}$")


def is_valid_stacks_address(address: str) -> bool:
    return bool(_MAINNET_RE.match(address) or _TESTNET_RE.match(address))


def get_address_network(address: str) -> NetworkType | None:
    if not is_valid_stacks_address(address):
        return None
    return "mainnet" if address.startswith("SP") else "testnet"


def truncate_address(address: str, chars: int = 6) -> str:
    if len(address) <= chars * 2:
        return address
    return f"{address[:chars]}...{address[-chars:]}"


# ── Network Utilities ────────────────────────────────────────────────


def get_api_endpoint(network: NetworkType) -> str:
    if network == "mainnet":
        return "https://stacks-node-api.mainnet.stacks.co"
    return "https://stacks-node-api.testnet.stacks.co"


def get_explorer_url(tx_id: str, network: NetworkType = "mainnet") -> str:
    chain_param = "?chain=testnet" if network == "testnet" else ""
    return f"https://explorer.hiro.so/txid/0x{tx_id}{chain_param}"


def get_network_instance(network: NetworkType) -> str:
    """Return a network identifier string (Python has no StacksNetwork class)."""
    return network


# ── Token Utilities ──────────────────────────────────────────────────


def get_default_sbtc_contract(network: NetworkType) -> TokenContract:
    if network == "mainnet":
        return TokenContract(
            address="SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4",
            name="sbtc-token",
        )
    return TokenContract(
        address="ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT",
        name="sbtc-token",
    )


def get_default_usdcx_contract(network: NetworkType) -> TokenContract:
    if network == "mainnet":
        return TokenContract(
            address="SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE",
            name="usdcx",
        )
    return TokenContract(
        address="ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
        name="usdcx",
    )


def get_token_symbol(token_type: TokenType) -> str:
    return {"STX": "STX", "sBTC": "sBTC", "USDCx": "USDCx"}.get(token_type, "STX")


def get_token_decimals(token_type: TokenType) -> int:
    return {"sBTC": 8, "USDCx": 6}.get(token_type, 6)


def get_token_smallest_unit(token_type: TokenType) -> str:
    return {"sBTC": "sats", "USDCx": "micro-USDCx"}.get(token_type, "microSTX")


# ── Payment Utilities ────────────────────────────────────────────────


def format_payment_amount(
    amount: int | str,
    *,
    include_symbol: bool = True,
    decimals: int = 6,
    token_type: TokenType = "STX",
) -> str:
    if token_type == "sBTC":
        btc = sats_to_btc(amount)
        formatted = f"{float(btc):.{decimals}f}"
        symbol = "sBTC"
    elif token_type == "USDCx":
        usdcx = micro_usdcx_to_usdcx(amount)
        formatted = f"{float(usdcx):.{decimals}f}"
        symbol = "USDCx"
    else:
        stx = micro_stx_to_stx(amount)
        formatted = f"{float(stx):.{decimals}f}"
        symbol = "STX"

    return f"{formatted} {symbol}" if include_symbol else formatted


def parse_payment_memo(memo: str) -> dict[str, Any]:
    result: dict[str, Any] = {}
    if not memo.startswith("x402:"):
        return result

    content = memo[5:]
    parts = content.split(",")
    for part in parts:
        if "=" in part:
            key, value = part.split("=", 1)
            if key == "resource":
                result["resource"] = value
            elif key == "nonce":
                result["nonce"] = value
            else:
                result.setdefault("custom", {})[key] = value
        elif "resource" not in result:
            # First part without = is the resource
            result["resource"] = part

    return result


def create_payment_memo(
    resource: str,
    nonce: str,
    custom: Optional[dict[str, str]] = None,
) -> str:
    memo = f"x402:{resource},nonce={nonce}"
    if custom:
        for key, value in custom.items():
            memo += f",{key}={value}"
    return memo


def estimate_fee(transaction_size: int = 180, fee_rate: int = 1) -> int:
    return transaction_size * fee_rate


# ── Timing Utilities ─────────────────────────────────────────────────


def is_payment_request_expired(expires_at: str) -> bool:
    expiration = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
    return expiration < datetime.now(timezone.utc)


def create_expiration_timestamp(seconds_from_now: int) -> str:
    ts = datetime.now(timezone.utc).timestamp() + seconds_from_now
    return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat().replace("+00:00", "Z")


async def wait_with_backoff(
    attempt: int,
    base_delay_ms: int = 1000,
    max_delay_ms: int = 30000,
) -> None:
    delay = min(base_delay_ms * (2 ** attempt), max_delay_ms)
    await asyncio.sleep(delay / 1000)


async def retry_with_backoff(
    fn: Callable[[], Awaitable[T]],
    max_attempts: int = 3,
    base_delay_ms: int = 1000,
) -> T:
    last_error: Exception | None = None
    for attempt in range(max_attempts):
        try:
            return await fn()
        except Exception as exc:
            last_error = exc
            if attempt < max_attempts - 1:
                await wait_with_backoff(attempt, base_delay_ms)
    raise last_error or RuntimeError("Max retry attempts exceeded")


# ── CAIP-2 Network Utilities ─────────────────────────────────────────


def network_to_caip2(network: NetworkType) -> str:
    return STACKS_NETWORKS["MAINNET"] if network == "mainnet" else STACKS_NETWORKS["TESTNET"]


def network_from_caip2(caip2: str) -> NetworkType:
    if caip2 == STACKS_NETWORKS["MAINNET"]:
        return "mainnet"
    if caip2 == STACKS_NETWORKS["TESTNET"]:
        return "testnet"
    if caip2.startswith("stacks:"):
        raise ValueError(f"Unsupported Stacks network: {caip2}")
    raise ValueError(f"Invalid CAIP-2 format: {caip2}")


def is_valid_stacks_caip2(caip2: str) -> bool:
    return caip2 in (STACKS_NETWORKS["MAINNET"], STACKS_NETWORKS["TESTNET"])


def asset_to_v2(token_type: TokenType, token_contract: Optional[TokenContract] = None) -> str:
    if token_type == "STX":
        return "STX"
    if token_type == "sBTC":
        if token_contract:
            return f"{token_contract.address}.{token_contract.name}"
        return "SBTC"
    if token_type == "USDCx":
        if token_contract:
            return f"{token_contract.address}.{token_contract.name}"
        return "USDCX"
    return "STX"


def asset_from_v2(asset: str) -> dict[str, Any]:
    if asset == "STX":
        return {"token_type": "STX"}
    if asset == "SBTC":
        return {"token_type": "sBTC"}
    if asset == "USDCX":
        return {"token_type": "USDCx"}

    if "." in asset:
        address, name = asset.split(".", 1)
        lower_name = name.lower()

        if "sbtc" in lower_name or lower_name == "sbtc-token":
            return {
                "token_type": "sBTC",
                "token_contract": TokenContract(address=address, name=name),
            }
        if "usdc" in lower_name or lower_name == "usdcx":
            return {
                "token_type": "USDCx",
                "token_contract": TokenContract(address=address, name=name),
            }

        return {
            "token_type": "STX",
            "token_contract": TokenContract(address=address, name=name),
        }

    return {"token_type": "STX"}


# ── Key Generation ───────────────────────────────────────────────────


def generate_keypair(network: NetworkType = "testnet") -> dict[str, str]:
    """Generate a random Stacks keypair."""
    from .stacks_tx.keys import private_key_to_public_key, private_key_to_address

    private_key_bytes = os.urandom(32)
    private_key_hex = private_key_bytes.hex()
    public_key = private_key_to_public_key(private_key_hex)
    address = private_key_to_address(private_key_hex, network)

    return {
        "private_key": private_key_hex,
        "public_key": public_key.hex(),
        "address": address,
    }
