import hashlib
import contextlib
import re
import socket


@contextlib.contextmanager
def fp_or_bytes(path_or_data):
    if isinstance(path_or_data, bytes):
        yield path_or_data
    else:
        with open(path_or_data, "rb") as fp:
            yield fp


def compute_checksums(path_or_data):
    h_md5 = hashlib.md5()
    h_sha1 = hashlib.sha1()
    h_sha256 = hashlib.sha256()
    if isinstance(path_or_data, bytes):
        h_md5.update(path_or_data)
        h_sha1.update(path_or_data)
        h_sha256.update(path_or_data)
        sz = len(path_or_data)
    else:
        sz = 0
        chunk = None
        with open(path_or_data, "rb") as fp:
            while chunk != b"":
                chunk = fp.read(1024)
                sz += len(chunk)
                h_md5.update(chunk)
                h_sha1.update(chunk)
                h_sha256.update(chunk)
    result = {
        "size": sz,
        "md5": h_md5.hexdigest(),
        "sha256": h_sha256.hexdigest(),
        "sha1": h_sha1.hexdigest(),
    }
    return result


def _drop_sfx(x):
    if x.endswith(".conda"):
        return x[:-6]
    if x.endswith(".tar.bz2"):
        return x[:-8]
    return x


def count_pkgs(pdict, filter=None):
    n_pkgs = sum(filter is None or filter(k, v) for k, v in pdict.items())
    n_blobs = len(
        set(_drop_sfx(k) for k, v in pdict.items() if filter is None or filter(k, v))
    )
    return n_pkgs, n_blobs


def site_to_url(ds, api_suffix=None):
    """
    Attempts to intelligently determine if a dest_site value can be mapped
    to a full URL. While not extremely strict, but it does these things:
    - If a valid http or https URL is supplied, it returns it unchanged
    - It confirms that the netloc is formatted as a a valid IPv4 address,
      IPv6 address, domain name, or "localhost"
    - It confirms that the port, if supplied, is an integer from 1-65536
    If these conditions are satisfied, it returns a URL, adding http://
    or https:// as appropriate. Otherwise it returns None. Examples:
    - 127.0.0.1 -> http://127.0.0.1
    - 127.0.0.1:443 -> https://127.0.0.1:443
    - anaconda.com/api -> https://anaconda.com/api
    - https://anaconda.com:84x3/api -> None
    """

    def _cleanup(url, secure):
        # Add scheme if missing
        # Strip any trailing slash
        # If the api suffix is missing, append it
        # If the api suffix is present, strip additional path elements
        if not url.startswith(("http://", "https://")):
            pfx = "https://" if secure else "http://"
            url = pfx + url
        url = url.rstrip("/")
        if api_suffix:
            sfx_s = "/" + api_suffix.lstrip("/")
            if not url.endswith(sfx_s):
                sfx_t = sfx_s + "/"
                if sfx_t in url:
                    url = url[: url.find(sfx_t) + len(sfx_s)]
                else:
                    url += sfx_s
        return url

    # Extract and normalize the netloc (which might include a port)
    _, netloc, _ = re.match("^(https?://)?([^/]+)(.*)$", ds).groups()
    netloc = netloc.encode("idna").decode("ascii").lower()

    # First check to see if it is a valid ipv6 host
    # without a trailing port value
    try:
        socket.inet_pton(socket.AF_INET6, netloc)
        return _cleanup(ds, False)
    except Exception:
        pass

    # If a port value is present, strip it off and check validity
    if ":" in netloc:
        netloc, port = netloc.rsplit(":", 1)
        try:
            port = int(port)
        except ValueError:
            return
        if port is None or not (0 < port < 65536):
            return
    else:
        port = None

    # A single trailing period is allowed
    if netloc.endswith("."):
        netloc = netloc[:-1]
    if not (0 < len(netloc) <= 255):
        return

    # Treat local hosts specially; default to http
    if netloc in ("localhost", "127.0.0.1", "0.0.0.0"):
        return _cleanup(ds, port in (443, 8443))

    # Check for valid domain names
    if not re.match(r"^([0-9a-z][-\w]*[0-9a-z]\.)+[a-z0-9\-]{2,15}$", netloc):
        # Check for IPv6 again with the port stripped off
        try:
            socket.inet_pton(socket.AF_INET6, netloc)
        except Exception:
            # Check for IPv4
            try:
                socket.inet_pton(socket.AF_INET, netloc)
            except Exception:
                return

    # Default to https unless port is 80, 8080, or 8088
    return _cleanup(ds, port not in (80, 8080, 8088))


class MirrorException(Exception):
    def __init__(self, msg):
        self.message = msg.lstrip()
