import abc
import asyncio
import re
import sys
from dataclasses import dataclass
from functools import partial
from typing import List, TextIO, Optional

import httpx
from packaging import version

from wapiti_arsenic.connection import Connection, RemoteConnection
from wapiti_arsenic.errors import ArsenicError
from wapiti_arsenic.http import Auth, BasicAuth
from wapiti_arsenic.subprocess import get_subprocess_impl
from wapiti_arsenic.utils import free_port
from wapiti_arsenic.webdriver import WebDriver


async def tasked(coro):
    return await asyncio.get_event_loop().create_task(coro)


async def check_service_status(client: httpx.AsyncClient, url: str) -> bool:
    response = await client.get(url + "/status")
    return 200 <= response.status_code < 300


async def subprocess_based_service(
    cmd: List[str],
    service_url: str,
    log_file: TextIO,
    start_timeout: float = 15,
) -> WebDriver:
    closers = []
    try:
        impl = get_subprocess_impl()
        process = await impl.start_process(cmd, log_file)
        closers.append(partial(impl.stop_process, process))
        client = httpx.AsyncClient()
        closers.append(client.aclose)

        async def wait_service():
            # Wait for service with exponential back-off
            for i in range(-10, 9999):
                try:
                    ok = await tasked(check_service_status(client, service_url))
                except httpx.ConnectError:
                    # We possibly checked too quickly
                    ok = False
                if ok:
                    return
                await asyncio.sleep(start_timeout * 2**i)

        try:
            await asyncio.wait_for(wait_service(), timeout=start_timeout)
        except asyncio.TimeoutError:
            raise ArsenicError("not starting?")
        return WebDriver(Connection(client, service_url), closers)
    except:
        for closer in reversed(closers):
            await closer()
        raise


class Service(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    async def start(self) -> WebDriver:
        raise NotImplementedError()


@dataclass
class Geckodriver(Service):
    log_file: TextIO = sys.stdout
    binary: str = "geckodriver"
    version_check: bool = True
    start_timeout: float = 15

    _version_re = re.compile(r"geckodriver (\d+\.\d+)")

    async def _check_version(self):
        if self.version_check:
            impl = get_subprocess_impl()
            output = await impl.run_process([self.binary, "--version"])
            match = self._version_re.search(output)
            if not match:
                raise ValueError(
                    "Could not determine version of geckodriver. To "
                    "disable version checking, set `version_check` to "
                    "`False`."
                )
            binary_version_str = match.group(1)
            binary_version = version.parse(binary_version_str)
            if binary_version < version.parse("0.16.1"):
                raise ValueError(
                    f"Geckodriver version {binary_version_str} is too old. 0.16.1 or "
                    f"higher is required. To disable version checking, set "
                    f"`version_check` to `False`."
                )

    async def start(self):
        port = free_port()
        await self._check_version()
        return await subprocess_based_service(
            [self.binary, "--port", str(port)],
            f"http://localhost:{port}",
            self.log_file,
            start_timeout=self.start_timeout,
        )


@dataclass
class Chromedriver(Service):
    log_file: TextIO = sys.stdout
    binary: str = "chromedriver"
    start_timeout: float = 15

    async def start(self):
        port = free_port()
        return await subprocess_based_service(
            [self.binary, f"--port={port}"],
            f"http://localhost:{port}",
            self.log_file,
            start_timeout=self.start_timeout,
        )


@dataclass
class MSEdgeDriver(Service):
    log_file: TextIO = sys.stdout
    binary: str = "msedgedriver"
    start_timeout: float = 15

    async def start(self):
        port = free_port()
        return await subprocess_based_service(
            [self.binary, f"--port={port}"],
            f"http://localhost:{port}",
            self.log_file,
            start_timeout=self.start_timeout,
        )


def auth_or_string(value):
    if value is None:
        return value

    if isinstance(value, Auth):
        return value

    if isinstance(value, str) and value.count(":") == 1:
        username, password = value.split(":")
        return BasicAuth(username, password)

    raise TypeError()


@dataclass
class Remote(Service):
    url: str
    auth: Optional[Auth] = None

    def __post_init__(self):
        self.auth = auth_or_string(self.auth)

    async def start(self):
        closers = []
        headers = {}
        if self.auth:
            headers.update(self.auth.get_headers())
        try:
            client = httpx.AsyncClient(headers=headers)
            closers.append(client.aclose)
            return WebDriver(RemoteConnection(client, self.url), closers)
        except:
            for closer in reversed(closers):
                await closer()
            raise


@dataclass
class IEDriverServer(Service):
    log_file: TextIO = sys.stdout
    binary: str = "IEDriverServer.exe"
    log_level: str = "FATAL"
    start_timeout: float = 15

    async def start(self):
        port = free_port()
        return await subprocess_based_service(
            [self.binary, f"/port={port}", f"/log-level={self.log_level}"],
            f"http://localhost:{port}",
            self.log_file,
            start_timeout=self.start_timeout,
        )
