from typing import List, Optional, Set

import asyncio
import configparser
import math
import operator
from pathlib import Path

import aiohttp
from rich.console import Console
from rich.table import Table
from user_agent import generate_user_agent

from rdf_linkchecker.checkers import CONFIG_DEFAULTS

_SESSION_HEADER = {
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
    "Sec-Fetch-Site": "same-origin",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-User": "?1",
    "Sec-Fetch-Dest": "document",
    "Referer": "https://www.google.com/",
    "Accept-Encoding": "gzip, deflate, br",
    "Accept-Language": "en-US,en;q=0.9",
    "User-Agent": generate_user_agent(),
}


class Checker:
    """`requests` based link checker"""

    def __init__(self, configfile: Optional[Path] = None):
        self.config = configparser.ConfigParser()
        self.config.read_dict(CONFIG_DEFAULTS)
        if configfile:
            self.config.read(filenames=[configfile])
        self.urls: Set[str] = set()
        try:
            self.skip_domains = self.config["skip"]["domains"].split(",")
        except AttributeError:
            self.skip_domains = []
        self._session = None

    def _accept_url(self, url):
        for skip in self.skip_domains:
            if skip in url:
                return False
        return True

    def add_urls(self, urls: List[str]) -> None:
        upd = {u for u in urls if self._accept_url(u)}
        self.urls.update(upd)

    async def _check_single(self, url: str, session: aiohttp.ClientSession) -> bool:
        """Return True if resource request succeeded, else False

        Uses the streaming version to cut down on dl size/time
        """
        con = self.config["connection"]

        async def _check():
            try:
                timeout = aiohttp.ClientTimeout(total=int(con["timeout"]))
                async with session.get(url, timeout=timeout) as response:
                    response.raise_for_status()
                    return None
            except (aiohttp.ClientError, asyncio.TimeoutError) as e:
                return e

        sleep_ms = float(con["sleep"]) * 1000
        sleep_fac = 2
        retries = min(0, int(con["retries"]))
        for try_no in range(retries + 1):
            err = await _check()
            if err is None:
                return None
            await asyncio.sleep(
                math.ceil(sleep_ms * math.pow(sleep_fac, try_no) / 1000)
            )
        return err

    async def _check(self):
        if len(self.urls) == 0:
            # we cannot await an empty tasks list
            tasks = []
        else:
            async with aiohttp.ClientSession(headers=_SESSION_HEADER) as session:
                tasks = [
                    asyncio.ensure_future(self._check_single(u, session=session))
                    for u in self.urls
                ]
                await asyncio.wait(tasks)
        self.results = [t.result() for t in tasks]

    def check(self) -> bool:
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._check())
        if self.config["reporting"]["level"] != "none":
            self.report_results(self.results)
        return all(r is None for r in self.results)

    def report_results(self, results):
        rptg = self.config["reporting"]
        only_failed = rptg["level"] == "only-failed"

        title = "Failed URLs" if only_failed else "Checked URLs"
        table = Table("URL", "Ok?", "Reason", title=title)
        for url, error in sorted(zip(self.urls, results), key=operator.itemgetter(0)):
            if rptg["level"] == "all" or (only_failed and error is None):
                if error is None:
                    table.add_row(url, "[green]✓[/green]", "")
                else:
                    table.add_row(url, "[red]x[/red]", str(error))

        def _print(_console):
            if table.row_count:
                _console.print(table)

        if rptg["target"] != "console":
            with open(rptg["target"], "wt") as report_file:
                console = Console(file=report_file)
                _print(console)
        else:
            console = Console()
            _print(console)
