"""Utils for messages."""

import logging
import re
from datetime import datetime, timedelta, timezone
from hashlib import sha256
from math import floor
from typing import Any, Dict, List, Union

LOGGER = logging.getLogger(__name__)
I32_BOUND = 2**31


def datetime_to_str(dt: Union[str, datetime, None]) -> Union[str, None]:
    """Convert a datetime object to an indy-standard datetime string.

    Args:
        dt: May be a string or datetime to allow automatic conversion
    """
    if isinstance(dt, datetime):
        dt = dt.replace(tzinfo=timezone.utc).isoformat().replace("+00:00", "Z")
    return dt


def str_to_datetime(dt: Union[str, datetime]) -> datetime:
    """Convert an indy-standard datetime string to a datetime.

    Using a fairly lax regex pattern to match slightly different formats.
    In Python 3.7 datetime.fromisoformat might be used.

    Args:
        dt: May be a string or datetime to allow automatic conversion

    """
    if isinstance(dt, str):
        match = re.match(
            r"^(\d{4})-(\d\d)-(\d\d)[T ](\d\d):(\d\d)"
            r"(?:\:(\d\d(?:\.\d+)?))?([+-]\d\d:?\d\d|Z|)$",
            dt,
        )
        if not match:
            raise ValueError("String does not match expected time format")
        year, month, day = match[1], match[2], match[3]
        hour, minute, second = match[4], match[5], match[6]
        tz = match[7]
        if second:
            flt_second = float(second)
            second = floor(flt_second)
            microsecond = round((flt_second - second) * 1_000_000)
        else:
            second = 0
            microsecond = 0
        result = datetime(
            int(year),
            int(month),
            int(day),
            int(hour),
            int(minute),
            int(second),
            microsecond,
            timezone.utc,
        )
        if tz != "Z" and tz != "":
            tz_sgn = int(tz[0] + "1")
            tz_hours = int(tz[1:3])
            tz_mins = int(tz[-2:])
            if tz_hours or tz_mins:
                result = result - timedelta(minutes=tz_sgn * (tz_hours * 60 + tz_mins))
        return result
    return dt


def str_to_epoch(dt: Union[str, datetime]) -> int:
    """Convert an indy-standard datetime string to epoch seconds.

    Args:
        dt: May be a string or datetime to allow automatic conversion

    """
    return int(str_to_datetime(dt).timestamp())


def epoch_to_str(epoch: int) -> str:
    """Convert epoch seconds to indy-standard datetime string.

    Args:
        epoch: epoch seconds

    """
    return datetime_to_str(datetime.fromtimestamp(epoch, tz=timezone.utc))


def datetime_now() -> datetime:
    """Timestamp in UTC."""
    return datetime.now(tz=timezone.utc)


def time_now() -> str:
    """Timestamp in ISO format."""
    return datetime_to_str(datetime_now())


def encode(orig: Any) -> str:
    """Encode a credential value as an int.

    Encode credential attribute value, purely stringifying any int32
    and leaving numeric int32 strings alone, but mapping any other
    input to a stringified 256-bit (but not 32-bit) integer.
    Predicates in indy-sdk operate
    on int32 values properly only when their encoded values match their raw values.

    Args:
        orig: original value to encode

    Returns:
        encoded value

    """

    if isinstance(orig, int) and -I32_BOUND <= orig < I32_BOUND:
        return str(int(orig))  # python bools are ints

    try:
        i32orig = int(str(orig))  # don't encode floats as ints
        if -I32_BOUND <= i32orig < I32_BOUND:
            return str(i32orig)
    except (ValueError, TypeError):
        pass

    rv = int.from_bytes(sha256(str(orig).encode()).digest(), "big")

    return str(rv)


def canon(raw_attr_name: str) -> str:
    """Canonicalize input attribute name for indy proofs and credential offers.

    Args:
        raw_attr_name: raw attribute name

    Returns:
        canonicalized attribute name

    """
    if raw_attr_name:  # do not dereference None, and "" is already canonical
        return raw_attr_name.replace(" ", "").lower()
    return raw_attr_name


def get_proto_default_version(
    versions: List[Dict[str, Any]], major_version: int = 1
) -> str:
    """Return default protocol version from version definition list."""

    for version in versions:
        if major_version == version["major_version"]:
            default_major_version = version["major_version"]
            default_minor_version = version["current_minor_version"]
            return f"{default_major_version}.{default_minor_version}"

    return "1.0"
