{% include '_header.py.jinja' %}
{% from '_utils.py.jinja' import recursive_types with context %}
# -- template models.py.jinja --
import os
import logging
import inspect
import warnings
from collections import OrderedDict

from pydantic import BaseConfig, BaseModel, Field, validator

from . import types, enums, errors, fields
from ._types import BaseModelT, FuncType
from .builder import serialize_base64
from .generator import partial_models_ctx, PartialModelField


class Config(BaseConfig):
    use_enum_values: bool = True
    arbitrary_types_allowed: bool = True
    allow_population_by_field_name: bool = True
    json_encoders: Dict[Any, FuncType] = {
        fields.Base64: serialize_base64,
    }


log: logging.Logger = logging.getLogger(__name__)
_created_partial_types: Set[str] = set()

{% if not recursive_types %}
# packages that implicitly subclass models
# this should not raise any warnings as users
# of these packages cannot do anything about it
_implicit_subclass_packages: Set[str] = {
    'fastapi',
}


def _maybe_warn_subclassing(new_model: str, base_model: str, *, stacklevel: int = 3) -> None:
    # at least 3 frames are guaranteed to exist if we are being called from __init_subclass__
    # stack: 1 = __init_subclass__, 2 = abc, 3 = <caller>
    try:
        frame = inspect.stack()[stacklevel]
        module = inspect.getmodule(frame[0])
        if module is not None:
            name, *_ = module.__name__.split('.')
            if name in _implicit_subclass_packages:
                return
    except Exception as exc:
        # disabling subclass warnings depending on the caller module is not a mission critical
        # feature, users can disable these warnings themselves
        # https://github.com/RobertCraigie/prisma-client-py/issues/278#issuecomment-1031421561
        log.debug('Ignoring exception encountered during stack inspection check: %s', str(exc))

    message = (
        'Subclassing models while using pseudo-recursive types may cause unexpected '
        'errors when static type checking;\n'
        'You can disable this warning by generating fully recursive types: \n'
        'https://prisma-client-py.readthedocs.io/en/stable/reference/config/#recursive\n'
        'or if that is not possible you can pass warn_subclass=False e.g.\n'
        f'  class {new_model}(prisma.models.{base_model}, warn_subclass=False):'
    )
    warnings.warn(message, errors.UnsupportedSubclassWarning, stacklevel=4)
{% endif %}


{% for model in dmmf.datamodel.models %}
class {{ model.name }}(BaseModel):
    {% if model.documentation is none %}
    """Represents a {{ model.name }} record"""
    {% else %}
    """{{ format_documentation(model.documentation) }}"""
    {% endif %}

    {% for field in model.all_fields %}
    {{ field.name }}:
            {%- if field.is_required and not field.relation_name -%}
                {{ ' ' }}{{ field.python_type }}
            {% else -%}
                {{ ' ' }}Optional[{{ field.python_type }}]
            {% endif %}
    {% if not field.documentation is none %}
    """{{ format_documentation(field.documentation) }}"""

    {% endif %}
    {% endfor %}

    Config = Config

    {% if recursive_types %}
    @classmethod
    def prisma(cls: Type[BaseModelT]) -> 'actions.{{ model.name }}Actions[BaseModelT]':
        from .client import get_client

        return actions.{{ model.name }}Actions[BaseModelT](get_client(), cls)
    {% else %}
    @classmethod
    def prisma(cls) -> 'actions.{{ model.name }}Actions':
        from .client import get_client

        return actions.{{ model.name }}Actions(get_client(), cls)

    # take *args and **kwargs so that other metaclasses can define arguments
    def __init_subclass__(
        cls,
        *args: Any,
        warn_subclass: bool = True,
        **kwargs: Any,
    ) -> None:
        super().__init_subclass__()
        if warn_subclass:
            _maybe_warn_subclassing(cls.__name__, '{{ model.name }}', stacklevel=3)
    {% endif %}

    {% if model.required_array_fields|list|length > 0 %}
    @validator({{ model.required_array_fields | map(attribute="name") | map('quote') | join(", ") }}, pre=True, allow_reuse=True)
    @classmethod
    def _transform_required_list_fields(cls, value: object) -> object:
        # When using raw queries, some databases will return `None` for an array field that has not been set yet.
        #
        # In our case we want to use an empty list instead as that is the internal Prisma behaviour and we want
        # to use the same consistent structure between the core ORM and raw queries. For example, if we updated
        # our type definitions to include `None` for `List` fields then it would be misleading as it will only
        # ever be `None` in raw queries.
        if value is None:
            return []

        return value
    {% endif %}

    @staticmethod
    def create_partial(
        name: str,
        include: Optional[Iterable['types.{{ model.name }}Keys']] = None,
        exclude: Optional[Iterable['types.{{ model.name }}Keys']] = None,
        required: Optional[Iterable['types.{{ model.name }}Keys']] = None,
        optional: Optional[Iterable['types.{{ model.name }}Keys']] = None,
        relations: Optional[Mapping['types.{{ model.name }}RelationalFieldKeys', str]] = None,
        exclude_relational_fields: bool = False,
    ) -> None:
        if not os.environ.get('PRISMA_GENERATOR_INVOCATION'):
            raise RuntimeError(
                'Attempted to create a partial type outside of client generation.'
            )

        if name in _created_partial_types:
            raise ValueError(f'Partial type "{name}" has already been created.')

        if include is not None:
            if exclude is not None:
                raise TypeError('Exclude and include are mutually exclusive.')
            if exclude_relational_fields is True:
                raise TypeError('Include and exclude_relational_fields=True are mutually exclusive.')

        if required and optional:
            shared = set(required) & set(optional)
            if shared:
                raise ValueError(f'Cannot make the same field(s) required and optional {shared}')

        if exclude_relational_fields and relations:
            raise ValueError(
                'exclude_relational_fields and relations are mutually exclusive'
            )

        fields: Dict['types.{{ model.name }}Keys', PartialModelField] = OrderedDict()

        try:
            if include:
                for field in include:
                    fields[field] = _{{ model.name }}_fields[field].copy()
            elif exclude:
                for field in exclude:
                    if field not in _{{ model.name }}_fields:
                        raise KeyError(field)

                fields = {
                    key: data.copy()
                    for key, data in _{{ model.name }}_fields.items()
                    if key not in exclude
                }
            else:
                fields = {
                    key: data.copy()
                    for key, data in _{{ model.name }}_fields.items()
                }

            if required:
                for field in required:
                    fields[field]['optional'] = False

            if optional:
                for field in optional:
                    fields[field]['optional'] = True

            {% if model.has_relational_fields %}
            if exclude_relational_fields:
                fields = {
                    key: data
                    for key, data in fields.items()
                    if key not in _{{ model.name }}_relational_fields
                }
            {% endif %}

            if relations:
                {% if model.has_relational_fields %}
                for field, type_ in relations.items():
                    if field not in _{{ model.name }}_relational_fields:
                        raise errors.UnknownRelationalFieldError('{{ model.name }}', field)

                    # TODO: this method of validating types is not ideal
                    # as it means we cannot two create partial types that
                    # reference each other
                    if type_ not in _created_partial_types:
                        raise ValueError(
                            f'Unknown partial type: "{type_}". '
                            f'Did you remember to generate the {type_} type before this one?'
                        )

                    # TODO: support non prisma.partials models
                    info = fields[field]
                    if info['is_list']:
                        info['type'] = f'List[\'partials.{type_}\']'
                    else:
                        info['type'] = f'\'partials.{type_}\''
                {% else %}
                raise ValueError('Model: "{{ model.name }}" has no relational fields.')
                {% endif %}
        except KeyError as exc:
            raise ValueError(
                f'{exc.args[0]} is not a valid {{ model.name }} / {name} field.'
            ) from None

        models = partial_models_ctx.get()

        # mypy does not like this as we are assigning a
        # Dict[Literal[str]] to a Dict[str] but this is fine
        models[name] = fields  # type: ignore[assignment]
        partial_models_ctx.set(models)
        _created_partial_types.add(name)


{% endfor %}

{% for model in dmmf.datamodel.models %}
{% if model.has_relational_fields -%}
    _{{ model.name }}_relational_fields: Set[str] = {
        {% for field in model.relational_fields %}
        '{{ field.name }}',
        {% endfor %}
    }
{% else -%}
    _{{ model.name }}_relational_fields: Set[str] = set()  # pyright: ignore[reportUnusedVariable]
{% endif %}
_{{ model.name }}_fields: Dict['types.{{ model.name }}Keys', PartialModelField] = OrderedDict(
    [
        {% for field in model.all_fields %}
        ('{{ field.name }}', {
            'name': '{{ field.name }}',
            'is_list': {{ field.is_list }},
            'optional': {{ field.is_optional }},
            'type': {{ field.python_type_as_string }},
            'is_relational': {{ field.relation_name is not none }},
            'documentation': {% if field.documentation is none %}None{% else %}'''{{ field.documentation }}'''{% endif %},
        }),
        {% endfor %}
    ],
)

{% endfor %}


# we have to import ourselves as relation types are namespaced to models
# e.g. models.Post
from . import models, actions

# required to support relationships between models
{% for model in dmmf.datamodel.models %}
{{ model.name }}.update_forward_refs()
{% endfor %}
