import os
from typing import Any, Tuple, TypedDict

import httpx
from tornado.httputil import HTTPServerRequest

from aext_shared.config import get_config
from aext_shared.errors import BackendError, BadRequest, UnknownError

config = get_config()
cloudflare_client_id = os.environ.get("NUCLEUS_CLOUDFLARE_CLIENT_ID", None)
cloudflare_client_secret = os.environ.get("NUCLEUS_CLOUDFLARE_CLIENT_SECRET", None)


class ProxyResponse(TypedDict):
    remote_status_code: int
    remote_data: Any


async def backend_proxy(incoming_request: HTTPServerRequest, url: str, **kwargs) -> ProxyResponse:
    return (await backend_proxy_core(incoming_request=incoming_request, url=url, **kwargs))[0]


async def backend_proxy_core(
    incoming_request: HTTPServerRequest, url: str, **kwargs
) -> Tuple[ProxyResponse, httpx.Response]:
    headers: dict[str, str] = dict(kwargs.get("headers", {}))
    kwargs["headers"] = headers
    parse_response = kwargs.pop("parse_response", True)
    # Only add CF headers if the request is being triggered to Anaconda Cloud
    if f"{config['cloud']['url']}" in url:
        # Add (overridable) Cloudflare headers to internal requests
        if cloudflare_client_id and cloudflare_client_secret:
            headers.update(
                {"CF-Access-Client-Id": cloudflare_client_id, "CF-Access-Client-Secret": cloudflare_client_secret}
            )

    if "json" in kwargs:
        headers["Content-Type"] = "application/json"

    shared_headers = [(name, incoming_request.headers.get(name, None)) for name in ["User-Agent", "Api-Version"]]
    shared_headers = [(name, val) for (name, val) in shared_headers if val and name not in headers]
    for name, val in shared_headers:
        headers[name] = val

    method = kwargs.pop("method", "GET")
    proxy_request = httpx.Request(method, url, **kwargs)

    # Redirects strip Authorization headers if not same URL (cookies are smart and follow browser logic)
    # Attention should be paid to Cloudflare headers as they are not stripped (but are only set on staging for now).
    # More info:
    # https://github.com/encode/httpx/blob/053bc57c3799801ff11273dd393cb0715e63ecf9/httpx/_client.py#L459
    # https://github.com/encode/httpx/blob/053bc57c3799801ff11273dd393cb0715e63ecf9/httpx/_client.py#L536
    async with httpx.AsyncClient(follow_redirects=True) as client:
        try:
            response: httpx.Response = await client.send(proxy_request)
        except Exception as e:
            raise UnknownError({"reason": "proxy_connection_error"}) from e

        try:
            if not parse_response:
                return response, None
            data = response.json() if len(response.text) else None
        except ValueError as e:
            raise BadRequest(
                {
                    "reason": "proxy_response_not_json",
                    "remote_status_code": response.status_code,
                    "raw": response.text,
                }
            ) from e

        if response.status_code < httpx.codes.OK or response.status_code >= httpx.codes.MULTIPLE_CHOICES:
            raise BackendError(
                200,
                {
                    "reason": "proxy_bad_response",
                    "remote_status_code": response.status_code,
                    "remote_data": data,
                },
            )

        return ProxyResponse(remote_status_code=response.status_code, remote_data=data), response
