# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import os
import pathlib
import re
import signal
import tempfile
import threading

from mozlog import get_proxy_logger
from mozprocess import ProcessHandler

from mozperftest.layers import Layer
from mozperftest.utils import (
    ON_TRY,
    download_file,
    get_adb_device_or_emu,
    get_output_dir,
    get_pretty_app_name,
    install_package,
)

LOG = get_proxy_logger(component="proxy")
HERE = os.path.dirname(__file__)


class OutputHandler:
    def __init__(self):
        self.proc = None
        self.port = None
        self.port_event = threading.Event()

    def __call__(self, line):
        line = line.strip()
        if not line:
            return
        line = line.decode("utf-8", errors="replace")
        try:
            data = json.loads(line)
        except ValueError:
            self.process_output(line)
            return

        if isinstance(data, dict) and "action" in data:
            # Retrieve the port number for the proxy server from the logs of
            # our subprocess.
            m = re.match(r"Proxy running on port (\d+)", data.get("message", ""))
            if m:
                self.port = int(m.group(1))
                self.port_event.set()
            LOG.log_raw(data)
        else:
            self.process_output(json.dumps(data))

    def finished(self):
        self.port_event.set()

    def process_output(self, line):
        if self.proc is None:
            LOG.process_output(line)
        else:
            LOG.process_output(self.proc.pid, line)

    def wait_for_port(self):
        self.port_event.wait()
        return self.port


class ProxyRunner(Layer):
    """Use a proxy"""

    name = "proxy"
    activated = False

    arguments = {
        "mode": {
            "type": str,
            "choices": ["record", "playback"],
            "help": "Proxy server mode. Use `playback` to replay from the provided file(s). "
            "Use `record` to generate a new recording at the path specified by `--file`. "
            "playback - replay from provided file. "
            "record - generate a new recording at the specified path.",
        },
        "file": {
            "type": str,
            "nargs": "+",
            "help": "The playback files to replay, or the file that a recording will be saved to. "
            "For playback, it can be any combination of the following: zip file, manifest file, "
            "or a URL to zip/manifest file. "
            "For recording, it's a zip fle.",
        },
        "perftest-page": {
            "type": str,
            "default": None,
            "help": "This option can be used to specify a single test to record rather than "
            "having to continuously modify the pageload_sites.json. This flag should only be "
            "used by the perftest team and selects items from "
            "`testing/performance/pageload_sites.json` based on the name field. Note that "
            "the login fields won't be checked with a request such as this (i.e. it overrides "
            "those settings).",
        },
        "deterministic": {
            "action": "store_true",
            "default": False,
            "help": "If set, the deterministic JS script will be injected into the pages.",
        },
    }

    def __init__(self, env, mach_cmd):
        super().__init__(env, mach_cmd)
        self.proxy = None
        self.tmpdir = None

    def setup(self):
        try:
            import mozproxy  # noqa: F401
        except ImportError:
            # Install mozproxy and its vendored deps.
            mozbase = pathlib.Path(self.mach_cmd.topsrcdir, "testing", "mozbase")
            mozproxy_deps = ["mozinfo", "mozlog", "mozproxy"]
            for i in mozproxy_deps:
                install_package(
                    self.mach_cmd.virtualenv_manager, pathlib.Path(mozbase, i)
                )

        # set MOZ_HOST_BIN to find cerutil. Required to set certifcates on android
        os.environ["MOZ_HOST_BIN"] = self.mach_cmd.bindir

    def run(self, metadata):
        self.metadata = metadata
        replay_file = self.get_arg("file")

        # Check if we have a replay file
        if replay_file is None:
            raise ValueError("Proxy file not provided!!")

        if replay_file is not None and replay_file.startswith("http"):
            self.tmpdir = tempfile.TemporaryDirectory()
            target = pathlib.Path(self.tmpdir.name, "recording.zip")
            self.info(f"Downloading {replay_file}")
            download_file(replay_file, target)
            replay_file = target

        self.info("Setting up the proxy")

        command = [
            self.mach_cmd.virtualenv_manager.python_path,
            "-m",
            "mozproxy.driver",
            "--topsrcdir=" + self.mach_cmd.topsrcdir,
            "--objdir=" + self.mach_cmd.topobjdir,
            "--profiledir=" + self.get_arg("profile-directory"),
        ]

        if not ON_TRY:
            command.extend(["--local"])

        if metadata.flavor == "mobile-browser":
            command.extend(["--tool=mitmproxy-android"])
            command.extend(["--binary=android"])
            command.extend([
                f"--app={get_pretty_app_name(self.get_arg('android-app-name'))}"
            ])
        else:
            command.extend(["--tool=mitmproxy"])
            # XXX See bug 1712337, we need a single point where we can get the binary used from
            # this is required to make it work localy
            binary = self.get_arg("browsertime-binary")
            if binary is None:
                binary = self.mach_cmd.get_binary_path()
            command.extend([f"--binary={binary}"])

        if self.get_arg("mode") == "record":
            output = self.get_arg("output")
            if output is None:
                output = pathlib.Path(self.mach_cmd.topsrcdir, "artifacts")
            results_dir = get_output_dir(output)

            command.extend(["--mode", "record"])
            command.append(str(pathlib.Path(results_dir, replay_file)))
        elif self.get_arg("mode") == "playback":
            command.extend(["--mode", "playback"])
            command.append(str(replay_file))
        else:
            raise ValueError("Proxy mode not provided please provide proxy mode")

        inject_deterministic = self.get_arg("deterministic")
        if inject_deterministic:
            command.extend(["--deterministic"])

        print(" ".join(command))
        self.output_handler = OutputHandler()
        self.proxy = ProcessHandler(
            command,
            processOutputLine=self.output_handler,
            onFinish=self.output_handler.finished,
        )
        self.output_handler.proc = self.proxy
        self.proxy.run()

        # Wait until we've retrieved the proxy server's port number so we can
        # configure the browser properly.
        port = self.output_handler.wait_for_port()
        if port is None:
            raise ValueError("Unable to retrieve the port number from mozproxy")
        self.info(f"Received port number {port} from mozproxy")

        prefs = {
            "network.proxy.type": 1,
            "network.proxy.http": "127.0.0.1",
            "network.proxy.http_port": port,
            "network.proxy.ssl": "127.0.0.1",
            "network.proxy.ssl_port": port,
            "network.proxy.no_proxies_on": "127.0.0.1",
        }
        browser_prefs = metadata.get_options("browser_prefs")
        browser_prefs.update(prefs)

        if metadata.flavor == "mobile-browser":
            self.info("Setting reverse port fw for android device")
            device = get_adb_device_or_emu()
            device.create_socket_connection("reverse", f"tcp:{port}", f"tcp:{port}")

        return metadata

    def teardown(self):
        err = None
        if self.proxy is not None:
            returncode = self.proxy.wait(0)
            if returncode is None:
                # Process still running, signal it and wait for exit
                kill_signal = getattr(signal, "CTRL_BREAK_EVENT", signal.SIGINT)
                os.kill(self.proxy.pid, kill_signal)
                self.proxy.wait()
            elif returncode != 0:
                err = ValueError(
                    f"mozproxy terminated early with return code {returncode}"
                )
            self.proxy = None
        if self.tmpdir is not None:
            self.tmpdir.cleanup()
            self.tmpdir = None

        if err:
            raise err
