#   Copyright 2017-present <NAME>
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

from __future__ import annotations

import asyncio
import sys
import time
from contextlib import asynccontextmanager
from typing import (
    TYPE_CHECKING,
    AsyncContextManager,
    AsyncIterator,
    List,
    NamedTuple,
    Optional,
    Protocol,
    TypeVar,
)

import aiohttp
from cachetools import TTLCache
from tldextract import extract as tld_extract


class TooManyRedirects(Exception):
    pass


class RedirectLoop(Exception):
    pass


class UnsafeRedirect(Exception):
    def __init__(self, from_url: str, to_url: str):
        self.from_url = from_url
        self.to_url = to_url
        super().__init__("Not following redicrect across domains %s -> %s" % (from_url, to_url))


class UnhandledResponseCode(Exception):
    def __init__(self, code: int):
        self.code = code
        super().__init__("Unhandled Response code %d" % code)


class Result(NamedTuple):
    success: bool
    urls: List[str]
    data: Optional[bytes]
    error: Optional[Exception]


__version__ = "0.0.1a"

USER_AGENT_FORMAT = (
    "Rattler (https://github.com/unified-moderation-netowrk/rattler {0}) Python/{1[0]}.{1[1]} aiohttp/{2}"
)

HEADERS = {"User-Agent": USER_AGENT_FORMAT.format(__version__, sys.version_info, aiohttp.__version__)}

#: We shouldn't need to use these...
ALT_HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"}


async def _get_response(
    session: aiohttp.ClientSession, url: str, remaining_redirects: int = 3, seen_urls: Optional[List[str]] = None
) -> Result:

    # this can't be seen_urls, because pylance freaks out and won't narrow this to List[str]
    _seen_urls: List[str] = seen_urls if seen_urls is not None else []
    _seen_urls.append(url)
    try:
        if remaining_redirects < 1:
            raise TooManyRedirects()

        timeout = aiohttp.client.ClientTimeout(total=15)  # type: ignore # pylance doesn't understand attr.s
        async with session.get(url, allow_redirects=False, timeout=timeout, raise_for_status=True) as resp:
            if resp.status in (
                301,
                308,
                302,
                303,
                307,
            ):  # permanent redirects: (301, 308) may need different handling later?
                # We can safely use the same handling for each type of redicrect as we are repeating the same request type

                new_loc = resp.headers["Location"]

                if new_loc in _seen_urls:
                    raise RedirectLoop()

                # allow subdomain redirects and properly handle various TLDs
                before, after = tld_extract(url), tld_extract(new_loc)
                if (before.domain, before.suffix) != (after.domain, after.suffix):
                    raise UnsafeRedirect(url, new_loc)

                _seen_urls.append(new_loc)
                return await _get_response(session, new_loc, remaining_redirects - 1, seen_urls)

            elif resp.status == 200:
                data = await resp.read()
                return Result(True, _seen_urls, data, None)

            else:
                raise UnhandledResponseCode(resp.status)
    except Exception as exc:
        return Result(False, _seen_urls, None, exc)


T = TypeVar("T")


class RequestHandlerType(Protocol):
    @staticmethod
    async def get(url: str) -> bytes:
        ...


@asynccontextmanager
async def RequestHandler() -> AsyncIterator[RequestHandlerType]:
    # we're hiding away implementation details to be innaccesible via trivial means.

    async def _safe_get_response(session, cache, url: str) -> Result:

        cache.expire()

        result = cache.get(url)
        if not result:
            result = await _get_response(session, url)
            for url in result.urls:
                cache[url] = result

        return result

    async with aiohttp.ClientSession(headers=HEADERS) as session:

        cache = TTLCache(100000, 600)
        # we use a semaphore here rather than a custom connector
        # because I'm somewhat lazy and also want
        # to make a distinction on handling of redirects

        # This is a completely arbitrary number that can be changed at a later date
        sem = asyncio.Semaphore(7)

        class _RequestHandler:
            @staticmethod
            async def get(url: str) -> bytes:

                async with sem:
                    result = await _safe_get_response(session, cache, url)
                if result.success:
                    assert isinstance(result.data, bytes), "invariantly true via construction"
                    return result.data
                else:
                    assert isinstance(result.error, Exception), "invariantly true via construction"
                    raise result.error

        try:
            yield _RequestHandler()
        finally:
            cache.expire(time.monotonic() + 1000)

#   Copyright 2017-present <NAME>
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

from __future__ import annotations

import asyncio
import sys
import time
from contextlib import asynccontextmanager
from typing import (
    TYPE_CHECKING,
    AsyncContextManager,
    AsyncIterator,
    List,
    NamedTuple,
    Optional,
    Protocol,
    TypeVar,
)

import aiohttp
from cachetools import TTLCache
from tldextract import extract as tld_extract


class TooManyRedirects(Exception):
    pass


class RedirectLoop(Exception):
    pass


class UnsafeRedirect(Exception):
    def __init__(self, from_url: str, to_url: str):
        self.from_url = from_url
        self.to_url = to_url
        super().__init__("Not following redicrect across domains %s -> %s" % (from_url, to_url))


class UnhandledResponseCode(Exception):
    def __init__(self, code: int):
        self.code = code
        super().__init__("Unhandled Response code %d" % code)


class Result(NamedTuple):
    success: bool
    urls: List[str]
    data: Optional[bytes]
    error: Optional[Exception]


__version__ = "0.0.1a"

USER_AGENT_FORMAT = (
    "Rattler (https://github.com/unified-moderation-netowrk/rattler {0}) Python/{1[0]}.{1[1]} aiohttp/{2}"
)

HEADERS = {"User-Agent": USER_AGENT_FORMAT.format(__version__, sys.version_info, aiohttp.__version__)}

#: We shouldn't need to use these...
ALT_HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"}


async def _get_response(
    session: aiohttp.ClientSession, url: str, remaining_redirects: int = 3, seen_urls: Optional[List[str]] = None
) -> Result:

    # this can't be seen_urls, because pylance freaks out and won't narrow this to List[str]
    _seen_urls: List[str] = seen_urls if seen_urls is not None else []
    _seen_urls.append(url)
    try:
        if remaining_redirects < 1:
            raise TooManyRedirects()

        timeout = aiohttp.client.ClientTimeout(total=15)  # type: ignore # pylance doesn't understand attr.s
        async with session.get(url, allow_redirects=False, timeout=timeout, raise_for_status=True) as resp:
            if resp.status in (
                301,
                308,
                302,
                303,
                307,
            ):  # permanent redirects: (301, 308) may need different handling later?
                # We can safely use the same handling for each type of redicrect as we are repeating the same request type

                new_loc = resp.headers["Location"]

                if new_loc in _seen_urls:
                    raise RedirectLoop()

                # allow subdomain redirects and properly handle various TLDs
                before, after = tld_extract(url), tld_extract(new_loc)
                if (before.domain, before.suffix) != (after.domain, after.suffix):
                    raise UnsafeRedirect(url, new_loc)

                _seen_urls.append(new_loc)
                return await _get_response(session, new_loc, remaining_redirects - 1, seen_urls)

            elif resp.status == 200:
                data = await resp.read()
                return Result(True, _seen_urls, data, None)

            else:
                raise UnhandledResponseCode(resp.status)
    except Exception as exc:
        return Result(False, _seen_urls, None, exc)


T = TypeVar("T")


class RequestHandlerType(Protocol):
    @staticmethod
    async def get(url: str) -> bytes:
        ...


@asynccontextmanager
async def RequestHandler() -> AsyncIterator[RequestHandlerType]:
    # we're hiding away implementation details to be innaccesible via trivial means.

    async def _safe_get_response(session, cache, url: str) -> Result:

        cache.expire()

        result = cache.get(url)
        if not result:
            result = await _get_response(session, url)
            for url in result.urls:
                cache[url] = result

        return result

    async with aiohttp.ClientSession(headers=HEADERS) as session:

        cache = TTLCache(100000, 600)
        # we use a semaphore here rather than a custom connector
        # because I'm somewhat lazy and also want
        # to make a distinction on handling of redirects

        # This is a completely arbitrary number that can be changed at a later date
        sem = asyncio.Semaphore(7)

        class _RequestHandler:
            @staticmethod
            async def get(url: str) -> bytes:

                async with sem:
                    result = await _safe_get_response(session, cache, url)
                if result.success:
                    assert isinstance(result.data, bytes), "invariantly true via construction"
                    return result.data
                else:
                    assert isinstance(result.error, Exception), "invariantly true via construction"
                    raise result.error

        try:
            yield _RequestHandler()
        finally:
            cache.expire(time.monotonic() + 1000)

#   Copyright 2017-present <NAME> # #   Licensed under the Apache License, Version 2.0 (the "License"); #   you may not use this file except in compliance with the License. #   You may obtain a copy of the License at # #       http://www.apache.org/licenses/LICENSE-2.0 # #   Unless required by applicable law or agreed to in writing, software #   distributed under the License is distributed on an "AS IS" BASIS, #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #   See the License for the specific language governing permissions and #   limitations under the License. #: We shouldn't need to use these... # this can't be seen_urls, because pylance freaks out and won't narrow this to List[str] # type: ignore # pylance doesn't understand attr.s # permanent redirects: (301, 308) may need different handling later? # We can safely use the same handling for each type of redicrect as we are repeating the same request type # allow subdomain redirects and properly handle various TLDs # we're hiding away implementation details to be innaccesible via trivial means. # we use a semaphore here rather than a custom connector # because I'm somewhat lazy and also want # to make a distinction on handling of redirects # This is a completely arbitrary number that can be changed at a later date