import types
import functools
import warnings
from urllib.robotparser import RobotFileParser
from typing import Protocol, cast, Callable
from httpx import Client, Response, URL


class RobotExclusionError(Exception):
    pass


def raise_robots_txt(url, robots):
    raise RobotExclusionError(f"{url} excluded by {robots.url}")


def warn_robots_txt(url, robots):
    warnings.warn(f"{url} excluded by {robots.url}")


class RobotsClient(Protocol):
    _robots_for_domain: dict[str, RobotFileParser]
    _robots_ua: str
    _rejected_action: Callable[[str, RobotFileParser], None]
    _no_check_request: Callable
    request: Callable
    headers: dict


def _robot_check_request(client: RobotsClient, *args, **kwargs) -> Response:
    method, url = args
    uurl = URL(url)
    domain = uurl.host
    if domain not in client._robots_for_domain:
        robots_url = f"{uurl.scheme}://{domain}/robots.txt"
        robots_resp = client._no_check_request("GET", robots_url)
        # pass url for output, but don't do read
        parser = RobotFileParser(robots_url)
        parser.parse(robots_resp.text.splitlines())
        client._robots_for_domain[domain] = parser
    if not client._robots_for_domain[domain].can_fetch(client._robots_ua, url):
        client._rejected_action(url, client._robots_for_domain[domain])
    # if action doesn't raise an exception, the request goes through
    return client._no_check_request(*args, **kwargs)


RobotsRejectFunc = Callable[[str, RobotFileParser], None]


def make_robots_txt_client(
    *,
    client: Client | None = None,
    as_user_agent: str | None = None,
    on_rejection: RobotsRejectFunc = raise_robots_txt,
) -> Client:
    if client is None:
        client = Client()

    tclient = cast(RobotsClient, client)

    tclient._robots_for_domain = {}
    if as_user_agent:
        tclient._robots_ua = as_user_agent
    else:
        tclient._robots_ua = tclient.headers["user-agent"]
    tclient._rejected_action = on_rejection

    tclient._no_check_request = client.request
    tclient.request = types.MethodType(
        functools.wraps(client.request)(_robot_check_request), client
    )
    return client
