import asyncio
from asyncio import Lock
from enum import Enum
from logging import getLogger
from typing import (
    Any,
    List,
    Mapping,
    NamedTuple,
    Optional,
    TYPE_CHECKING,
)

from aiohttp import (
    ClientSession,
    ClientTimeout,
    TCPConnector,
)
from aiohttp.typedefs import (
    LooseHeaders,
    StrOrURL,
)
from aiohttp_socks import ProxyConnector
from requests import Session
from typing_extensions import (
    Self,
)
from yarl import URL

from async_pixiv.utils.func import proxies_from_env
from async_pixiv.utils.typed import RequestMethod

try:
    import regex as re
except ImportError:
    import re

try:
    import ujson as json
except ImportError:
    import json

try:
    from uvloop import EventLoopPolicy

    asyncio.set_event_loop_policy(EventLoopPolicy())
except ImportError:
    EventLoopPolicy = None

if TYPE_CHECKING:
    from aiohttp import ClientResponse

__all__ = ["Net"]

logger = getLogger(__name__)


class Config(NamedTuple):
    client_id: str
    client_secret: str
    user_agent: str


class Net(object):
    __slots__ = (
        "_conn_kwargs",
        "_timeout",
        "_session",
        "_proxies",
        "_trust_env",
        "_retry",
        "_retry_sleep",
        "_sync_session",
    )

    _limit: int
    _timeout: ClientTimeout
    _trust_env: bool
    _proxies: Optional[List[URL]]
    _session: Optional[ClientSession]

    _aio_lock: Lock = Lock()

    async def _init_session(self) -> ClientSession:
        async with self._aio_lock:
            if self._session is None or self._session.closed:
                proxy: Optional[URL] = None

                for p in self._proxies:
                    if p.scheme in ["socks5", "socks4", "http", "ws"]:
                        proxy = p
                        break

                if proxy is not None and proxy.scheme in ["socks5", "socks4", "http"]:
                    connector = ProxyConnector.from_url(str(proxy))
                else:
                    connector = TCPConnector(**self._conn_kwargs)
                self._session = ClientSession(
                    connector=connector,
                    timeout=self._timeout,
                    json_serialize=json.dumps,
                )

        return self._session

    def __init__(
        self,
        *,
        limit: int = 30,
        timeout: float = 10,
        proxy: Optional[StrOrURL] = None,
        trust_env: bool = False,
        retry: int = 5,
        retry_sleep: float = 1,
    ) -> None:
        self._session = None
        self._conn_kwargs = {"limit": limit}
        self._timeout = ClientTimeout(timeout)

        self._proxies = [URL(str(proxy))] if proxy is not None else []
        if trust_env:
            self._proxies.extend(proxies_from_env())
        self._proxies = list(set(self._proxies))
        self._retry = retry
        self._retry_sleep = retry_sleep
        self._sync_session = Session()

    async def __aenter__(self) -> Self:
        return self.start()

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        await self._session.close()

    def __del__(self) -> None:
        if self._session and not self._session.closed:
            try:
                loop = asyncio.get_running_loop()
            except RuntimeError:
                loop = asyncio.new_event_loop()

            loop.run_until_complete(self._session.close())

    async def start(self) -> Self:
        await self._init_session()
        return self

    async def close(self) -> None:
        async with self._aio_lock:
            if self._session and not self._session.closed:
                await self._session.close()

    async def _request(
        self,
        method: RequestMethod,
        url: StrOrURL,
        *,
        params: Optional[Mapping[str, Any]] = None,
        headers: Optional[LooseHeaders] = None,
        data: Any = None,
    ) -> "ClientResponse":
        proxy = None
        for p in self._proxies:
            if p.scheme in ["http", "ws"]:
                proxy = p
                break
        param = dict()
        for key, value in (params or {}).items():
            if value in [None, {}, []]:
                continue
            if isinstance(value, Enum):
                value = value.value
            if isinstance(value, (str, int, float)):
                param.update({key: value})
            if isinstance(value, list):
                if key in ["sizes", "types"]:
                    param.update({key: ",".join(map(str, value))})
                elif key in ["ids"]:
                    param.update({f"{key}[]": ",".join([str(pid) for pid in value])})
                elif key in ["viewed"]:
                    [param.update({f"{key}[{k}]": value[k]}) for k in range(len(value))]
                else:
                    param.update({f"{key}[]": ",".join(map(str, value))})
            if isinstance(value, bool):
                param.update({key: str(value).lower()})
        for time in range(self._retry):
            try:
                return await (await self._init_session()).request(
                    method,
                    url,
                    params=param,
                    headers=headers,
                    data=data,
                    proxy=proxy,
                    allow_redirects=False,
                )
            except Exception as e:
                if time != self._retry - 1:
                    await asyncio.sleep(self._retry_sleep)
                    logger.debug(f"Encountered an error: \n{e}")
                    continue
                raise e

    async def get(
        self,
        url: StrOrURL,
        *,
        params: Optional[Mapping[str, Any]] = None,
        headers: Optional[LooseHeaders] = None,
        data: Any = None,
    ) -> "ClientResponse":
        return await self._request(
            "GET", url, params=params, headers=headers, data=data
        )

    async def post(
        self,
        url: StrOrURL,
        *,
        params: Optional[Mapping[str, Any]] = None,
        headers: Optional[LooseHeaders] = None,
        data: Any = None,
    ) -> "ClientResponse":
        return await self._request(
            "POST", url, params=params, headers=headers, data=data
        )

    async def head(
        self,
        url: StrOrURL,
        *,
        params: Optional[Mapping[str, Any]] = None,
        headers: Optional[LooseHeaders] = None,
        data: Any = None,
    ) -> "ClientResponse":
        return await self._request(
            "HEAD", url, params=params, headers=headers, data=data
        )

    async def put(
        self,
        url: StrOrURL,
        *,
        params: Optional[Mapping[str, Any]] = None,
        headers: Optional[LooseHeaders] = None,
        data: Any = None,
    ) -> "ClientResponse":
        return await self._request(
            "PUT", url, params=params, headers=headers, data=data
        )

    async def patch(
        self,
        url: StrOrURL,
        *,
        params: Optional[Mapping[str, Any]] = None,
        headers: Optional[LooseHeaders] = None,
        data: Any = None,
    ) -> "ClientResponse":
        return await self._request(
            "PATCH", url, params=params, headers=headers, data=data
        )

    async def delete(
        self,
        url: StrOrURL,
        *,
        params: Optional[Mapping[str, Any]] = None,
        headers: Optional[LooseHeaders] = None,
        data: Any = None,
    ) -> "ClientResponse":
        return await self._request(
            "DELETE", url, params=params, headers=headers, data=data
        )

    def sync_request(self, *args, **kwargs):
        proxy = None
        for p in self._proxies:
            if p.scheme in ["http", "ws"]:
                proxy = p
                break
        if proxy is not None:
            self._sync_session.proxies.update({proxy.scheme: str(proxy)})
        return self._sync_session.request(*args, **kwargs)
