{%- macro validations(fields) %}
{%- for validation in ("datetime", "drop", "emails", "orcids") %}
{%- set fields = fields|selectattr("validation", "eq", validation)|list -%}
{% if fields %}

    @field_validator({{ fields|map("attr", "scicat_name")|map("quote")|join(", ") }}, mode="before")
    def _validate_{{ validation }}(cls, value: Any) -> Any:
        return validate_{{ validation }}(value)
{%- endif %}
{%- endfor %}
{% endmacro -%}

{% macro mask_keyword(spec, kind) %}
{% if kind == "download" and spec.masked_fields_download %}
, masked=({{ spec.masked_fields_download|map("quote")|join(", ") }})
{% endif %}
{% endmacro %}

{% macro field_default(field) %}
{%- if not field.required -%}
 = None
{%- endif -%}
{% endmacro %}

{% macro model_field(field, kind) %}
{%- if field.scicat_name.startswith("_") %}
    {{ field.scicat_name[1:] }}: {{ field.full_type_for(kind) }} = pydantic.Field(alias={{ field.scicat_name|quote }}{% if not field.required or kind == "download" %}, default=None{% endif -%})
{%- else %}
    {{ field.scicat_name }}: {{ field.full_type_for(kind) }}{% if not field.required or kind == "download" %} = None{% endif -%}
{%- endif -%}
{% endmacro %}

{{ banner }}
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 SciCat Project (https://github.com/SciCatProject/scitacean)
"""Models for communication with SciCat and user facing dataclasses.

The high-level :class:`scitacean.Client` and :class:`scitacean.Dataset` return objects
from the SciCat database in the form of 'user models'.
Those are usually all that is required for working with Scitacean.

At a lower level, those models are converted to or from upload or download models,
respectively, by the corresponding methods of the user models.
These upload and download models represent SciCat's schemas more closely and are used
by the lower-level :class:`scitacean.client.ScicatClient`
and :class:`scitacean.testing.client.FakeClient`.

.. rubric:: User models

Dataclasses exposed to users, primarily through :class:`Dataset`.

.. autosummary::
  :toctree: ../classes
  :template: scitacean-class-template.rst
{% set model_names = ["DatasetType"]
                   + specs.values()|map(attribute="name")|reject("in", ["DataFile", "Datablock", "OrigDatablock"])|list -%}
{% for name in model_names|sort %}
  {{ name }}
{%- endfor %}

.. rubric:: Download models

Pydantic models for the data received from SciCat in downloads.

.. autosummary::
  :toctree: ../classes
{% set model_names = ["DownloadDataset"]
                   + (specs.values()|map(attribute="download_name")|list) -%}
{% for name in model_names|sort %}
  {{ name }}
{%- endfor %}

.. rubric:: Upload models

Pydantic models sent to SciCat in uploads.

.. autosummary::
  :toctree: ../classes
{% set model_names = ["UploadRawDataset", "UploadDerivedDataset"]
                   + (specs.values()|map(attribute="upload_name")|select|list) -%}
{% for name in model_names|sort %}
  {{ name }}
{%- endfor %}

.. rubric:: Functions

.. autosummary::
   :toctree: ../functions

   construct
"""

from __future__ import annotations

from datetime import datetime
from typing import Any, Dict, List, Optional, Type

import pydantic
from pydantic import NonNegativeInt

from ._base_model import (
    BaseModel,
    BaseUserModel,
    DatasetType,
    construct,
    validate_datetime,
    validate_drop,
    validate_emails,
    validate_orcids,
)
from ._internal.dataclass_wrapper import dataclass_optional_args
from ._internal.pydantic_compat import field_validator
from .filesystem import RemotePath
from .pid import PID
from .thumbnail import Thumbnail

{% set fields = dset_spec.fields_for("download") -%}
class DownloadDataset(BaseModel{{ mask_keyword(dset_spec, "download") }}):
{%- for field in fields -%}
{{ model_field(field, "download") }}
{%- endfor %}
{{- validations(fields) }}

{% for dset_type in ("derived", "raw") %}
{%- set fields = dset_spec.dset_fields_for("upload", dset_type) -%}
class Upload{{ dset_type|capitalize }}Dataset(BaseModel{{ mask_keyword(dset_spec, "upload") }}):
{%- for field in fields -%}
{{ model_field(field, "upload") }}
{%- endfor %}
{{- validations(fields) }}

{% endfor -%}

{% for spec in specs.values() -%}
{% for name, kind in ((spec.download_name, "download"), (spec.upload_name, "upload")) %}
{%- set fields = spec.fields_for(kind) -%}
{%- if fields -%}
class {{ name }}(BaseModel{{ mask_keyword(spec, kind) }}):
{% for field in fields -%}
{{ model_field(field, kind) }}
{%- endfor %}
{{- validations(fields) }}

{%- if spec.name not in ["DataFile", "Datablock", "OrigDatablock"] %}
    @classmethod
    def user_model_type(cls) -> Type[{{ spec.name }}]:
        return {{ spec.name }}
{%- endif %}

{%- if kind == "download" and spec.upload_name %}
    @classmethod
    def upload_model_type(cls) -> Type[{{ spec.upload_name }}]:
        return {{ spec.upload_name }}
{%- endif %}

{%- if kind == "upload" %}
    @classmethod
    def download_model_type(cls) -> Type[{{ spec.download_name }}]:
        return {{ spec.download_name }}
{%- endif %}

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

{% for spec in specs.values()|rejectattr("name", "in", ["DataFile", "Datablock", "OrigDatablock"]) -%}
@dataclass_optional_args(kw_only=True, slots=True)
class {{ spec.name }}(BaseUserModel):
{% set fields = spec.fields_for("user")|sort(attribute="upload", reverse=True) -%}
{% for field in fields %}
    {% if field.upload %}{{ field.name }}{% else %}_{{ field.name }}{% endif %}: {{ field.full_type_for("user") }}{{ field_default(field) }}
{%- endfor %}
{% for field in fields|rejectattr("upload") %}
    @property
    def {{ field.name }}(self) -> {{ field.full_type_for("user") }}:
        return self._{{ field.name }}
{% endfor %}
    @classmethod
    def from_download_model(cls, download_model: {{ spec.download_name }}) -> {{ spec.name }}:
        """Construct an instance from an associated SciCat download model."""
        return cls(**cls._download_model_dict(download_model))
{% if spec.upload_name %}
    def make_upload_model(self) -> {{ spec.upload_name }}:
        """Construct a SciCat upload model from self."""
        return {{ spec.upload_name }}(**self._upload_model_dict())
{% endif %}

{%- if spec.upload_name %}
    @classmethod
    def upload_model_type(cls) -> Type[{{ spec.upload_name }}]:
        return {{ spec.upload_name }}
{%- endif %}

    @classmethod
    def download_model_type(cls) -> Type[{{ spec.download_name }}]:
        return {{ spec.download_name }}

{% endfor -%}

# Some models contain fields that are other models which are defined
# further down in the file.
# Instead of ordering models according to their dependencies, resolve
# references once all classes have been defined.
{% for spec in specs.values() -%}
{%- for name in (spec.download_name, spec.upload_name) -%}
{%- if name -%}
{{ name }}.model_rebuild()
{% endif -%}
{%- endfor -%}
{%- endfor -%}
DownloadDataset.model_rebuild()
UploadDerivedDataset.model_rebuild()
UploadRawDataset.model_rebuild()

__all__ = (
    "BaseModel",
    "BaseUserModel",
    "DatasetType",
    "construct",
    "DownloadDataset",
    "UploadDerivedDataset",
    "UploadRawDataset",
{% for spec in specs.values() -%}
{%- for name in (spec.download_name, spec.upload_name) -%}
{%- if name -%}
    "{{ name }}",
{% endif -%}
{%- endfor -%}
{%- endfor -%}
{% for spec in specs.values()|rejectattr("name", "in", ["DataFile", "Datablock", "OrigDatablock"]) -%}
    "{{ spec.name }}",
{%- endfor -%}
)
