from typing import (
    Any,
    Iterable,
    Iterator,
    Tuple,
    Union,
)
from urllib import (
    parse,
)

from eth_typing import (
    URI,
    Hash32,
)

from eth_utils.currency import (
    denoms,
    from_wei,
)

from .toolz import (
    sliding_window,
    take,
)


def humanize_seconds(seconds: Union[float, int]) -> str:
    if int(seconds) == 0:
        return "0s"

    unit_values = _consume_leading_zero_units(_humanize_seconds(int(seconds)))

    return "".join((f"{amount}{unit}" for amount, unit in take(3, unit_values)))


SECOND = 1
MINUTE = 60
HOUR = 60 * 60
DAY = 24 * HOUR
YEAR = 365 * DAY
MONTH = YEAR // 12
WEEK = 7 * DAY


UNITS = (
    (YEAR, "y"),
    (MONTH, "m"),
    (WEEK, "w"),
    (DAY, "d"),
    (HOUR, "h"),
    (MINUTE, "m"),
    (SECOND, "s"),
)


def _consume_leading_zero_units(
    units_iter: Iterator[Tuple[int, str]]
) -> Iterator[Tuple[int, str]]:
    for amount, unit in units_iter:
        if amount == 0:
            continue
        else:
            yield (amount, unit)
            break

    yield from units_iter


def _humanize_seconds(seconds: int) -> Iterator[Tuple[int, str]]:
    remainder = seconds

    for duration, unit in UNITS:
        if not remainder:
            break

        num = remainder // duration
        yield num, unit

        remainder %= duration


DISPLAY_HASH_CHARS = 4


def humanize_bytes(value: bytes) -> str:
    if len(value) <= DISPLAY_HASH_CHARS + 1:
        return value.hex()
    value_as_hex = value.hex()
    head = value_as_hex[:DISPLAY_HASH_CHARS]
    tail = value_as_hex[-1 * DISPLAY_HASH_CHARS :]
    return f"{head}..{tail}"


def humanize_hexstr(value: str) -> str:
    tail = value[-1 * DISPLAY_HASH_CHARS :]

    if value[:2] == "0x":
        if len(value[2:]) <= DISPLAY_HASH_CHARS * 2:
            return value
        head = value[2 : DISPLAY_HASH_CHARS + 2]
        return f"0x{head}..{tail}"
    else:
        if len(value) <= DISPLAY_HASH_CHARS * 2:
            return value
        head = value[:DISPLAY_HASH_CHARS]
        return f"{head}..{tail}"


def humanize_hash(value: Hash32) -> str:
    return humanize_bytes(value)


def humanize_ipfs_uri(uri: URI) -> str:
    if not is_ipfs_uri(uri):
        raise TypeError(
            f"{uri} does not look like a valid IPFS uri. Currently, "
            "only CIDv0 hash schemes are supported."
        )

    parsed = parse.urlparse(uri)
    ipfs_hash = parsed.netloc
    head = ipfs_hash[:DISPLAY_HASH_CHARS]
    tail = ipfs_hash[-1 * DISPLAY_HASH_CHARS :]
    return f"ipfs://{head}..{tail}"


def is_ipfs_uri(value: Any) -> bool:
    if not isinstance(value, str):
        return False

    parsed = parse.urlparse(value)
    if parsed.scheme != "ipfs" or not parsed.netloc:
        return False

    return _is_CIDv0_ipfs_hash(parsed.netloc)


def _is_CIDv0_ipfs_hash(ipfs_hash: str) -> bool:
    if ipfs_hash.startswith("Qm") and len(ipfs_hash) == 46:
        return True
    return False


def _find_breakpoints(*values: int) -> Iterator[int]:
    yield 0
    for index, (left, right) in enumerate(sliding_window(2, values), 1):
        if left + 1 == right:
            continue
        else:
            yield index
    yield len(values)


def _extract_integer_ranges(*values: int) -> Iterator[Tuple[int, int]]:
    """
    Return a tuple of consecutive ranges of integers.

    :param values: a sequence of ordered integers

    - fn(1, 2, 3) -> ((1, 3),)
    - fn(1, 2, 3, 7, 8, 9) -> ((1, 3), (7, 9))
    - fn(1, 7, 8, 9) -> ((1, 1), (7, 9))
    """
    for left, right in sliding_window(2, _find_breakpoints(*values)):
        chunk = values[left:right]
        yield chunk[0], chunk[-1]


def _humanize_range(bounds: Tuple[int, int]) -> str:
    left, right = bounds
    if left == right:
        return str(left)
    else:
        return f"{left}-{right}"


def humanize_integer_sequence(values_iter: Iterable[int]) -> str:
    """
    Return a concise, human-readable string representing a sequence of integers.

    - fn((1, 2, 3)) -> '1-3'
    - fn((1, 2, 3, 7, 8, 9)) -> '1-3|7-9'
    - fn((1, 2, 3, 5, 7, 8, 9)) -> '1-3|5|7-9'
    - fn((1, 7, 8, 9)) -> '1|7-9'
    """
    values = tuple(values_iter)
    if not values:
        return "(empty)"
    else:
        return "|".join(map(_humanize_range, _extract_integer_ranges(*values)))


def humanize_wei(number: int) -> str:
    if number >= denoms.finney:
        unit = "ether"
    elif number >= denoms.mwei:
        unit = "gwei"
    else:
        unit = "wei"
    amount = from_wei(number, unit)
    x = f"{str(amount)} {unit}"
    return x
