import json
import linecache
import re
from collections.abc import Mapping
from typing import NamedTuple

from httpx import AsyncClient
from starlette.authentication import UnauthenticatedUser
from starlette.datastructures import Headers
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Route

from starlette_proxy_router.exceptions import AttributeAuthUserError
from starlette_proxy_router.parser import EndpointArgs, MethodHTTP
from starlette_proxy_router.types import AuthType


class DataRequest(NamedTuple):
    headers: Headers
    query_params: Mapping[str, str]
    path_params: Mapping[str, str]
    body: bytes


async def _parse_request(request: Request) -> DataRequest:
    return DataRequest(
        headers=request.scope["headers"],
        query_params=request.query_params,
        body=await request.body(),
        path_params=request.path_params,
    )


def _build_target_url(host: str, target: str, path_params: Mapping[str, str]) -> str:
    target = target.format_map(path_params)
    return f"{host}{target[1:]}"


_AUTH_CODE_LINES = """
    is_auth = {is_auth}
    auth_type = '{auth_type}'
    auth_fields = {auth_fields}

    query_params = {{k: v for k, v in data_request.query_params.items()}}
    exclude_keys = [b'content-length', b'host']
    headers = {{k.decode(): v.decode() for k, v in data_request.headers if k not in exclude_keys}}

    if is_auth:
        if isinstance(request.user, UnauthenticatedUser):
            raise HTTPException(status_code=401, detail='User is not authenticated')
        if auth_type == AuthType.QUERY_PARAMS:
            for field in auth_fields:
                try:
                    query_params[field] = request.user.__dict__[field]
                except AttributeError as e:
                    raise AttributeAuthUserError(field)
        else:
            for field in auth_fields:
                try:
                    headers[field.replace('_', '-').capitalize()] = request.user.__dict__[field]
                except AttributeError as e:
                    raise AttributeAuthUserError(field)
"""
_HTTP_PROXY_ENDPOINT_IS_BODY = """
async def {func_name}(request):
    data_request = await _parse_request(request)
    target_url = _build_target_url('{host}', '{target}', data_request.path_params)

    {auth_code_lines}

    try:
        body = json.loads(data_request.body)
    except json.decoder.JSONDecodeError:
        body = None

    async with AsyncClient() as ac:
        res = await ac.{method}(
            target_url,
            params=query_params,
            json=body,
            timeout={timeout},
            headers=headers
        )
        return Response(
            content=res.content,
            status_code=res.status_code,
            headers=res.headers,
        )
"""
_HTTP_PROXY_ENDPOINT_NOT_BODY = """
async def {func_name}(request):
    data_request = await _parse_request(request)
    target_url = _build_target_url('{host}', '{target}', data_request.path_params)

    {auth_code_lines}

    async with AsyncClient() as ac:
        res = await ac.{method}(
            target_url,
            params=query_params,
            timeout={timeout},
            headers=headers
        )
        return Response(
            content=res.content,
            status_code=res.status_code,
            headers=res.headers,
        )
"""
_METHOD_BODIES = {
    MethodHTTP.GET: _HTTP_PROXY_ENDPOINT_NOT_BODY,
    MethodHTTP.POST: _HTTP_PROXY_ENDPOINT_IS_BODY,
    MethodHTTP.PUT: _HTTP_PROXY_ENDPOINT_IS_BODY,
    MethodHTTP.DELETE: _HTTP_PROXY_ENDPOINT_NOT_BODY,
    MethodHTTP.PATCH: _HTTP_PROXY_ENDPOINT_IS_BODY
}


class RouteCompilier:
    __slots__ = ()

    def compile_route(self, endpoint_args: EndpointArgs) -> Route:
        func_name = self._build_func_name(endpoint_args.path)
        body_auth_code = _AUTH_CODE_LINES.format_map({
            "is_auth": endpoint_args.is_auth,
            "auth_type": endpoint_args.auth_type,
            "auth_fields": endpoint_args.auth_fields,
        })
        body_http_endpoint = _METHOD_BODIES[endpoint_args.method]
        body_http_endpoint = body_http_endpoint.format_map({
            "auth_code_lines": body_auth_code,
            "func_name": func_name,
            "method": endpoint_args.method.value,
            "target": endpoint_args.target,
            "host": endpoint_args.host,
            "timeout": endpoint_args.timeout,
        })
        global_vars = {
            'json': json,
            "AsyncClient": AsyncClient,
            "Response": Response,
            "AuthType": AuthType,
            "AttributeAuthUserError": AttributeAuthUserError,
            "HTTPException": HTTPException,
            "UnauthenticatedUser": UnauthenticatedUser,
            "_parse_request": _parse_request,
            "_build_target_url": _build_target_url,
        }
        file_name = f"{endpoint_args.service_name}_route"
        lines = body_http_endpoint.splitlines(keepends=True)
        linecache.cache[file_name] = (
            len(body_http_endpoint),
            None,
            lines,
            file_name,
        )
        compiled = compile(body_http_endpoint, file_name, "exec")
        exec(compiled, global_vars)

        return Route(
            endpoint_args.path,
            global_vars[func_name],  # type: ignore
            methods=[endpoint_args.method],
        )

    def _build_func_name(self, path: str) -> str:
        sub_paths = re.sub(r"/{[^{}]+}", "", path)
        sub_paths_list = re.split(r"[-_/]", sub_paths)
        result = "_".join(sub_paths_list[1:])

        if not result:
            return "homepage"
        return result
