import logging
import os
import re

import chromedriver_autoinstaller
import geckodriver_autoinstaller
import urllib3
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.edge.options import Options as EdgeOptions
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.remote.command import Command

from cucu import config, edgedriver_autoinstaller, logger
from cucu.browser.core import Browser
from cucu.browser.frames import search_in_all_frames


class DisableLogger:
    def __enter__(self):
        logging.disable(logging.CRITICAL)

    def __exit__(self, exit_type, exit_value, exit_traceback):
        logging.disable(logging.NOTSET)


def init():
    """
    initialize any selenium specific needs
    """
    if config.CONFIG["CUCU_BROWSER"] == "chrome":
        try:
            with DisableLogger():
                # auto install chromedriver if not present
                chromedriver_autoinstaller.install()
        except:  # noqa: E722
            logging.debug("unable to auto install chromedriver")

    if config.CONFIG["CUCU_BROWSER"] == "firefox":
        # https://github.com/mozilla/geckodriver/issues/330
        logger.warning("browser console logs not available on firefox")
        geckodriver_autoinstaller.install()

    if config.CONFIG["CUCU_BROWSER"] == "edge":
        edgedriver_autoinstaller.install()


class Selenium(Browser):
    def __init__(self):
        self.driver = None

    def open(
        self, browser, headless=False, selenium_remote_url=None, detach=False
    ):
        if selenium_remote_url is None:
            init()

        timeout = float(config.CONFIG["CUCU_SELENIUM_DEFAULT_TIMEOUT_S"])
        os.environ["GLOBAL_DEFAULT_TIMEOUT"] = str(timeout)

        height = config.CONFIG["CUCU_BROWSER_WINDOW_HEIGHT"]
        width = config.CONFIG["CUCU_BROWSER_WINDOW_WIDTH"]
        cucu_downloads_dir = config.CONFIG["CUCU_BROWSER_DOWNLOADS_DIR"]
        ignore_ssl_certificate_errors = config.CONFIG[
            "CUCU_IGNORE_SSL_CERTIFICATE_ERRORS"
        ]

        if browser.startswith("chrome"):
            options = ChromeOptions()
            options.add_experimental_option("detach", detach)

            prefs = {"download.default_directory": cucu_downloads_dir}
            options.add_experimental_option("prefs", prefs)

            options.add_argument(f"--window-size={width},{height}")
            options.add_argument("--disable-dev-shm-usage")

            # Disable the automation message to get back some vertical space
            options.add_argument("--disable-infobars")  # old way
            options.add_experimental_option(
                "excludeSwitches", ["enable-automation"]
            )  # new way

            if headless:
                options.add_argument("--headless")

            if ignore_ssl_certificate_errors:
                options.add_argument("ignore-certificate-errors")
            options.set_capability("goog:loggingPrefs", {"browser": "ALL"})

            if selenium_remote_url is not None:
                logger.debug(f"webdriver.Remote init: {selenium_remote_url}")
                try:
                    self.driver = webdriver.Remote(
                        command_executor=selenium_remote_url,
                        options=options,
                    )
                except urllib3.exceptions.ReadTimeoutError:
                    print("*" * 80)
                    print(
                        "* unable to connect to the remote selenium setup,"
                        " you may need to restart it"
                    )
                    print("*" * 80)
                    print("")
                    raise
            else:
                logger.debug("webdriver.Chrome init")
                self.driver = webdriver.Chrome(
                    options=options,
                )

        elif browser.startswith("firefox"):
            options = FirefoxOptions()

            options.set_preference("browser.download.folderList", 2)
            options.set_preference(
                "browser.download.manager.showWhenStarting", False
            )
            options.set_preference("browser.download.dir", cucu_downloads_dir)
            options.set_preference(
                "browser.helperApps.neverAsk.saveToDisk",
                "images/jpeg, application/pdf, application/octet-stream, text/plain",
            )

            if ignore_ssl_certificate_errors:
                options.accept_insecure_certs = True

            options.add_argument(f"--width={width}")
            options.add_argument(f"--height={height}")

            # TODO: re-enable once Firefox supports this
            # options.set_capability("loggingPrefs", {"browser": "ALL"})

            if headless:
                options.add_argument("--headless")

            if selenium_remote_url is not None:
                logger.debug(f"webdriver.Remote init: {selenium_remote_url}")
                try:
                    self.driver = webdriver.Remote(
                        command_executor=selenium_remote_url,
                        options=options,
                    )
                except urllib3.exceptions.ReadTimeoutError:
                    print("*" * 80)
                    print(
                        "* unable to connect to the remote selenium setup,"
                        " you may need to restart it"
                    )
                    print("*" * 80)
                    print("")
                    raise
            else:
                logger.debug("webdriver.Firefox init")
                self.driver = webdriver.Firefox(
                    options=options,
                )

        elif browser.startswith("edge"):
            options = EdgeOptions()

            options.add_experimental_option(
                "prefs", {"download.default_directory": cucu_downloads_dir}
            )

            if headless:
                options.use_chromium = True
                options.add_argument("--headless")

            if ignore_ssl_certificate_errors:
                options.set_capability("acceptSslCerts", True)

            if selenium_remote_url is not None:
                logger.debug(f"webdriver.Remote init: {selenium_remote_url}")
                try:
                    self.driver = webdriver.Remote(
                        command_executor=selenium_remote_url,
                        options=options,
                    )
                except urllib3.exceptions.ReadTimeoutError:
                    print("*" * 80)
                    print(
                        "* unable to connect to the remote selenium setup,"
                        " you may need to restart it"
                    )
                    print("*" * 80)
                    print("")
                    raise
            else:
                logger.debug("webdriver.Edge init")
                edgedriver_filepath = (
                    edgedriver_autoinstaller.utils.download_edgedriver()
                )
                self.driver = webdriver.Edge(
                    executable_path=edgedriver_filepath,
                    options=options,
                )

        else:
            raise Exception(f"unknown browser {browser}")

        self.driver.set_window_size(width, height)
        session_id = self.get_session_id()
        logger.debug(f"cucu started Selenium session with ID: {session_id}")

    def get_session_id(self):
        if self.driver:
            return getattr(self.driver, "session_id", None)
        return None

    def get_log(self):
        if config.CONFIG["CUCU_BROWSER"] == "firefox":
            return []

        try:
            # local selenium
            return self.driver.get_log("browser")
        except AttributeError:
            # remote selemium
            # workaround from https://github.com/SeleniumHQ/selenium/issues/15772#issuecomment-3357920550
            return self.driver.execute(Command.GET_LOG, {"type": "browser"})[
                "value"
            ]

    def get_current_url(self):
        return self.driver.current_url

    def navigate(self, url):
        self.driver.get(url)
        self.wait_for_page_to_load()

    def switch_to_next_tab(self):
        window_handles = self.driver.window_handles
        window_handle = self.driver.current_window_handle
        window_handle_index = window_handles.index(window_handle)

        if window_handle_index == len(window_handles) - 1:
            raise RuntimeError("no next browser tab available")
        self.driver.switch_to.window(window_handles[window_handle_index + 1])

    def switch_to_previous_tab(self):
        window_handles = self.driver.window_handles
        window_handle = self.driver.current_window_handle
        window_handle_index = window_handles.index(window_handle)

        if window_handle_index == 0:
            raise RuntimeError("no previous browser tab available")
        self.driver.switch_to.window(window_handles[window_handle_index - 1])

    def switch_to_nth_tab(self, tab_number):
        print(f"tab_number, {tab_number}")
        window_handles = self.driver.window_handles
        total_tabs = len(window_handles)
        if tab_number > total_tabs:
            raise RuntimeError(f"no {tab_number} browser tab available")
        self.driver.switch_to.window(window_handles[tab_number])

    def switch_to_tab_that_matches_regex(self, title_pattern):
        window_handles = self.driver.window_handles

        for handle in window_handles:
            self.driver.switch_to.window(handle)
            if re.search(title_pattern, self.driver.title):
                return

        raise Exception(f"No tab title matches pattern: {title_pattern}")

    def get_tab_info(self):
        window_handles = self.driver.window_handles
        current_window = self.driver.current_window_handle
        window_handle_index = window_handles.index(current_window)

        return {
            "tab_count": len(window_handles),
            "index": window_handle_index,
            "title": self.driver.title,
            "url": self.driver.current_url,
        }

    def get_all_tabs_info(self):
        window_handles = self.driver.window_handles
        current_window = self.driver.current_window_handle
        tabs_info = []

        for handle in window_handles:
            self.driver.switch_to.window(handle)
            tabs_info.append(
                {
                    "title": self.driver.title,
                    "url": self.driver.current_url,
                }
            )

        # Switch back to the original window
        self.driver.switch_to.window(current_window)

        return tabs_info

    def back(self):
        self.driver.back()
        self.wait_for_page_to_load()

    def refresh(self):
        self.driver.refresh()
        self.wait_for_page_to_load()

    def title(self):
        return self.driver.title

    def css_find_elements(self, selector):
        def find_elements_in_frame():
            elements = self.driver.find_elements(By.CSS_SELECTOR, selector)

            def visible(element):
                return element.is_displayed()

            return list(filter(visible, elements))

        return search_in_all_frames(self, find_elements_in_frame)

    def execute(self, javascript, *args):
        return self.driver.execute_script(javascript, *args)

    def click(self, element):
        # if Firefox just do normal click
        if config.CONFIG["CUCU_BROWSER"] == "firefox":
            element.click()
        else:
            actions = ActionChains(self.driver)
            actions.move_to_element(element).click().perform()

        # let cucu's own wait for page to load checks run
        self.wait_for_page_to_load()

    def switch_to_default_frame(self):
        self.driver.switch_to.default_content()

    def switch_to_frame(self, frame):
        self.driver.switch_to.frame(frame)

    def screenshot(self, filepath):
        self.driver.get_screenshot_as_file(filepath)

    def close_window(self):
        window_handles = self.driver.window_handles
        window_handle = self.driver.current_window_handle
        window_handle_index = window_handles.index(window_handle)

        self.driver.close()
        self.driver.switch_to.window(window_handles[window_handle_index - 1])

    def download_mht(self, target_filepath):
        if self.driver is None:
            logger.warning(
                "No active browser; will not attempt to download .mht file."
            )
            return

        browser_name = self.driver.name.lower()
        if "chrome" not in browser_name:
            logger.warning(
                "The web driver is not using Chrome as a web browser"
                f", but {browser_name}. This browser does not support"
                "dowloading .mht files; will not attempt to download one."
            )
            return

        mht_data = None
        if self.driver._is_remote:
            cdp_url = f"{self.driver.command_executor._client_config.remote_server_addr}/session/{self.driver.session_id}/chromium/send_command_and_get_result"
            cdp_request_body = '{"cmd": "Page.captureSnapshot", "params": {}}'
            cdp_response = self.driver.command_executor._request(
                "POST", cdp_url, cdp_request_body
            )
            # Flaky: Adding try catch block to handle the situation where we do not get a dictionary object.
            # Could not reproduce this in local. So far we have seen this issue only when running on a remote web driver.
            try:
                mht_data = cdp_response.get("value").get("data")
            except Exception as e:
                logger.error(f'object "{cdp_response.get("value")}" : {e}')
        else:
            mht_response = self.driver.execute_cdp_cmd(
                "Page.captureSnapshot", {}
            )
            mht_data = mht_response.get("data")

        if mht_data is None:
            logger.warning(
                "Something unexpected has happened: fetched MHT data, but that data was empty. Not writing MHT file."
            )
            return

        with open(target_filepath, "w") as mht_file:
            mht_file.write(mht_data)

    def quit(self):
        self.driver.quit()
