import os
from urllib.parse import urlparse
from .retries import make_retry_client, retry_default_rule
from .throttle import make_throttled_client
from .robots import (
    make_robots_txt_client,
    RobotExclusionError,
    RobotsRejectFunc,
    raise_robots_txt,
)
from .dev_cache import (
    make_dev_caching_client,
    MemoryCache,
    FileCache,
    SqliteCache,
    CacheStorage,
    CacheResponse,
    _cache_200s,
    _default_keyfunc,
)
from ._types import ResponsePredicate, CacheKeyfunc
from httpx import Client


def make_careful_client(
    *,
    client: Client | None = None,
    retry_attempts: int = 0,
    retry_wait_seconds: float = 10,
    should_retry: ResponsePredicate = retry_default_rule,
    requests_per_minute: int = 0,
    cache_storage: CacheStorage | None = None,
    cache_write_only: bool = False,
    should_cache: ResponsePredicate = _cache_200s,
    cache_keyfunc: CacheKeyfunc = _default_keyfunc,
    check_robots_txt: bool = False,
    robots_txt_user_agent: str | None = None,
    robots_txt_on_reject: RobotsRejectFunc = raise_robots_txt,
) -> Client:
    """
    This function patches an `httpx.Client` so that all requests made with the client support
     [retries](#retries), [throttling](#throttling), and [development caching](#development-caching).


    Parameters:
        client: A pre-configured `httpx.Client`. If omitted a default client will be created.

        retry_attempts: Maximum number of retries. If non-zero will retry up to this many times
                         with increasing wait times, starting with `retry_wait_seconds`.

        retry_wait_seconds: Number of seconds to sleep between first attempt and first retry.
                             Subsequent attempts will increase exponentially (2x, 4x, 8x, etc.)

        should_retry: Predicate function that takes a `httpx.Response` and returns `True` if it should be retried.

        requests_per_minute: Maximum number of requests per minute. (e.g. 30 will throttle to ~2s between requests)

        cache_storage: An object that implements the [cache storage interface](#cache-storage).

        cache_write_only: Update cache, but never read from it.

        should_cache: Predicate function that takes a `httpx.Response` and returns `True` if it should be cached.

        cache_keyfunc: Function that takes request details and returns a unique cache key.

    """
    if client is None:
        client = Client()
    # order matters, retry on inside b/c it is last-chance scenario
    if retry_attempts:
        client = make_retry_client(
            client=client,
            attempts=retry_attempts,
            wait_seconds=retry_wait_seconds,
            should_retry=should_retry,
        )
    # throttling around retries
    if requests_per_minute:
        client = make_throttled_client(
            client=client, requests_per_minute=requests_per_minute
        )
    # caching on top layer, so cache will be checked before throttling/etc.
    if cache_storage:
        client = make_dev_caching_client(
            client=client,
            cache_storage=cache_storage,
            cache_keyfunc=cache_keyfunc,
            should_cache=should_cache,
            write_only=cache_write_only,
        )
    # robots.txt before cache
    if check_robots_txt:
        client = make_robots_txt_client(
            client=client,
            as_user_agent=robots_txt_user_agent,
            on_rejection=robots_txt_on_reject,
        )

    return client


def _int_env(var_name: str, default: int) -> int:
    return int(os.environ.get(var_name, default))


def _float_env(var_name: str, default: float) -> float:
    return float(os.environ.get(var_name, default))


def _bool_env(var_name: str, default: bool) -> bool:
    """helper function for bool env vars"""
    return bool(os.environ.get(var_name, "T" if default else ""))


def _cache_env(var_name: str, default: CacheStorage | None) -> CacheStorage | None:
    """
    helper function that reads cache as a protocol string
    """
    cache_str = os.environ.get(var_name)
    if not cache_str:
        return default
    parsed = urlparse(cache_str)
    if parsed.scheme == "memory":
        return MemoryCache()
    elif parsed.scheme == "file":
        return FileCache(parsed.path)
    elif parsed.scheme == "sqlite":
        return SqliteCache(parsed.path)


def make_careful_client_from_env(
    *,
    client: Client | None = None,
    retry_attempts: int = 0,
    retry_wait_seconds: float = 10,
    should_retry: ResponsePredicate = retry_default_rule,
    requests_per_minute: int = 0,
    cache_storage: CacheStorage | None = None,
    cache_write_only: bool = False,
    should_cache: ResponsePredicate = _cache_200s,
    cache_keyfunc: CacheKeyfunc = _default_keyfunc,
    check_robots_txt: bool = False,
    robots_txt_user_agent: str | None = None,
    robots_txt_on_reject: RobotsRejectFunc = raise_robots_txt,
) -> Client:
    """
    Make a careful client from environment variables.

    Any set environment variables will override parameters if set.

    Numeric:
        - CAREFUL_RETRY_ATTEMPTS
        - CAREFUL_RETRY_WAIT_SECONDS
        - CAREFUL_REQUESTS_PER_MINUTE
        - CAREFUL_CHECK_ROBOTS_TXT
    Booleans (any non-empty value is true):
        - CAREFUL_CACHE_WRITE_ONLY
        - CAREFUL_ROBOTS_TXT_USER_AGENT
    Cache:
        - CAREFUL_CACHE, which can be:
            memory://
            cache://path/to/db.sqlite3
            file://path/to/directory

    Function parameters do not have environment variables.
    """
    return make_careful_client(
        client=client,
        retry_attempts=_int_env("CAREFUL_RETRY_ATTEMPTS", retry_attempts),
        retry_wait_seconds=_float_env("CAREFUL_RETRY_WAIT_SECONDS", retry_wait_seconds),
        should_retry=should_retry,
        requests_per_minute=_int_env(
            "CAREFUL_REQUESTS_PER_MINUTE", requests_per_minute
        ),
        cache_storage=_cache_env("CAREFUL_CACHE", cache_storage),
        cache_write_only=_bool_env("CAREFUL_CACHE_WRITE_ONLY", cache_write_only),
        should_cache=should_cache,
        cache_keyfunc=cache_keyfunc,
        check_robots_txt=_bool_env("CAREFUL_CHECK_ROBOTS_TXT", check_robots_txt),
        robots_txt_user_agent=os.environ.get(
            "CAREFUL_ROBOTS_TXT_USER_AGENT", robots_txt_user_agent
        ),
        robots_txt_on_reject=robots_txt_on_reject,
    )


__all__ = [
    "make_careful_client",
    "make_retry_client",
    "make_throttled_client",
    "make_dev_caching_client",
    "make_robots_txt_client",
    "MemoryCache",
    "FileCache",
    "SqliteCache",
    "CacheResponse",
    "RobotExclusionError",
]
