import asyncio
import logging
from collections.abc import Iterable
from decimal import Decimal
from typing import List

import aiohttp
from aleph_message.models import ItemHash, PaymentType
from eth_typing import HexAddress
from eth_utils import from_wei
from superfluid import CFA_V1, Web3FlowInfo

from aleph.vm.conf import settings
from aleph.vm.models import VmExecution
from aleph.vm.utils import to_normalized_address

from .chain import ChainInfo, InvalidChainError, get_chain

logger = logging.getLogger(__name__)


async def get_address_balance(address: str) -> dict:
    """
    Get the balance of the user from the PyAleph API.

    API Endpoint:
        GET /api/v0/addresses/{address}/balance

    For more details, see the PyAleph API documentation:
    https://github.com/aleph-im/pyaleph/blob/master/src/aleph/web/controllers/routes.py#L62
    """

    async with aiohttp.ClientSession() as session:
        url = f"{settings.API_SERVER}/api/v0/addresses/{address}/balance"
        resp = await session.get(url)

        # Consider the balance as null if the address is not found
        if resp.status == 404:
            return {}

        # Raise an error if the request failed
        resp.raise_for_status()
        ret = await resp.json()
        if not isinstance(ret, dict):
            msg = f"get_address_blaance: Invalid response received from  {url}, should be dict"
            raise Exception(msg)

        return ret


async def fetch_balance_of_address(address: str) -> Decimal:
    """
    Get the balance of the user from the PyAleph API.
    """

    resp_data = await get_address_balance(address)
    balance = resp_data.get("balance", 0)
    if not isinstance(balance, Decimal | int | float | str | tuple):
        logger.warning(
            "Invalid balance format for address %s: %r. API Server : %s", address, balance, settings.API_SERVER
        )
        return Decimal(0)
    return Decimal(balance)


async def fetch_credit_balance_of_address(address: str) -> Decimal:
    """
    Get the credits of the user from the PyAleph API.
    """

    resp_data = await get_address_balance(address)
    balance = resp_data.get("credit_balance", 0)
    if not isinstance(balance, Decimal | int | float | str | tuple):
        logger.warning(
            "Invalid balance format for address %s: %r. API Server : %s", address, balance, settings.API_SERVER
        )
        return Decimal(0)
    return Decimal(balance)


async def fetch_execution_price(
    item_hash: ItemHash, allowed_payments: List[PaymentType], payment_type_required: bool = True
) -> Decimal:
    """Fetch the credit price of an execution from the reference API server."""
    async with aiohttp.ClientSession() as session:
        url = f"{settings.API_SERVER}/api/v0/price/{item_hash}"
        resp = await session.get(url)
        # Raise an error if the request failed
        resp.raise_for_status()

        resp_data = await resp.json()
        required_cost: float = resp_data["cost"]
        payment_type: str | None = resp_data["payment_type"]

        if payment_type_required and payment_type is None:
            msg = "Payment type must be specified in the message"
            raise ValueError(msg)

        if payment_type:
            if payment_type not in allowed_payments:
                msg = f"Payment type {payment_type} is not supported"
                raise ValueError(msg)

        return Decimal(required_cost)


class InvalidAddressError(ValueError):
    """The blockchain address could not be parsed."""

    pass


async def get_stream(sender: str, receiver: str, chain: str) -> Decimal:
    """
    Get the stream of the user from the Superfluid API.
    See https://community.aleph.im/t/pay-as-you-go-using-superfluid/98/11
    """
    chain_info: ChainInfo = get_chain(chain=chain)
    if not chain_info.active:
        msg = f"Chain : {chain} is not active for superfluid"
        raise InvalidChainError(msg)

    superfluid_instance = CFA_V1(chain_info.rpc, chain_info.chain_id)

    try:
        super_token: HexAddress = to_normalized_address(chain_info.super_token)
    except ValueError as error:
        msg = f"Invalid token address '{chain_info.super_token}' - {error.args}"
        raise InvalidAddressError(msg) from error

    try:
        sender_address: HexAddress = to_normalized_address(sender)
    except ValueError as error:
        msg = f"Invalid sender address '{sender}' - {error.args}"
        raise InvalidAddressError(msg) from error

    try:
        receiver_address: HexAddress = to_normalized_address(receiver)
    except ValueError as error:
        msg = f"Invalid receiver address '{receiver}' - {error.args}"
        raise InvalidAddressError(msg) from error

    # Run the network request in a background thread and wait for it to complete.
    loop = asyncio.get_event_loop()
    flow_data: Web3FlowInfo = await loop.run_in_executor(
        None, superfluid_instance.get_flow, super_token, sender_address, receiver_address
    )
    # TODO: Implement and use the SDK to make the conversion
    stream = from_wei(flow_data["flowRate"], "ether")
    return Decimal(stream)


async def compute_required_balance(executions: Iterable[VmExecution]) -> Decimal:
    """Get the balance required for the resources of the user from the messages and the pricing aggregate."""
    costs = await asyncio.gather(
        *(
            fetch_execution_price(execution.vm_hash, [PaymentType.hold], payment_type_required=False)
            for execution in executions
        )
    )
    return sum(costs, Decimal(0))


async def compute_required_credit_balance(executions: Iterable[VmExecution]) -> Decimal:
    """Get the balance required for the resources of the user from the messages and the pricing aggregate."""
    costs = await asyncio.gather(
        *(fetch_execution_price(execution.vm_hash, [PaymentType.credit]) for execution in executions)
    )
    return sum(costs, Decimal(0))


async def compute_required_flow(executions: Iterable[VmExecution]) -> Decimal:
    """Compute the flow required for a collection of executions, typically all executions from a specific address"""
    flows = await asyncio.gather(
        *(fetch_execution_price(execution.vm_hash, [PaymentType.superfluid]) for execution in executions)
    )
    return sum(flows, Decimal(0))
