import os
import platform as pl
import struct
import subprocess
import xml.etree.ElementTree as ElTree
import zipfile
from urllib.request import urlopen

import requests
from requests.exceptions import HTTPError
from requests.exceptions import RequestException

from get_chrome_driver import downloader, constants
from get_chrome_driver.enums import Platform, Phase, OsPlatform
from get_chrome_driver.exceptions import (
    GetChromeDriverError,
    UnknownPlatformError,
    UnknownVersionError,
    DownloadError,
    VersionError,
    VersionUrlError,
)


class GetChromeDriver:
    def __init__(self, os_platform: OsPlatform = None):
        self.__os_platforms_list = [os_platform for os_platform in OsPlatform]

        if not os_platform:
            if pl.system() == "Windows":
                self.__os_platform = OsPlatform.win
            elif pl.system() == "Linux":
                self.__os_platform = OsPlatform.linux
            elif pl.system() == "Darwin":
                self.__os_platform = OsPlatform.mac
        else:
            if self.__check_if_os_platform_is_valid(os_platform):
                self.__os_platform = os_platform
            else:
                raise UnknownPlatformError(
                    f"Unknown platform, choose a platform from: {str(self.__os_platforms_list)}"
                )

        self.__arch_current = struct.calcsize("P") * 8
        self.__arch_64 = 64

        self.__chromedriver_str = "chromedriver"
        self.__zip_ext = ".zip"

    def stable_version(self) -> str:
        """Return the latest stable version"""

        return self.__latest_version_by_phase(Phase.stable)

    def beta_version(self) -> str:
        """Return the latest beta version"""

        return self.__latest_version_by_phase(Phase.beta)

    def __latest_version_by_phase(self, phase: Phase) -> str:
        """
        Return the latest stable or latest beta version

        :param phase: Stable or beta
        """

        response = requests.get(constants.LAST_KNOWN_GOOD_VERSIONS_URL)
        if not response.ok:
            raise GetChromeDriverError(
                f"Could not get {constants.LAST_KNOWN_GOOD_VERSIONS_URL}"
            )

        try:
            if phase == Phase.stable:
                return response.json()["channels"]["Stable"]["version"]
            if phase == Phase.beta:
                return response.json()["channels"]["Beta"]["version"]
        except KeyError:
            raise UnknownVersionError("Could not find version")

        raise UnknownVersionError("Could not find version")

    def stable_version_url(self) -> str:
        """Return the latest stable version url"""

        return self.version_url(self.__latest_version_by_phase(Phase.stable))

    def beta_version_url(self) -> str:
        """Return the latest beta version url"""

        return self.version_url(self.__latest_version_by_phase(Phase.beta))

    def __version_url_for_platform(
        self,
        new_api_known_good_versions: list,
        version: str,
        platform_64: str,
        platform_32: str = None,
        is_mac: bool = False,
    ) -> str:
        """
        Return the version download url for a platform

        :param new_api_known_good_versions: The latest known good driver versions from the new api
        :param version: Chromedriver version
        :param platform_64: 64bit platform
        :param platform_32: 32bit platform
        """

        url = None

        # New api
        for driver_version in new_api_known_good_versions:
            if driver_version.get("version") == version:
                drivers = driver_version.get("downloads").get("chromedriver")

                # 64
                for driver in drivers or []:
                    if is_mac:
                        _platform_64 = Platform.mac_x64.value
                    else:
                        _platform_64 = platform_64

                    if driver.get("platform") == _platform_64:
                        url = driver.get("url")

                # 32
                if url is None and platform_32 and not is_mac:
                    for driver in drivers or []:
                        if driver.get("platform") == platform_32:
                            url = driver.get("url")

                if url is not None:
                    if self.__check_if_url_is_valid(url):
                        return url

        # Old chromedriver storage
        # 64
        url = f"{constants.CHROMEDRIVER_STORAGE_URL}/{version}/{self.__chromedriver_str}_{platform_64}{self.__zip_ext}"
        if self.__check_if_url_is_valid(url):
            return url

        # 32
        if platform_32:
            url = f"{constants.CHROMEDRIVER_STORAGE_URL}/{version}/{self.__chromedriver_str}_{platform_32}{self.__zip_ext}"
            if self.__check_if_url_is_valid(url):
                return url

        raise VersionUrlError(f"Could not find download url for version {version}")

    def version_url(self, version: str) -> str:
        """
        Return the version download url

        :param version: Chromedriver version
        """

        if not self.__check_if_version_format_is_valid(version):
            raise UnknownVersionError("Invalid version format")

        # Get driver urls from the new api
        response = requests.get(constants.KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS_URL)
        if not response.ok:
            raise GetChromeDriverError(
                f"Could not get {constants.KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS_URL}"
            )
        new_api_known_good_versions = response.json()["versions"]

        if self.__os_platform == OsPlatform.win:
            url = self.__version_url_for_platform(
                new_api_known_good_versions=new_api_known_good_versions,
                version=version,
                platform_32=Platform.win32.value,
                platform_64=Platform.win64.value,
            )

            return url

        elif self.__os_platform == OsPlatform.linux:
            url = self.__version_url_for_platform(
                new_api_known_good_versions=new_api_known_good_versions,
                version=version,
                platform_32=Platform.linux32.value,
                platform_64=Platform.linux64.value,
            )

            return url

        elif self.__os_platform == OsPlatform.mac:
            url = self.__version_url_for_platform(
                new_api_known_good_versions=new_api_known_good_versions,
                version=version,
                platform_64=Platform.mac64.value,
                is_mac=True,
            )

            return url

    def download_stable_version(self, output_path: str = None, extract: bool = False):
        """
        Download the latest stable chromedriver version

        :param output_path: Path to download the driver to
        :param extract: Extract the downloaded driver or not
        """

        version = self.__latest_version_by_phase(Phase.stable)
        self.download_version(version=version, output_path=output_path, extract=extract)

    def download_beta_version(self, output_path: str = None, extract: bool = False):
        """
        Download the latest beta chromedriver version

        :param output_path: Path to download the driver to
        :param extract: Extract the downloaded driver or not
        """

        version = self.__latest_version_by_phase(Phase.beta)
        self.download_version(version=version, output_path=output_path, extract=extract)

    def download_version(
        self, version, output_path: str = None, extract: bool = False
    ) -> str:
        """
        Download a chromedriver version

        :param version: Chromedriver version
        :param output_path: Path to download the driver to
        :param extract: Extract the downloaded driver or not
        """

        if not self.__check_if_version_format_is_valid(version):
            raise UnknownVersionError("Invalid version format")

        if not output_path:
            # On path is None, the driver will be downloaded at e.g. chromedriver/88.0.4324.96/bin/chromedriver.exe
            output_path = self._output_path(version)

        # e.g. if path == 'webdriver/bin', the driver will be downloaded at 'webdriver/bin/chromedriver.exe'
        def download(download_url: str, download_output_path: str) -> str:
            try:
                file_path, file_name = downloader.download(
                    url=download_url, output_path=download_output_path
                )
            except (OSError, HTTPError, RequestException) as err:
                raise DownloadError(err)
            if extract:
                with zipfile.ZipFile(file_path, "r") as zip_ref:
                    zip_ref.extractall(path=download_output_path)
                os.remove(file_path)
                if (
                    self.__os_platform == OsPlatform.linux
                    or self.__os_platform == OsPlatform.mac
                ):
                    os.chmod(f"{download_output_path}/chromedriver", 0o755)
            return download_output_path

        url = self.version_url(version)

        # Download the driver file and return the path of the driver file
        return download(download_url=url, download_output_path=output_path)

    def __check_if_url_is_valid(self, url: str) -> bool:
        """
        Check if url is valid

        :param url: The driver download url
        """

        if requests.head(url).status_code != 200:
            return False

        return True

    def __check_if_version_format_is_valid(self, version: str) -> bool:
        """
        Check if version format is valid

        :param version: Chromedriver version
        """

        split_version = version.split(".")
        for number in split_version:
            if not number.isnumeric():
                return False

        return True

    def __check_if_os_platform_is_valid(self, os_platform: OsPlatform) -> bool:
        """
        Check if platform is valid

        :param os_platform: OS
        """

        if os_platform not in self.__os_platforms_list:
            return False

        return True

    def matching_version(self) -> str:
        """Return a matching ChromeDriver version"""

        all_chromedriver_versions = self.__get_all_chromedriver_versions()
        installed_chrome_version = self.__get_installed_chrome_version()
        for chromedriver_version in reversed(all_chromedriver_versions):
            if ".".join(installed_chrome_version.split(".")[:-1]) == ".".join(
                chromedriver_version.split(".")[:-1]
            ):
                return chromedriver_version

    def auto_download(self, output_path: str = None, extract: bool = False) -> str:
        """
        Download ChromeDriver for the installed Chrome version on machine

        :param output_path: Path to download the driver to
        :param extract: Extract the downloaded driver or not
        """

        version = self.matching_version()
        if version == "" or version is None:
            raise VersionError(
                "Unable to find a ChromeDriver version for the installed Chrome version"
            )

        return self.download_version(version, output_path, extract)

    def install(self):
        """Install ChromeDriver for the installed Chrome version on machine"""

        output_path = self.auto_download(extract=True)
        path = os.path.join(os.path.abspath(os.getcwd()), output_path)
        os.environ["PATH"] += os.pathsep + path

    def __get_all_chromedriver_versions(self) -> list:
        """Return a list with all ChromeDriver versions"""

        key_texts = []
        versions = []

        with urlopen(constants.CHROMEDRIVER_STORAGE_URL) as xml_file:
            tree = ElTree.parse(xml_file)
            root = tree.getroot()

            for root_item in root:
                # Remove namespace
                root_item.tag = root_item.tag.split("}", 1)[1]

            for content in root.findall("Contents"):
                for content_item in content:
                    # Remove namespace
                    content_item.tag = content_item.tag.split("}", 1)[1]

                    key_texts.append(content.find("Key").text)

        for text in key_texts:
            version = ""
            for char in text:
                if char.isnumeric() or char == ".":
                    version += char
                else:
                    break

            if len(version) < 1:
                continue

            versions.append(version)

        return list(dict.fromkeys(versions))

    def __get_installed_chrome_version(self) -> str:
        """Return the installed Chrome version on the machine"""

        if self.__os_platform == OsPlatform.win:
            process = subprocess.Popen(
                [
                    "reg",
                    "query",
                    "HKEY_CURRENT_USER\\SOFTWARE\\Google\\Chrome\\BLBeacon",
                    "/v",
                    "version",
                ],
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                stdin=subprocess.DEVNULL,
            )

            return process.communicate()[0].decode("UTF-8").split()[-1]

        elif self.__os_platform == OsPlatform.linux:
            process = subprocess.Popen(
                ["google-chrome", "--version"],
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                stdin=subprocess.DEVNULL,
            )
            return process.communicate()[0].decode("UTF-8").split()[-1]

        elif self.__os_platform == OsPlatform.mac:
            process = subprocess.Popen(
                [
                    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
                    "--version",
                ],
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                stdin=subprocess.DEVNULL,
            )

            return process.communicate()[0].decode("UTF-8").split()[-1]

    def _output_path(self, version: str) -> str:
        """
        Return the output path

        :param version: Chromedriver version
        """

        return f"chromedriver/{version}/bin"
