## Note to editors and future/current contributors:  ##
# this file is very messy.                            #
# Any contributions to tidying it up would be greatly #
# appriciated.                                        #
#                                                     #
# - eek                                               #
# # # # # # # # # # # # # # # # # # # # # # # # # # # #

import typing

try:
    import requests
    RR = requests.Response
except ImportError:
    requests = None
    RR = None

try:
    import aiohttp
    CR = aiohttp.ClientResponse
except ImportError:
    aiohttp = None
    CR = None
import time
import asyncio

_FALLBACKS = [
    "https://mystb.in",
    "https://haste.unbelievaboat.com",
    "https://paste.pythondiscord.com",    
    "https://hastebin.com"
]
#_HASTE_URLS_FOR_REGEX = '|'.join(_FALLBACKS[8:]).replace(".", "\\.")
# _HASTE_URLS_RAW = "(https://|http://)?({})/(raw/)?(?P<key>.+)".format(_HASTE_URLS_FOR_REGEX)
# _HASTE_REGEX = re.compile(_HASTE_URLS_RAW)

# we don't need the above anymore - Will be removed in the future.


class ResponseError(Exception):
    """Generic class raised when contacting the server failed."""
    def __init__(self, response: typing.Union[RR,CR]):
        self.raw_response = response
        if isinstance(response, requests.Response):
            self.status = response.status_code
        else:
            self.status = response.status

class NoFallbacks(Exception):
    """Raised when no fallback could be contacted."""

class NoMoreRetries(NoFallbacks):
    """Raised when we ran out of attempts to post."""


def findFallBackSync(verbose: bool = False):
    """Tries to find a fallback URL, if haste.clicksminuteper.net isn't working."""
    if not requests:
        raise RuntimeError("You need to install requests to be able to use findFallBackSync.")
    with requests.Session() as session:
        n = 0
        for n, url in enumerate(_FALLBACKS, 1):
            if verbose:
                print(f"Trying service {n}/{len(_FALLBACKS)}",end="\r")
            try:
                response = session.post(url+"/documents", data="")
            except:
                if verbose:
                    print(f"Service {n}/{len(_FALLBACKS)} failed. trying again.", end="\r")
                continue
            if response.status_code != 200:
                continue
            else:
                if verbose:
                    print(f"{url} ({n}) worked. Using that.")
                break
        else:
            if verbose:
                print("No functional URLs could be found. Are you sure you're online?")
            raise NoFallbacks()
        return url

async def findFallBackAsync(verbose: bool = False):
    """Same as findFallBackSync, but just async."""
    if not aiohttp:
        raise RuntimeError("You need to install aiohttp to be able to use findFallBackAsync.")
    async with aiohttp.ClientSession() as session:
        for n, url in enumerate(_FALLBACKS, 1):
            if verbose:
                print(f"Trying service {n}/{len(_FALLBACKS)} (URL {url})",end="\r")
            try:
                async with session.post(url+"/documents", data="") as response:
                    if response.status != 200:
                        continue
                    else:
                        if verbose:
                            print(f"{url} ({n}) worked. Using that.")
                        return url
            except:
                if verbose:
                    print(f"Service {n}/{len(_FALLBACKS)} failed. trying again.", end="\r")
                continue
        else:
            if verbose:
                print("No functional URLs could be found. Are you sure you're online?")
            raise NoFallbacks()


def post(sync: bool = False, *, content: str, url: str = None, retry: int = 5, find_fallback: bool = True):
    """
    Creates a new haste.

    If :param:`sync` is ``False``, this function returns a coroutine that you need to await.

    Examples: ::

        # You can run this in ipython or `python3 -m asyncio`.
        from postbin import post
        sync_url = post(True, content="Hello")

        async def main():
            async_url = await post(False, content="Hello")
        await main()
    """
    if sync:
        return postSync(content, url=url, retry=retry, find_fallback_on_unavailable=find_fallback)
    else:
        return postAsync(content, url=url, retry=retry, find_fallback_on_unavailable=find_fallback)


# noinspection PyIncorrectDocstring
def postSync(content:  str, *, url: str = None, retry: int = 5, find_fallback_on_unavailable: bool = True,
             find_fallback_on_retry_runout: bool = False):
    """
    Creates a new haste

    :param content: Union[str, Iterable] - the content to post to hastebin. If this is a list, it will join with a newline.
    :keyword url: the custom URL to post to. Defaults to CMP Haste.
    :keyword retry: the number of times to retry. Pass `0` to disable
    :keyword find_fallback_on_unavailable: Whether or not to find a fallback or give up if the url fails to return.
    :keyword find_fallback_on_retry_runout: if True, instead of raising NoMoreRetries(), find a fallback instead.
    :raise RuntimeError: requests is not installed or you provided a retry value above 1000 (which would raise recursion error)
    :raise TypeError: Either the provided `content` was not string/iterable, or you disabled find_..._unavailable.
    :return: the returned URL
    """
    if not requests:
        raise RuntimeError("requests must be installed if you want to be able to run postSync.")
    if not isinstance(content, str):
        content = repr(content)
    url = url or _FALLBACKS[0]
    with requests.Session() as session:
        try:
            response = session.post(url+"/documents", data=content)
            if response.status_code == 503:
                return postSync(content, url=findFallBackSync(True), find_fallback_on_retry_runout=True)
            if response.status_code != 200:
                raise ResponseError(response)
            if response.headers.get("Content-Type", "").lower() != "application/json":
                return postSync(content, url=findFallBackSync(True), find_fallback_on_retry_runout=True)
            key = response.json()["key"]
        except (requests.ConnectionError, ConnectionError):
            print(url, "is unable. Finding a fallback...")
            return postSync(content, url=findFallBackSync(True), find_fallback_on_retry_runout=True)
        except Exception as e:
            if retry <= 0:
                if find_fallback_on_retry_runout:
                    print(url, "is unavailable. Finding a fallback...")
                    return postSync(content, url=findFallBackSync(True), find_fallback_on_retry_runout=True)
                raise NoMoreRetries()
            retry -= 1
            if find_fallback_on_unavailable:
                return postSync(content, url=url, retry=retry, find_fallback_on_unavailable=True,
                                find_fallback_on_retry_runout=True)
            raise TypeError("Unable to create a haste with the provided URL.")
    return url+"/"+key

async def postAsync(content: str, *, url: str = None, retry: int = 5, find_fallback_on_unavailable: bool = True,
                    find_fallback_on_retry_runout: bool = False):
    """The same as :func:postSync, but async."""
    if not aiohttp:
        raise RuntimeError("aiohttp must be installed if you want to be able to run postAsync.")
    if not isinstance(content, str):
        content = repr(content)
    url = url or _FALLBACKS[0]
    async with aiohttp.ClientSession() as session:
        try:
            async with session.post(url+"/documents", data=content) as response:
                if response.status == 503:
                    return await postAsync(content, url=await findFallBackAsync(True), find_fallback_on_retry_runout=True)
                if response.status != 200:
                    raise ResponseError(response)
                if response.headers.get("Content-Type", "").lower() != "application/json":
                    return await postAsync(content, url=await findFallBackAsync(True), find_fallback_on_retry_runout=True)
                key = (await response.json())["key"]
                return f"{url}/{key}"
        except aiohttp.ClientConnectionError:
            if find_fallback_on_unavailable:
                print(url, "is unavailable. Finding a fallback...")
                return await postAsync(content, url=await findFallBackAsync(True), find_fallback_on_retry_runout=True)
            raise TypeError("Unable to create a haste with the provided URL")
        except Exception as e:
            print(e)
            if retry <= 0:
                if find_fallback_on_retry_runout:
                    return await postAsync(content, url=await findFallBackAsync(True), find_fallback_on_retry_runout=True)
                raise NoMoreRetries()
            retry -= 1
            # return await postAsync(content, url=url, retry=retry, find_fallback_on_retry_runout=True)
            if find_fallback_on_unavailable:
                return await postAsync(content, url=url, retry=retry, find_fallback_on_unavailable=True,
                                find_fallback_on_retry_runout=True)
            raise TypeError("Unable to create a haste with the provided URL.")
    return url+"/"+key
