# ==============================================================================
#
# {{ name }} (HTTP-client)
#
# This file was generated by a code generator.
# Don't make changes to it manually.
#
# ==============================================================================

# jinja2: lstrip_blocks: "True"
# mypy: ignore-errors

from __future__ import annotations

import abc

from datetime import datetime
from datetime import date

from httpx import Timeout
try:
    from typing import Literal
except ImportError:
    from typing_extensions import Literal
from typing import List
from typing import Tuple
from typing import Any
from typing import Dict
from typing import Optional
from typing import Union
from typing import Callable
from typing import get_type_hints
from typing import Mapping
from typing import Sequence
from typing import IO
from typing import cast

from jaeger_client import Tracer
from jaeger_client.span import Span
from opentracing.propagation import Format

from prometheus_client import Counter
from prometheus_client import Histogram

import httpx
from pydantic import BaseModel
from pydantic import Field
from pydantic import root_validator
from pydantic import validator
from pydantic import HttpUrl
import logging
from functools import wraps
from opentracing.ext import tags


tracer: Tracer


class TracerNotConfigured(Exception):
    pass


class TracerConfig(BaseModel):
    app_name: str
    host: str
    port: int
    propagation: str
    jaeger_enabled: bool


class BaseTracerIntegration(abc.ABC):
    def __init__(self, tracer: Optional[Tracer] = None):
        self.tracer = tracer

    @abc.abstractmethod
    def get_tracing_http_headers(self) -> Dict[str, str]: ...

    @abc.abstractmethod
    def get_current_trace_id(self) -> Optional[str]: ...

    @abc.abstractmethod
    def get_tracer(self) -> Tracer: ...

    @abc.abstractmethod
    def get_current_span(self) -> Optional[Span]: ...


class DefaultTracerIntegration(BaseTracerIntegration):
    def get_tracing_http_headers(self) -> Dict[str, str]:
        tracer = self.get_tracer()
        span = self.get_current_span()
        if not span:
            return {}
        span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT)
        headers: Dict[str, str] = {}
        tracer.inject(span, Format.HTTP_HEADERS, headers)
        return headers

    def get_current_trace_id(self) -> Optional[str]:
        span = self.get_current_span()
        trace_id = span.trace_id if span else None
        return '{:x}'.format(trace_id) if trace_id else None

    def get_tracer(self) -> Tracer:
        if not self.tracer:
            raise TracerNotConfigured('configure tracing first')
        return self.tracer

    def get_current_span(self) -> Optional[Span]:
        tracer = self.get_tracer()
        active = tracer.scope_manager.active
        return cast(Span, active.span) if active else None


def tracing(f: Callable[..., Any]) -> Callable[..., Any]:
    @wraps(f)
    {%- if sync %}
    def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
    {%- else %}
    async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
    {%- endif %}
        if not self.tracer_integration:
            {%- if sync %}
            return f(self, *args, **kwargs)
            {%- else %}
            return await f(self, *args, **kwargs)
            {%- endif %}

        db_query = kwargs.get('query', None)
        current_tags = {}
        if db_query is not None:
            current_tags[tags.DATABASE_TYPE] = 'postgres'
            current_tags[tags.DATABASE_STATEMENT] = db_query

        tracer = self.tracer_integration.get_tracer()
        span = tracer.start_span(operation_name=f.__qualname__, child_of=self.tracer_integration.get_current_span(), tags=current_tags)
        scope = tracer.scope_manager.activate(span, True)
        try:
            {%- if sync %}
            result = f(self, *args, **kwargs)
            {%- else %}
            result = await f(self, *args, **kwargs)
            {%- endif %}
        except Exception as exp:
            span.set_tag(tags.ERROR, True)
            span.set_tag('error.message', str(exp))
            raise exp
        finally:
            scope.close()
        return result

    return wrapper

# backward compatibility for httpx<0.18.2
try:
    DEFAULT_AUTH = httpx.USE_CLIENT_DEFAULT
except AttributeError:
    DEFAULT_AUTH = None


class BaseMetricsIntegration(abc.ABC):
    def __init__(
        self,
        client_response_time_histogram: Optional[Histogram] = None,
        client_non_http_errors_counter: Optional[Counter] = None,
    ):
        self._client_response_time_histogram = client_response_time_histogram
        self._client_non_http_errors_counter = client_non_http_errors_counter

    @abc.abstractmethod
    def on_request_error(self, client_name: str, error: Exception, http_method: str, http_target: str) -> None: ...

    @abc.abstractmethod
    def on_request_success(self, client_name: str, response, http_method: str, http_target: str) -> None: ...


class DefaultMetricsIntegration(BaseMetricsIntegration):
    def on_request_error(self, client_name: str, error: Exception, http_method: str, http_target: str) -> None:
        self._client_non_http_errors_counter.labels(
            client_name=client_name,
            http_method=http_method,
            http_target=http_target,
            exception=error.__class__.__name__,
        ).inc(1)
        raise error

    def on_request_success(self, client_name: str, response, http_method: str, http_target: str) -> None:
        self._client_response_time_histogram.labels(
            client_name=client_name,
            http_method=http_method,
            http_target=http_target,
            http_status_code=response.status_code,
        ).observe(response.elapsed.total_seconds())


FileContent = Union[IO[str], IO[bytes], str, bytes]
FileTypes = Union[
    # file (or text)
    FileContent,
    # (filename, file (or text))
    Tuple[Optional[str], FileContent],
    # (filename, file (or text), content_type)
    Tuple[Optional[str], FileContent, Optional[str]],
]

class EmptyBody(BaseModel):
    status_code: int
    text: str

{% for schema in discriminator_base_class_schemas %}
class {{ schema.name }}(BaseModel):
    {{ schema.attr }}: str

    @validator("{{ schema.attr }}", pre=True)
    def check(cls, v: str) -> str:
        type_hints = get_type_hints(cls)
        {{ schema.attr }}_values: Tuple[str] = type_hints["{{ schema.attr }}"].__dict__['__args__']

        if v not in {{ schema.attr }}_values:
            raise ValueError(f'invalid {{ schema.attr }} for {cls}')

        return v
{%- endfor %}


{%- for model in models %}


class {{ classname(model.id) }}(BaseModel):
    """
    {{ model.title }}
    """

    {%- if model.type.value == "any_of"  %}
    __root__: Union[
    {%- for item in model.items %}
        '{{ item.id }}',
    {%- endfor %}
    ]
    {%- else %}

    # required ---
    {%- for property in model.required_properties %}
        {%- if property.schema.description %}
    {{ property.key }}: {{ typerepr(property.schema) }} = Field(description="{{ property.schema.description }}"{%- if property.safety_key %}, alias="{{ property.orig_key }}"{%- endif %})
        {%- else %}
    {{ property.key }}: {{ typerepr(property.schema) }}
        {%- endif %}
    {%- endfor %}

    # optional ---
    {%- for property in model.optional_properties %}
        {%- if property.schema.description %}
    {{ property.key }}: Optional[{{ typerepr(property.schema) }}] = Field(description="{{ property.schema.description }}"{%- if property.safety_key %}, alias="{{ property.orig_key }}"{%- endif %})
        {%- else %}
    {{ property.key }}: Optional[{{ typerepr(property.schema) }}] = None
        {%- endif %}
    {%- endfor %}

    {%- for property in model.required_properties + model.optional_properties if property.safety_key %}

    class Config:
        # Обращение по имени поля, даже если есть алиас.
        allow_population_by_field_name = True

    @root_validator
    def change_name(cls, values):
        """
        Каст полей согласно алиасам

        В OpenApi-спеке названия полей могут быть в формате, в котором
        нельзя создавать имена переменных в python, они заменяются безопасными алиасами
        """
        {%- for property in model.required_properties + model.optional_properties if property.safety_key %}
        values["{{ property.orig_key }}"] = values["{{ property.safety_key }}"]
        del values["{{ property.safety_key }}"]
        {% endfor %}
        return values
    {%- break %}
    {%- endfor %}

    {%- endif %}
{%- endfor %}


class BasicAuth(BaseModel):
    username: str
    password: str


class {{ name }}:
    def __init__(
        self,
        base_url: str,
        timeout: int = 5,
        client_name: str = "{{ name | replace('Client', '') | lower }}",
        {%- if sync %}
        client: Optional[httpx.Client] = None,
        {%- else %}
        client: Optional[httpx.AsyncClient] = None,
        {%- endif %}
        headers: Optional[Dict[str, str]] = None,
        tracer_integration: Optional[BaseTracerIntegration] = None,
        metrics_integration: Optional[BaseMetricsIntegration] = None,
    ):
        {%- if sync %}
        self.client = client or httpx.Client(timeout=Timeout(timeout))
        {%- else %}
        self.client = client or httpx.AsyncClient(timeout=Timeout(timeout))
        {%- endif %}
        self.base_url = base_url
        self.headers = headers or {}
        self.tracer_integration = tracer_integration
        self.metrics_integration=metrics_integration
        self.client_name = client_name

    {#-  get items begin  #}
    {% with items=get.items(), method='get'%}
    {%- include 'client/httpx-method.j2' %}
    {%- endwith %}
    {#-  get items end  #}
    {#-  post items begin  #}
    {%- with items=post.items(), method='post'%}
    {%- include 'client/httpx-method.j2' %}
    {%- endwith %}
    {#-  post items end  #}
    {#-  patch items begin  #}
    {%- with items=patch.items(), method='patch'%}
    {%- include 'client/httpx-method.j2' %}
    {%- endwith %}
    {#-  patch items end  #}
    {#-  put items begin  #}
    {%- with items=put.items(), method='put'%}
    {%- include 'client/httpx-method.j2' %}
    {%- endwith %}
    {#-  put items end  #}
    {#-  delete_no_body items begin  #}
    {%- with items=delete_no_body.items(), method='delete', body=False %}
    {%- include 'client/httpx-method.j2' %}
    {%- endwith %}
    {#-  delete_no_body items end  #}
    {% if sync %}
    def close(self) -> None:
        self.client.close()
    {%- else %}
    async def close(self) -> None:
        await self.client.aclose()
    {%- endif %}

    def _get_url(self, path: str) -> str:
        return f'{self.base_url}{path}'

    def log_extra(self, **kwargs: Any) -> Dict[str, Any]:
        return {'extra': {'props': {'data': kwargs}}}

    def log_error(self, client_name: str, method, url: str, params, content, headers) -> None:
        msg = f"request error"
        msg += f" | client={client_name}"
        msg += f" | method={method}"
        msg += f" | url={url}"
        msg += f" | params={params}"
        msg += f" | content={content}"
        msg += f" | headers={headers}"

        logging.error(
            msg,
            **self.log_extra(
                client=client_name,
                method=method,
                content=content,
                url=url,
                params=params,
            ),
        )

    def add_tracing_data_to_headers(self, headers_: Dict[str, str]) -> None:
        tracing_headers = self.tracer_integration.get_tracing_http_headers()
        headers_.update(tracing_headers)
        trace_id = self.tracer_integration.get_current_trace_id() or ''
        headers_['x-trace-id'] = trace_id


{% for model in models -%}
{{ classname(model.id) }}.update_forward_refs()
{% endfor %}
