from __future__ import annotations

import io
import json
import urllib.request
import urllib.response
from typing import TYPE_CHECKING, Any

import requests
import requests.adapters
import requests.models

from mockmesh.core.fallback import FallbackConfig, safe_intercept
from mockmesh.core.logger import get_logger
from mockmesh.plugins.base import BasePlugin

if TYPE_CHECKING:
    from mockmesh.response.engine import ResponseEngine
    from mockmesh.storage.manager import StorageManager

_log = get_logger("interceptors.http")


class HttpInterceptor(BasePlugin):

    name = "http"

    _url_dispatchers: dict[str, tuple[Any, Any]] = {}

    def __init__(self, response_engine: "ResponseEngine") -> None:
        super().__init__(response_engine)
        self._original_requests_send: Any = None
        self._original_urllib_open: Any = None

    @classmethod
    def register_url_dispatcher(
        cls,
        provider: str,
        url_matcher: Any,
        handler: Any,
    ) -> None:
        cls._url_dispatchers[provider] = (url_matcher, handler)
        _log.debug("URL dispatcher registered", extra={"mm_provider": provider})

    @classmethod
    def unregister_url_dispatcher(cls, provider: str) -> None:
        cls._url_dispatchers.pop(provider, None)
        _log.debug("URL dispatcher removed", extra={"mm_provider": provider})

    @staticmethod
    def _make_generic_response(url: str) -> Any:
        resp = requests.models.Response()
        resp.status_code = 200
        resp._content = b'{"mocked": true}'
        resp.headers["Content-Type"] = "application/json"
        resp.encoding = "utf-8"
        resp.url = url
        return resp

    def activate(
        self,
        config: dict,
        *,
        storage: "StorageManager | None" = None,
        provider_defaults: dict | None = None,
        fallback_config: FallbackConfig | None = None,
        custom_handlers: dict | None = None,
        **_: Any,
    ) -> None:
        self._fallback_config = fallback_config
        self._custom_handlers = custom_handlers if custom_handlers is not None else {}
        self._patch_requests()
        self._patch_urllib()
        self._active = True
        _log.info("HTTP interceptor activated")

    def deactivate(self) -> None:
        self._unpatch_requests()
        self._unpatch_urllib()
        self._active = False
        _log.info("HTTP interceptor deactivated")

    def _patch_requests(self) -> None:
        self._original_requests_send = requests.adapters.HTTPAdapter.send
        interceptor = self

        def _mock_send(adapter_self: Any, request: Any, **kwargs: Any) -> Any:
            url = getattr(request, "url", "") or ""
            for _provider, (url_matcher, handler) in interceptor._url_dispatchers.items():
                if url_matcher(url):
                    return handler(adapter_self, request, **kwargs)

            method = getattr(request, "method", "GET") or "GET"
            return safe_intercept(
                provider="http",
                operation=f"{method} {url[:80]}",
                handler_fn=interceptor._handle_requests,
                handler_args=(request,),
                fallback_config=interceptor._fallback_config,
                original_fn=interceptor._original_requests_send,
                original_args=(adapter_self, request),
                original_kwargs=kwargs,
                generic_response=interceptor._make_generic_response(url),
                custom_handlers=interceptor._custom_handlers,
            )

        requests.adapters.HTTPAdapter.send = _mock_send  # type: ignore
        _log.debug("Patched requests.adapters.HTTPAdapter.send")

    def _unpatch_requests(self) -> None:
        if self._original_requests_send is None:
            return
        requests.adapters.HTTPAdapter.send = self._original_requests_send
        self._original_requests_send = None
        _log.debug("Restored requests.adapters.HTTPAdapter.send")

    def _handle_requests(self, prepared_request: Any) -> Any:
        url    = prepared_request.url or ""
        method = prepared_request.method or "GET"

        mock = self.response_engine.resolve_http(url=url, method=method)

        _log.info(
            "Intercepted HTTP request",
            extra={
                "mm_url":    url,
                "mm_method": method,
                "mm_status": mock.status,
                "mm_tier":   getattr(mock, "tier", "unknown"),
            },
        )

        response              = requests.models.Response()
        response.status_code  = mock.status
        response.headers.update(mock.headers)

        body = mock.resolve_body(url=url, method=method)
        if body is None:
            raw_bytes = b""
        elif isinstance(body, (dict, list)):
            raw_bytes = json.dumps(body).encode("utf-8")
            if "Content-Type" not in response.headers:
                response.headers["Content-Type"] = "application/json"
        elif isinstance(body, str):
            raw_bytes = body.encode("utf-8")
        elif isinstance(body, bytes):
            raw_bytes = body
        else:
            raw_bytes = str(body).encode("utf-8")

        response._content = raw_bytes
        response.encoding = "utf-8"
        response.url      = url
        return response

    def _patch_urllib(self) -> None:
        self._original_urllib_open = urllib.request.OpenerDirector.open
        interceptor = self

        def _mock_open(opener_self: Any, fullurl: Any,
                       data: Any = None, timeout: Any = None) -> Any:
            url    = fullurl if isinstance(fullurl, str) else getattr(fullurl, "full_url", str(fullurl))
            method = "POST" if data else "GET"
            return safe_intercept(
                provider="http",
                operation=f"{method} {url[:80]}",
                handler_fn=interceptor._handle_urllib,
                handler_args=(url, method),
                fallback_config=interceptor._fallback_config,
                original_fn=interceptor._original_urllib_open,
                original_args=(opener_self, fullurl, data, timeout),
                generic_response=urllib.response.addinfourl(
                    io.BytesIO(b'{"mocked": true}'), headers={}, url=url, code=200,
                ),
                custom_handlers=interceptor._custom_handlers,
            )

        urllib.request.OpenerDirector.open = _mock_open  # type: ignore
        _log.debug("Patched urllib.request.OpenerDirector.open")

    def _unpatch_urllib(self) -> None:
        if self._original_urllib_open is None:
            return
        urllib.request.OpenerDirector.open = self._original_urllib_open
        self._original_urllib_open = None
        _log.debug("Restored urllib.request.OpenerDirector.open")

    def _handle_urllib(self, url: str, method: str) -> Any:
        mock = self.response_engine.resolve_http(url=url, method=method)
        body = mock.resolve_body(url=url, method=method)

        _log.info(
            "Intercepted urllib request",
            extra={"mm_url": url, "mm_method": method, "mm_status": mock.status},
        )

        if body is None:
            raw_bytes = b""
        elif isinstance(body, (dict, list)):
            raw_bytes = json.dumps(body).encode("utf-8")
        elif isinstance(body, str):
            raw_bytes = body.encode("utf-8")
        elif isinstance(body, bytes):
            raw_bytes = body
        else:
            raw_bytes = str(body).encode("utf-8")

        fp   = io.BytesIO(raw_bytes)
        resp = urllib.response.addinfourl(fp, headers={}, url=url, code=mock.status)  # type: ignore
        return resp
