# -*- coding: utf-8 -*-
import pip
import pip._internal
import requests
from os.path import expanduser, join, isfile, exists
import os
import subprocess
import json
import struct
from sys import version_info, executable
from itertools import product
import pyprind
import js2py
from bs4 import BeautifulSoup
import re
import ssl
from pySmartDL import SmartDL
from requests.adapters import HTTPAdapter
from urllib3.poolmanager import PoolManager
from urllib3.util.ssl_ import create_urllib3_context
import logging

try:
    unicode
except NameError:
    unicode = str

logger = logging.getLogger(__name__)

# Python 2.X 3.X input
try:
    input = raw_input
except NameError:
    pass

MAIN_URL = "http://www.lfd.uci.edu/~gohlke/pythonlibs/"

# Added header for postman client
HEADER = {
    "User-Agent": "PostmanRuntime/7.22.0",
    "Accept": "*/*",
    "Cache-Control": "no-cache",
    "Accept-Encoding": "gzip, deflate, br",
    "Referer": "https://www.lfd.uci.edu/~gohlke/pythonlibs",
    "Connection": "keep-alive",
}


class DESAdapter(HTTPAdapter):
    """
    Workaround to prevent https errors with newer versions of the requests package.

    Forces all HTTPS connections to be negotiated using a limited set of legacy
    TLS ciphers which are supported by the UCI LFD server. A short list of ciphers is
    used so that the handshake message does not exceed 255 bytes - see
    https://github.com/ssllabs/research/wiki/Long-Handshake-Intolerance

    If the UCI LFD server is ever upgraded, this code should be removed as the resulting
    connection is potentially vulnerable to attack!
    """

    CIPHERS = "RSA+3DES:ECDH+3DES:DH+3DES"

    def init_poolmanager(self, connections, maxsize, block=False, *args, **kwargs):
        context = create_urllib3_context(ciphers=DESAdapter.CIPHERS)
        kwargs["ssl_context"] = context
        self.poolmanager = PoolManager(
            num_pools=connections,
            maxsize=maxsize,
            block=block,
            ssl_version=ssl.PROTOCOL_SSLv23,
            *args,
            **kwargs
        )


def build_cache():
    """
    Get current data from the website http://www.lfd.uci.edu/~gohlke/pythonlibs/

    Returns
    -------
    Dictionary containing package details
    """
    data = {}
    response = requests.request("GET", MAIN_URL, headers=HEADER)

    soup = BeautifulSoup(response.text, features="html.parser")

    # We mock out a little javascript environment within which to run Gohlke's obfuscation code
    context = js2py.EvalJs()
    context.execute(
        """
    top = {location: {href: ''}};
    location = {href: ''};
    function setTimeout(f, t) {
        f();
    };
    """
    )

    # We grab Gohlke's code and evaluate it within py2js
    dl_function = re.search(r"function dl.*\}", soup.find("script").text).group(0)
    context.execute(dl_function)

    links = soup.find(class_="pylibs").find_all("a")
    for link in links:
        if link.get("onclick") is not None:
            # Evaluate the obfuscation javascript, store the result (squirreled away within location.href) into url
            regex_result = re.search(r"dl\(.*\)", link.get("onclick"))
            if regex_result is None:
                logger.info(u"Skip %s (wrong link format)" % unicode(link.string))
                continue
            context.execute(regex_result.group(0))
            url = context.location.href

            # Details = [package, version, pyversion, --, arch]
            details = url.split("/")[-1].split("-")
            pkg = details[0].lower().replace("_", "-")

            # Not using EXEs and ZIPs
            if len(details) != 5:
                logger.info(u"Skip %s (wrong name format)" % unicode(link.string))
                continue
            else:
                logger.debug(u"Add %s" % unicode(link.string))
            # arch = win32 / win_amd64 / any
            arch = details[4]
            arch = arch.split(".")[0]
            # ver = cpXX / pyX / pyXXx
            pkg_ver = details[1]
            py_ver = details[2]

            py_ver_key = py_ver + "-" + arch
            # print({py_ver_key: {pkg_ver: url}})
            if pkg in data.keys():
                if py_ver_key in data[pkg].keys():
                    data[pkg][py_ver_key].update({pkg_ver: url})
                else:
                    data[pkg][py_ver_key] = {pkg_ver: url}
            else:
                data[pkg] = {py_ver_key: {pkg_ver: url}}
        else:
            if link.string:
                logger.debug(u"Skip %s (missing link)" % unicode(link.string))
    return data


def filter_packages(data):
    """
    Filter packages based on your current system
    """

    sys_data = {}

    # Check lists
    verlist = []
    archlist = []
    ver = version_info[:2]
    verlist.append("cp" + str(ver[0]) + str(ver[1]))
    verlist.append("py" + str(ver[0]))
    verlist.append("py" + str(ver[0]) + str(ver[1]))
    verlist.append("py2.py3")

    archlist.append("any")
    if (struct.calcsize("P") * 8) == 32:
        archlist.append("win32")
    elif (struct.calcsize("P") * 8) == 64:
        archlist.append("win_amd64")

    checklist = list(map("-".join, list(product(verlist, archlist))))

    for key in data.keys():
        presence = list(map(lambda x: x in data[key].keys(), checklist))
        try:
            id = presence.index(True)
        except ValueError:
            # Version not found
            continue
        sys_data[key] = data[key][checklist[id]]

    return sys_data


class PipwinCache(object):
    """
    Pipwin cache class
    """

    def __init__(self, refresh=False):
        """
        Search if cache file is there in HOME.
        If not, build one.

        Parameters
        ----------
        refresh: boolean
            If True, rebuilds the cache.
        """

        home_dir = expanduser("~")
        self.cache_file = join(home_dir, ".pipwin")

        if isfile(self.cache_file) and not refresh:
            with open(self.cache_file) as fp:
                cache_dump = fp.read()
            self.data = json.loads(cache_dump)
        else:
            print("Building cache. Hang on . . .")
            self.data = build_cache()

            with open(self.cache_file, "w") as fp:
                fp.write(
                    json.dumps(
                        self.data, sort_keys=True, indent=4, separators=(",", ": ")
                    )
                )

            print("Done")

        if not refresh:
            # Create a package list for the system
            self.sys_data = filter_packages(self.data)

    def print_list(self):
        """
        Print the list of packages available for system
        """

        print("# Listing packages available for your system\n")
        for package in self.sys_data.keys():
            print(package)
        print("")

    def search(self, requirement):
        """
        Search for a package

        Returns
        -------
        exact_match : boolean
            True if exact match is found
        matches : list
            List of matches. Is a string if exact_match is True.
        """

        if requirement.name in self.sys_data.keys():
            return [True, requirement.name]

        # find all packages that contain our search term within them
        found = [pack for pack in self.sys_data.keys() if requirement.name in pack]
        return [False, found]

    def _get_url(self, requirement):
        versions = list(
            requirement.specifier.filter(self.sys_data[requirement.name].keys())
        )
        if not versions:
            raise ValueError("Could not satisfy requirement %s" % (str(requirement)))
        return self.sys_data[requirement.name][max(versions)]

    def _get_pipwin_dir(self):
        home_dir = expanduser("~")
        pipwin_dir = join(home_dir, "pipwin")
        if not exists(pipwin_dir):
            os.makedirs(pipwin_dir)
        return pipwin_dir

    def _download(self, requirement, dest):
        url = self._get_url(requirement)
        wheel_name = url.split("/")[-1]
        print("Downloading package . . .")
        print(url)
        print(wheel_name)

        if dest is not None:
            # Ensure the download directory
            if not exists(dest):
                os.makedirs(dest)
        else:
            dest = self._get_pipwin_dir()

        wheel_file = join(dest, wheel_name)

        if exists(wheel_file):
            print("File " + wheel_file + " already exists")
            return wheel_file

        obj = SmartDL(url, dest)
        obj.start()
        return wheel_file

    def download(self, requirement, dest=None):
        return self._download(requirement, dest)

    def install(self, requirement):
        """
        Install a package
        """
        wheel_file = self.download(requirement)
        subprocess.check_call([executable, "-m", "pip", "install", wheel_file])
        os.remove(wheel_file)

    def uninstall(self, requirement):
        """
        Uninstall a package
        """
        subprocess.check_call([executable, "-m", "pip", "uninstall", requirement.name])


def refresh():
    """
    Rebuild the cache
    """

    PipwinCache(refresh=True)


if __name__ == "__main__":
    refresh()
