import os
import pathlib
import logging
import structlog
import typing
from urllib.parse import urlparse
from .retries import RetryPlugin, retry_default_rule
from .throttle import ThrottlePlugin
from .robots import (
    RobotsTxtPlugin,
    RobotExclusionError,
    RobotsRejectFunc,
    raise_robots_txt,
)
from .dev_cache import (
    DevCachePlugin,
    MemoryCache,
    FileCache,
    SqliteCache,
    CacheStorage,
    CacheResponse,
    _cache_200s,
    _default_keyfunc,
)
from ._types import ResponsePredicate, CacheKeyfunc
from .client import PluginClient

log = structlog.get_logger("careful")


def make_careful_client(
    *,
    client: PluginClient | 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,
    log_level: int = logging.INFO,
) -> PluginClient:
    """
    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.

        check_robots_txt: Should `robots.txt` be respected?

        robots_txt_user_agent: What user-agent should be used for evaluating `robots.txt` rules? (If not set, will use client's configured UA)

        robots_txt_on_reject: What should happen when a page isn't accessible according to `robots.txt`

        log_level: A `logging` level to use for internal structured logging.
    """
    # set log level first
    structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(log_level))

    if client is None:
        client = PluginClient()
    # nothing should be able to exceed throttle so it goes first
    if requests_per_minute:
        client.add_plugin(ThrottlePlugin(requests_per_minute=requests_per_minute))
    # retry next in order b/c it is last-chance scenario
    if retry_attempts:
        client.add_plugin(
            RetryPlugin(
                attempts=retry_attempts,
                wait_seconds=retry_wait_seconds,
                should_retry=should_retry,
            )
        )
    # caching on top layer, so cache will be checked before throttling/etc.
    if cache_storage:
        client.add_plugin(
            DevCachePlugin(
                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.add_plugin(
            RobotsTxtPlugin(
                as_user_agent=robots_txt_user_agent,
                on_rejection=robots_txt_on_reject,
            )
        )

    return client


def _env(var_name: str, default: int | float, type_c: typing.Callable) -> int:
    val = type_c(os.environ.get(var_name, default))
    log.info("load environment setting", var_name=var_name, val=val, type=type_c)
    return val


def _bool_env(var_name: str, default: bool) -> bool:
    """helper function for bool env vars"""
    val = bool(os.environ.get(var_name, "T" if default else ""))
    log.info("load environment setting", var_name=var_name, val=val)
    return val


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:
        log.debug("no cache env variable", var_name=var_name, default=default)
        return default
    parsed = urlparse(cache_str)
    # urlparse always starts with a / (var needs :/// to skip netloc -> into path)
    true_path = parsed.path[1:] if parsed.path.startswith("/") else parsed.path
    # if it starts with a //// then it is an absolute path
    if true_path.startswith("/"):
        path = pathlib.Path(true_path)
    else:
        path = pathlib.Path.cwd() / true_path

    lg = log.bind(var_name=var_name, cache_env_val=cache_str, path=path)
    if parsed.scheme == "memory":
        lg.info("load environment MemoryCache")
        return MemoryCache()
    elif parsed.scheme == "file":
        lg.info("load environment FileCache")
        return FileCache(path)
    elif parsed.scheme == "sqlite":
        lg.info("load environment SqliteCache")
        return SqliteCache(path)
    else:
        lg.warning("invalid cache")
        return default


def make_careful_client_from_env(
    *,
    client: PluginClient | 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,
    log_level: int = logging.INFO,
) -> PluginClient:
    """
    Make a careful client from environment variables.

    Provided parameters are used as defaults, environment variables
    will override these if set.

    | Name | Type | Default |
    |-|-|-|
    | CAREFUL_RETRY_ATTEMPTS | int | `0` |
    | CAREFUL_RETRY_WAIT_SECONDS | float | `10` |
    | CAREFUL_REQUESTS_PER_MINUTE | int | `0` |
    | CAREFUL_CHECK_ROBOTS_TXT | bool | `""` |
    | CAREFUL_CACHE_WRITE_ONLY | bool | `""` |
    | CAREFUL_ROBOTS_TXT_USER_AGENT | str | `None` |
    | CAREFUL_CACHE | str | `None` |
    | CAREFUL_LOG_LEVEL | str | `"INFO"` |

    - Booleans are `True` if set to a non-empty string.
    - `CAREFUL_CACHE` can be:
        - memory://
        - cache://path/to/db.sqlite3
        - file://path/to/directory

    Function parameters do not have environment variables.
    """
    log_level_str = _env("CAREFUL_LOG_LEVEL", log_level, str)
    if isinstance(log_level_str, str):
        if log_level_str in logging.getLevelNamesMapping():
            # replace log_level with int version
            log_level = logging.getLevelNamesMapping()[log_level_str]

    return make_careful_client(
        client=client,
        retry_attempts=_env("CAREFUL_RETRY_ATTEMPTS", retry_attempts, int),
        retry_wait_seconds=_env(
            "CAREFUL_RETRY_WAIT_SECONDS", retry_wait_seconds, float
        ),
        should_retry=should_retry,
        requests_per_minute=_env(
            "CAREFUL_REQUESTS_PER_MINUTE", requests_per_minute, int
        ),
        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,
        log_level=log_level,
    )


__all__ = [
    "PluginClient",
    "make_careful_client_from_env",
    "make_careful_client",
    "RetryPlugin",
    "ThrottlePlugin",
    "RobotsTxtPlugin",
    "RobotExclusionError",
    "DevCachePlugin",
    "MemoryCache",
    "FileCache",
    "SqliteCache",
    "CacheResponse",
]
