import http.client
import re
from typing import Any, Final, Mapping
from urllib.parse import urlparse

from soia._impl.method import Method, Request, Response


class ServiceClient:
    _scheme: Final[str]
    _host: Final[str]  # May include the port
    _path: Final[str]

    def __init__(self, url: str):
        parsed_url = urlparse(url)
        if parsed_url.query:
            raise ValueError("Service URL must not contain a query string")
        scheme = parsed_url.scheme
        if scheme not in ["http", "https"]:
            raise ValueError("Service URL must start with http:// or https://")
        self._scheme = scheme
        self._host = parsed_url.netloc
        self._path = parsed_url.path

    def invoke_remote(
        self,
        method: Method[Request, Response],
        request: Request,
        headers: Mapping[str, str] = {},
        *,
        res_headers: list[tuple[str, str]] | None = None,
        timeout_secs: float | None = None,
    ) -> Response:
        """Invokes the given method on the remote server through an RPC."""

        request_json = method.request_serializer.to_json_code(request)
        body = ":".join(
            [
                method.name,
                str(method.number),
                "",
                request_json,
            ]
        )
        headers = {
            **headers,
            "Content-Type": "text/plain; charset=utf-8",
            "Content-Length": str(len(body)),
        }
        connection_options: dict[str, Any] = {}
        if timeout_secs is not None:
            connection_options["timeout"] = timeout_secs
        if self._scheme == "https":
            conn = http.client.HTTPSConnection(self._host, **connection_options)
        else:
            conn = http.client.HTTPConnection(self._host, **connection_options)
        try:
            conn.request(
                "POST",
                self._path,
                body=body,
                headers=headers,
            )
            response = conn.getresponse()
            if res_headers is not None:
                res_headers.clear()
                res_headers.extend(response.getheaders())
            status_code = response.status
            content_type = response.getheader("Content-Type") or ""
            response_data = response.read().decode("utf-8", errors="ignore")
        finally:
            conn.close()
        if status_code in range(200, 300):
            return method.response_serializer.from_json_code(response_data)
        else:
            message = f"HTTP status {status_code}"
            if re.match(r"text/plain\b", content_type):
                message = f"{message}: {response_data}"
            raise RuntimeError(message)
