# pyright: reportInvalidTypeForm=false
# pylint: disable=too-many-lines
"""Pydantic classes for the data model."""

from datetime import datetime
from enum import Enum
from typing import (
    Annotated,
    Any,
    Callable,
    Collection,
    Iterator,
    Literal,
    Optional,
    Self,
    TypeVar,
    Union,
)
from uuid import UUID

import edtf  # type: ignore
from pydantic import (
    AnyUrl,
    ConfigDict,
    Field,
    StringConstraints,
    field_validator,
    model_validator,
)
from pydantic import (
    BaseModel as PydanticBaseModel,
)
from pydantic.main import IncEx

SiblingModel = TypeVar("SiblingModel", bound=PydanticBaseModel)
FieldValue = Union[str, int, "BaseModel"]
T = TypeVar("T")


class BaseModel(PydanticBaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    def model_dump(
        self,
        *,
        mode: Literal["json", "python"] | str = "python",
        include: IncEx | None = None,
        exclude: IncEx | None = None,
        context: Any | None = None,
        by_alias: bool | None = True,  # Overriding this
        exclude_unset: bool = True,  # Overriding this
        exclude_defaults: bool = True,  # Overriding this
        exclude_none: bool = True,  # Overriding this
        round_trip: bool = False,
        warnings: bool | Literal["none", "warn", "error"] = True,
        fallback: Callable[[Any], Any] | None = None,
        serialize_as_any: bool = False,
    ) -> dict[str, Any]:
        return super().model_dump(
            mode=mode,
            include=include,
            exclude=exclude,
            context=context,
            by_alias=by_alias,
            exclude_unset=exclude_unset,
            exclude_defaults=exclude_defaults,
            exclude_none=exclude_none,
            round_trip=round_trip,
            warnings=warnings,
            fallback=fallback,
            serialize_as_any=serialize_as_any,
        )

    def model_dump_json(
        self,
        *,
        indent: int | None = None,
        include: IncEx | None = None,
        exclude: IncEx | None = None,
        context: Any | None = None,
        by_alias: bool | None = True,  # Overriding this
        exclude_unset: bool = True,  # Overriding this
        exclude_defaults: bool = True,  # Overriding this
        exclude_none: bool = True,  # Overriding this
        round_trip: bool = False,
        warnings: bool | Literal["none", "warn", "error"] = True,
        fallback: Callable[[Any], Any] | None = None,
        serialize_as_any: bool = False,
    ) -> str:
        return super().model_dump_json(
            indent=indent,
            include=include,
            exclude=exclude,
            context=context,
            by_alias=by_alias,
            exclude_unset=exclude_unset,
            exclude_defaults=exclude_defaults,
            exclude_none=exclude_none,
            round_trip=round_trip,
            warnings=warnings,
            fallback=fallback,
            serialize_as_any=serialize_as_any,
        )

    # type: ignore
    def convert(self, cls: type[SiblingModel], **updated_data: Any) -> SiblingModel:
        data = self.model_dump() | updated_data
        return cls(**data)

    def deep_get(
        self, *names: str, cls: type[T], exclude: Collection[str] = tuple()
    ) -> Iterator[T]:
        for field_name in self.model_fields_set:
            if field_name in exclude:
                continue

            field_value: tuple[FieldValue] | FieldValue = getattr(self, field_name)
            field_value_tuple: tuple[FieldValue] = (
                field_value if isinstance(field_value, tuple) else (field_value,)
            )

            for obj in field_value_tuple:
                if isinstance(obj, cls) and (len(names) == 0 or field_name in names):
                    yield obj

                if isinstance(obj, BaseModel) and field_name not in exclude:
                    yield from obj.deep_get(*names, cls=cls, exclude=exclude)


NonEmptyStr = Annotated[
    str,
    StringConstraints(min_length=1),
]

Ark = Annotated[
    str,
    StringConstraints(pattern=r"^ark:/21198/([a-z]|[0-9])+"),
]


class ControlledTerm(BaseModel):
    id: NonEmptyStr
    label: NonEmptyStr


# generated by datamodel-codegen:
#   filename:  agent.schema.json
#   timestamp: 2025-05-28T17:21:15+00:00


class Iso(BaseModel):
    not_before: str
    not_after: str | None = None

    @field_validator("not_before", mode="after")
    @classmethod
    def is_date_string(cls, value: str) -> str:
        assert isinstance(edtf.parse_edtf(value), edtf.Date)
        return value

    @field_validator("not_after", mode="after")
    @classmethod
    def is_date_string_or_none(cls, value: str) -> str:
        assert value is None or isinstance(edtf.parse_edtf(value), edtf.Date)
        return value

    def years(self) -> range:
        start = int(edtf.parse_edtf(self.not_before).year)
        end = int(edtf.parse_edtf(self.not_after or self.not_before).year)
        return range(start, end + 1)


class Date(BaseModel):
    value: Optional[NonEmptyStr] = None
    iso: Iso


class RelatedConceptSource(Enum):
    HAF = "HAF"
    LoC = "LoC"
    Perseus = "Perseus"
    Pinakes = "Pinakes"
    Pleiades = "Pleiades"
    Syriaca = "Syriaca"
    VIAF = "VIAF"


class RelConItem(BaseModel):
    label: NonEmptyStr
    uri: AnyUrl
    source: RelatedConceptSource


class ReferenceNumberSource(Enum):
    CPG = "CPG"
    CPL = "CPL"
    TLG = "TLG"


class RefnoItem(BaseModel):
    label: NonEmptyStr
    idno: NonEmptyStr
    source: ReferenceNumberSource


class BibItem(BaseModel):
    id: UUID
    type: ControlledTerm
    shortcode: Optional[str] = (
        None  # TODO: required in JSON schema but not found in all data
    )
    citation: Optional[str] = (
        None  # TODO: required in JSON schema but not found in all data
    )
    range: Optional[NonEmptyStr] = None
    alt_shelf: Optional[NonEmptyStr] = None
    url: Optional[NonEmptyStr] = None
    note: tuple[str, ...] = tuple()


class RelItem(BaseModel):
    id: NonEmptyStr
    label: NonEmptyStr


class RelAgentItem(BaseModel):
    id: Ark = Field(..., description="A unique Archival Resource Key (ARK)")
    source: tuple[str, ...] = tuple()
    rel: tuple[RelItem, ...]


class RelPlaceItem(BaseModel):
    id: Ark = Field(..., description="A unique Archival Resource Key (ARK)")
    source: tuple[str, ...] = tuple()
    rel: tuple[RelItem, ...]


class CataloguerItem(BaseModel):
    message: NonEmptyStr
    contributor: Optional[NonEmptyStr] = Field(
        None,
        description="The person responsible for the intellectual contributions made to the record via this change",
    )
    added_by: NonEmptyStr = Field(
        ...,
        description="The user account responsible for editing the JSON record, likely supplied by the content management system",
    )
    timestamp: datetime


class Agent(BaseModel):
    ark: Ark
    type: ControlledTerm
    pref_name: NonEmptyStr
    alt_name: tuple[NonEmptyStr, ...] = tuple()
    desc: Optional[NonEmptyStr] = None
    gender: Optional[ControlledTerm] = None
    birth: Optional[Date] = None
    death: Optional[Date] = None
    floruit: Optional[Date] = None
    rel_con: tuple[RelConItem, ...] = tuple()
    refno: tuple[RefnoItem, ...] = tuple()
    bib: tuple[BibItem, ...] = tuple()
    note: tuple[str, ...] = tuple()
    rel_agent: tuple[RelAgentItem, ...] = tuple()
    rel_place: tuple[RelPlaceItem, ...] = tuple()
    internal: tuple[str, ...] = tuple()
    cataloguer: tuple[CataloguerItem, ...] = tuple()


# generated by datamodel-codegen:
#   filename:  work.schema.json
#   timestamp: 2025-05-28T17:23:18+00:00


class Creation(BaseModel):
    value: Optional[NonEmptyStr] = None
    iso: Iso


# TODO: not attested
class Incipit(BaseModel):
    value: NonEmptyStr
    translation: tuple[str, ...] = tuple()
    source: tuple[str, ...] = tuple()


# TODO not attested
class Explicit(BaseModel):
    value: NonEmptyStr
    translation: tuple[str, ...] = tuple()
    source: tuple[str, ...] = tuple()


# TODO not attested
class RelWorkItem(BaseModel):
    id: Ark
    source: tuple[str, ...] = tuple()
    rel: tuple[RelItem, ...]


class AssocNameItem(BaseModel):
    id: Optional[Ark] = None
    agent_record: Optional[Agent] = None
    value: Optional[NonEmptyStr] = None
    as_written: Optional[NonEmptyStr] = None
    role: ControlledTerm
    note: tuple[str, ...] = tuple()


class AssocNameItemMerged(AssocNameItem):
    agent_record: Optional[Agent] = None

    @model_validator(mode="after")
    def agent_record_loaded(self) -> Self:
        if self.id and not self.agent_record:
            raise ValueError("has agent id but agent_record not loaded")
        return self


class AssocNameItemUnmerged(AssocNameItem):
    agent_record: None = None


class ConceptualWork(BaseModel):
    ark: Ark
    pref_title: NonEmptyStr
    orig_lang: Optional[ControlledTerm] = None
    orig_lang_title: Optional[NonEmptyStr] = None
    alt_title: tuple[NonEmptyStr, ...] = tuple()
    desc: Optional[NonEmptyStr] = None
    genre: tuple[ControlledTerm, ...] = tuple()
    creator: tuple[AssocNameItemUnmerged, ...] | tuple[AssocNameItemMerged, ...] = (
        tuple()
    )
    creation: Optional[Creation] = None
    incipit: Optional[Incipit] = None
    explicit: Optional[Explicit] = None
    rel_con: tuple[RelConItem, ...] = tuple()
    refno: tuple[RefnoItem, ...] = tuple()
    bib: tuple[BibItem, ...] = tuple()
    note: tuple[str, ...] = tuple()
    rel_work: tuple[RelWorkItem, ...] = tuple()
    rel_agent: tuple[RelAgentItem, ...] = tuple()
    rel_place: tuple[RelPlaceItem, ...] = tuple()
    internal: tuple[str, ...] = tuple()
    cataloguer: tuple[CataloguerItem, ...] = tuple()


class ConceptualWorkUnmerged(ConceptualWork):
    creator: tuple[AssocNameItemUnmerged, ...] = tuple()


class ConceptualWorkMerged(ConceptualWork):
    creator: tuple[AssocNameItemMerged, ...] = tuple()


# generated by datamodel-codegen:
#   filename:  text_unit.schema.json
#   timestamp: 2025-05-28T17:22:58+00:00


class WorkBriefCreator(BaseModel):
    id: Ark
    agent_record: Agent


class WorkStub(BaseModel):
    id: Ark


class WorkBrief(BaseModel):
    desc_title: Optional[NonEmptyStr] = None
    creator: tuple[Ark, ...] | tuple[WorkBriefCreator, ...] = tuple()
    genre: tuple[ControlledTerm, ...] = tuple()


class WorkBriefUnmerged(WorkBrief):
    creator: tuple[Ark, ...] = tuple()


class WorkBriefMerged(WorkBrief):
    creator: tuple[WorkBriefCreator, ...] = tuple()


class ExcerptItem(BaseModel):
    type: ControlledTerm
    locus: Optional[NonEmptyStr] = None
    as_written: Optional[NonEmptyStr] = None
    translation: tuple[str, ...] = tuple()
    note: tuple[str, ...] = tuple()


class Contents(BaseModel):
    label: Optional[NonEmptyStr] = None
    work_id: Optional[Ark] = None
    locus: Optional[NonEmptyStr] = None
    note: tuple[str, ...] = tuple()

    @model_validator(mode="after")
    def has_label_or_work_id(self) -> Self:
        if not (self.label or self.work_id):
            raise ValueError("`Contents.label` and `.work_id` cannot both be None")
        return self


class ContentsUnmerged(Contents):
    pass


class ContentsMerged(Contents):
    pref_title: Optional[NonEmptyStr] = None


class WorkWitItem(BaseModel):
    work: WorkBrief | WorkStub | ConceptualWorkMerged
    alt_title: Optional[str] = Field(
        None,
        description="An alternative title providing witness-specific information about the work",
    )
    as_written: Optional[NonEmptyStr] = None
    locus: Optional[NonEmptyStr] = None
    excerpt: tuple[ExcerptItem, ...] = tuple()
    contents: tuple[ContentsUnmerged, ...] | tuple[ContentsMerged, ...] = tuple()
    note: tuple[str, ...] = tuple()
    bib: tuple[BibItem, ...] = tuple()


class WorkWitItemUnmerged(WorkWitItem):
    work: WorkBriefUnmerged | WorkStub
    contents: tuple[ContentsUnmerged, ...] = tuple()


class WorkWitItemMerged(WorkWitItem):
    work: WorkBriefMerged | ConceptualWorkMerged
    contents: tuple[ContentsMerged, ...] = tuple()


class ScriptItem(BaseModel):
    id: NonEmptyStr
    label: NonEmptyStr
    writing_system: str


class Place(BaseModel):
    ark: Ark
    pref_name: NonEmptyStr
    alt_name: tuple[NonEmptyStr, ...] = tuple()


class AssocPlaceItem(BaseModel):
    id: Optional[Ark] = None
    value: Optional[NonEmptyStr] = None
    as_written: Optional[NonEmptyStr] = None
    event: ControlledTerm
    note: tuple[str, ...] = tuple()


class AssocPlaceItemUnmerged(AssocPlaceItem):
    pass


class AssocPlaceItemMerged(AssocPlaceItem):
    place_record: Place | None = None

    @model_validator(mode="after")
    def has_place_record_if_possible(self) -> Self:
        if self.id and not self.place_record:
            raise ValueError(f"{self} has id {self.id} but no place_record loaded")
        return self


class AssocDateItem(BaseModel):
    value: NonEmptyStr
    iso: Optional[Iso] = None
    type: ControlledTerm
    as_written: Optional[NonEmptyStr] = None
    note: tuple[str, ...] = tuple()


class ParaItem(BaseModel):
    type: ControlledTerm
    subtype: tuple[ControlledTerm, ...] = tuple()
    locus: NonEmptyStr
    lang: tuple[ControlledTerm, ...] = Field(..., min_length=1)
    script: tuple[ScriptItem, ...] = tuple()
    label: Optional[NonEmptyStr] = None
    as_written: Optional[NonEmptyStr] = None
    translation: tuple[str, ...] = tuple()
    assoc_name: tuple[AssocNameItemUnmerged, ...] | tuple[AssocNameItemMerged, ...] = (
        tuple()
    )
    assoc_place: (
        tuple[AssocPlaceItemUnmerged, ...] | tuple[AssocPlaceItemMerged, ...]
    ) = tuple()
    assoc_date: tuple[AssocDateItem, ...] = tuple()
    note: tuple[str, ...] = tuple()


class ParaItemUnmerged(ParaItem):
    assoc_name: tuple[AssocNameItemUnmerged, ...] = tuple()
    assoc_place: tuple[AssocPlaceItemUnmerged, ...] = tuple()


class ParaItemMerged(ParaItem):
    assoc_name: tuple[AssocNameItemMerged, ...] = tuple()
    assoc_place: tuple[AssocPlaceItemMerged, ...] = tuple()


class NoteItem(BaseModel):
    type: ControlledTerm
    value: Optional[NonEmptyStr]


class DescProvenanceProgramItem(BaseModel):
    label: str
    description: str


class DescProvenance(BaseModel):
    program: tuple[DescProvenanceProgramItem, ...] = tuple()
    rights: Optional[str] = None


class TextUnit(BaseModel):
    ark: Ark
    reconstruction: bool
    label: NonEmptyStr
    summary: Optional[NonEmptyStr] = None
    locus: Optional[NonEmptyStr] = None
    lang: tuple[ControlledTerm, ...] = Field(..., min_length=1)
    work_wit: tuple[WorkWitItemUnmerged, ...] | tuple[WorkWitItemMerged, ...] = Field(
        ..., min_length=1
    )
    para: tuple[ParaItemUnmerged, ...] | tuple[ParaItemMerged, ...] = tuple()
    features: tuple[ControlledTerm, ...] = tuple()
    note: tuple[NoteItem, ...] = tuple()
    bib: tuple[BibItem, ...] = tuple()
    cataloguer: tuple[CataloguerItem, ...] = tuple()
    reconstructed_from: tuple[Ark, ...] = tuple()
    parent: tuple[Ark, ...] = Field(..., min_length=1)
    internal: tuple[str, ...] = tuple()
    desc_provenance: Optional[DescProvenance] = None


class TextUnitUnmerged(TextUnit):
    work_wit: tuple[WorkWitItemUnmerged, ...] = Field(..., min_length=1)
    para: tuple[ParaItemUnmerged, ...] = tuple()


class TextUnitMerged(TextUnit):
    work_wit: tuple[WorkWitItemMerged, ...] = Field(..., min_length=1)
    para: tuple[ParaItemMerged, ...] = tuple()


# generated by datamodel-codegen:
#   filename:  layer.schema.json
#   timestamp: 2025-05-28T17:21:54+00:00

# from __future__ import annotations

# from datetime import datetime
# from typing import List, Optional
# from uuid import UUID

# from pydantic import AnyUrl, BaseModel, Field, constr


class WritingItem(BaseModel):
    script: tuple[ScriptItem, ...] = Field(..., min_length=1)
    locus: Optional[NonEmptyStr] = None
    note: tuple[str, ...] = tuple()


class InkItem(BaseModel):
    locus: Optional[NonEmptyStr] = None
    color: tuple[str, ...] = tuple()
    note: tuple[str, ...] = tuple()


class LayoutItem(BaseModel):
    locus: Optional[NonEmptyStr] = None
    columns: Optional[str] = None
    lines: Optional[str] = None
    dim: Optional[NonEmptyStr] = Field(
        None,
        description="A string expression of an object's dimensions, whether manuscript block, folio, or writing area",
    )
    note: tuple[str, ...] = tuple()


class LayerTextUnit(BaseModel):
    id: Ark = Field(..., description="A unique Archival Resource Key (ARK)")
    label: NonEmptyStr
    locus: Optional[NonEmptyStr] = None


class LayerTextUnitUnmerged(LayerTextUnit):
    pass


class LayerTextUnitMerged(LayerTextUnit):
    text_unit_record: TextUnitMerged


class Ms(BaseModel):
    label: NonEmptyStr
    id: Optional[Ark] = None
    url: Optional[AnyUrl] = None


class RelatedMs(BaseModel):
    type: ControlledTerm
    label: NonEmptyStr
    note: tuple[str, ...] = tuple()
    mss: tuple[Ms, ...] = Field(..., min_length=1)


class InscribedLayer(BaseModel):
    ark: Ark
    reconstruction: bool
    state: ControlledTerm = Field(
        ..., description="The current state of the inscribed layer"
    )
    label: NonEmptyStr
    locus: Optional[NonEmptyStr] = None
    summary: Optional[NonEmptyStr] = None
    extent: Optional[NonEmptyStr] = Field(
        None,
        description="The extent, expressed in number of folios, which comprise the manuscript object",
    )
    writing: tuple[WritingItem, ...] = Field(..., min_length=1)
    ink: tuple[InkItem, ...] = tuple()
    layout: tuple[LayoutItem, ...] = tuple()
    text_unit: tuple[LayerTextUnitUnmerged, ...] | tuple[LayerTextUnitMerged, ...] = (
        Field(..., min_length=1)
    )
    para: tuple[ParaItemUnmerged, ...] | tuple[ParaItemMerged, ...] = tuple()
    assoc_date: tuple[AssocDateItem, ...] = tuple()
    assoc_name: tuple[AssocNameItemUnmerged, ...] | tuple[AssocNameItemMerged, ...] = (
        tuple()
    )
    assoc_place: (
        tuple[AssocPlaceItemUnmerged, ...] | tuple[AssocPlaceItemMerged, ...]
    ) = tuple()
    features: tuple[ControlledTerm, ...] = tuple()
    related_mss: tuple[RelatedMs, ...] = tuple()
    note: tuple[NoteItem, ...] = tuple()
    bib: tuple[BibItem, ...] = tuple()
    cataloguer: tuple[CataloguerItem, ...] = tuple()
    reconstructed_from: tuple[Ark, ...] = tuple()
    parent: tuple[Ark, ...] = Field(..., min_length=1)
    internal: tuple[str, ...] = tuple()
    desc_provenance: Optional[DescProvenance] = None


class InscribedLayerUnmerged(InscribedLayer):
    text_unit: tuple[LayerTextUnitUnmerged, ...] = Field(..., min_length=1)
    para: tuple[ParaItemUnmerged, ...] = tuple()
    assoc_name: tuple[AssocNameItemUnmerged, ...] = tuple()
    assoc_place: tuple[AssocPlaceItemUnmerged, ...] = tuple()


class InscribedLayerMerged(InscribedLayer):
    text_unit: tuple[LayerTextUnitMerged, ...] = Field(..., min_length=1)
    para: tuple[ParaItemMerged, ...] = tuple()
    assoc_name: tuple[AssocNameItemMerged, ...] = tuple()
    assoc_place: tuple[AssocPlaceItemMerged, ...] = tuple()


# generated by datamodel-codegen:
#   filename:  ms_obj.schema.json
#   timestamp: 2025-05-28T17:22:16+00:00

# from __future__ import annotations

# from datetime import datetime
# from enum import Enum
# from typing import List, Optional
# from uuid import UUID

# from pydantic import AnyUrl, BaseModel, Field, constr


class ManuscriptLayer(BaseModel):
    id: Ark
    label: NonEmptyStr
    type: ControlledTerm
    layer_record: Optional[InscribedLayerMerged] = None
    locus: Optional[NonEmptyStr] = None


class ManuscriptLayerUnmerged(ManuscriptLayer):
    layer_record: None = None


class ManuscriptLayerMerged(ManuscriptLayer):
    layer_record: InscribedLayerMerged


class UndertextManuscriptLayerMerged(ManuscriptLayer):
    id: Ark = Field(..., serialization_alias="uto_layer_ark")

    layer_record: None = None

    uto_ms_ark: Optional[Ark] = None

    script: tuple[str, ...] = tuple()
    lang: tuple[str, ...] = tuple()
    orig_date: tuple[AssocDateItem, ...] = tuple()

    @model_validator(mode="after")
    def undertext_type(self) -> Self:
        if self.type != ControlledTerm(id="undertext", label="Undertext"):
            raise ValueError(
                'UndertextManuscriptLayerMerged records must have type="undertext"'
            )
        return self


class Part(BaseModel):
    label: Optional[NonEmptyStr] = None
    summary: Optional[NonEmptyStr] = None
    locus: Optional[NonEmptyStr] = None
    support: tuple[ControlledTerm, ...] = Field(..., min_length=1)
    extent: Optional[NonEmptyStr] = Field(
        None,
        description="The extent, expressed in number of folios, which comprise the manuscript object",
    )
    dim: Optional[NonEmptyStr] = Field(
        None,
        description="A string expression of an object's dimensions, whether manuscript block, folio, or writing area",
    )
    layer: tuple[ManuscriptLayerUnmerged, ...] = Field(..., min_length=1)
    para: tuple[ParaItemUnmerged, ...] | tuple[ParaItemMerged, ...] = tuple()
    note: tuple[NoteItem, ...] = tuple()
    related_mss: tuple[RelatedMs, ...] = tuple()


class PartUnmerged(Part):
    para: tuple[ParaItemUnmerged, ...] = tuple()


class PartMerged(Part):
    layer: tuple[ManuscriptLayerUnmerged, ...] = Field(..., max_length=0, exclude=True)
    ot_layer: tuple[ManuscriptLayerMerged, ...] = Field(..., min_length=1)
    guest_layer: tuple[ManuscriptLayerMerged, ...] = tuple()
    uto: tuple[UndertextManuscriptLayerMerged, ...] = tuple()
    para: tuple[ParaItemMerged, ...] = tuple()


class LocationItem(BaseModel):
    id: NonEmptyStr
    collection: Optional[str] = None
    repository: NonEmptyStr


class ViscodexItem(BaseModel):
    type: ControlledTerm
    label: NonEmptyStr
    url: AnyUrl


class TextDirection(Enum):
    right_to_left = "right-to-left"
    top_to_bottom = "top-to-bottom"
    bottom_to_top = "bottom-to-top"


class Behavior(Enum):
    paged = "paged"
    individuals = "individuals"


class IiifItem(BaseModel):
    type: ControlledTerm
    manifest: AnyUrl
    text_direction: Optional[TextDirection] = None
    behavior: Optional[Behavior] = None
    thumbnail: Optional[AnyUrl] = None

    @field_validator("manifest", mode="before")
    @classmethod
    def remove_ingest_from_url(cls, value: AnyUrl) -> AnyUrl:
        return AnyUrl(
            str(value).replace("ingest.iiif.library.ucla.edu", "iiif.library.ucla.edu")
        )


# TODO only used in unattested class DigProvenance
class Image(BaseModel):
    camera_operator: tuple[str, ...] = tuple()
    imaging_date: Optional[str] = None
    delivery: Optional[str] = None
    msi_processing: tuple[str, ...] = Field(
        tuple(),
        description="Names of the person(s) responsible for processing the MSI data for this record",
    )
    program: Optional[str] = None
    condition_category: Optional[str] = None
    note: tuple[str, ...] = tuple()


# TODO only used in unattested class DigProvenance
class Description(BaseModel):
    program: str
    note: tuple[str, ...] = tuple()


# TODO class not attested in data, replace by DescProvenance and ImageProvenance?
class DigProvenance(BaseModel):
    images: tuple[Image, ...] = tuple()
    descriptions: tuple[Description, ...] = tuple()
    image_rights: NonEmptyStr
    metadata_rights: NonEmptyStr


# TODO attested in all data but only found in the export_test schema
class ImageProvenanceProgramItem(BaseModel):
    label: Optional[str] = None
    description: Optional[str] = None
    camera_operator: tuple[str, ...] = tuple()
    imaging_date: Optional[str] = None
    delivery: Optional[str] = None
    msi_processing: tuple[str, ...] = Field(
        tuple(),
        description="Names of the person(s) responsible for processing the MSI data for this record",
    )
    condition_category: Optional[str] = None
    note: tuple[str, ...] = tuple()

    # TODO: not found in schemas, attested in data
    imaging_system: Optional[str] = None


class ImageProvenance(BaseModel):
    program: tuple[ImageProvenanceProgramItem, ...] = tuple()
    rights: Optional[NonEmptyStr] = (
        None  # TODO schema defines as not optional but gives None as default
    )


class ReconstructedFrom(BaseModel):
    id: Ark
    shelfmark: NonEmptyStr


class ManuscriptObject(BaseModel):
    ark: Ark = Field(..., description="A unique Archival Resource Key (ARK)")
    reconstruction: bool
    type: ControlledTerm
    shelfmark: NonEmptyStr
    summary: Optional[NonEmptyStr] = None
    extent: Optional[NonEmptyStr] = Field(
        None,
        description="The extent, expressed in number of folios, which comprise the manuscript object",
    )
    weight: Optional[NonEmptyStr] = Field(
        None, description="A string expression of the manuscript object's weight"
    )
    dim: Optional[NonEmptyStr] = Field(
        None,
        description="A string expression of an object's dimensions, whether manuscript block, folio, or writing area",
    )
    state: ControlledTerm = Field(
        ..., description="The current state of the manuscript object"
    )
    fol: Optional[NonEmptyStr] = Field(
        None,
        description="A string expressing the foliation of the object in a semi-controlled format",
    )
    coll: Optional[NonEmptyStr] = Field(
        None,
        description="A string expressing the collation of the object following a semi-controlled format, such as a collational formula",
    )
    features: tuple[ControlledTerm, ...] = tuple()
    part: tuple[Part, ...] | tuple[PartUnmerged, ...] | tuple[PartMerged, ...] = Field(
        ..., min_length=1
    )
    layer: tuple[ManuscriptLayerUnmerged, ...] = tuple()
    para: tuple[ParaItemUnmerged, ...] | tuple[ParaItemMerged, ...] = tuple()
    location: tuple[LocationItem, ...]
    assoc_date: tuple[AssocDateItem, ...] = tuple()
    assoc_name: tuple[AssocNameItemMerged, ...] | tuple[AssocNameItemUnmerged, ...] = (
        tuple()
    )
    assoc_place: (
        tuple[AssocPlaceItemUnmerged, ...] | tuple[AssocPlaceItemMerged, ...]
    ) = tuple()
    note: tuple[NoteItem, ...] = tuple()
    related_mss: tuple[RelatedMs, ...] = tuple()
    viscodex: tuple[ViscodexItem, ...] = tuple()
    bib: tuple[BibItem, ...] = tuple()
    iiif: tuple[IiifItem, ...] = tuple()
    internal: tuple[str, ...] = tuple()
    cataloguer: tuple[CataloguerItem, ...] = tuple()
    reconstructed_from: tuple[Ark, ...] | tuple[ReconstructedFrom, ...] = tuple()

    # in regular schemas but not export_test schema, unattested in data
    # think this got replaced with desc_provenance and/or image_provenance
    dig_provenance: Optional[DigProvenance] = None

    # found in export_test schema but not other schemas, seems to be used in all data
    desc_provenance: Optional[DescProvenance] = None
    image_provenance: Optional[ImageProvenance] = None

    # not in the schemas, found in the export_test data but not elsewhere
    has_bind: Optional[bool] = None


class ManuscriptObjectUnmerged(ManuscriptObject):
    part: tuple[PartUnmerged, ...]
    assoc_name: tuple[AssocNameItemUnmerged, ...] = tuple()
    assoc_place: tuple[AssocPlaceItemUnmerged, ...] = tuple()
    para: tuple[ParaItemUnmerged, ...] = tuple()
    reconstructed_from: tuple[Ark, ...] = tuple()


class ManuscriptObjectMerged(ManuscriptObject):
    part: tuple[PartMerged, ...]

    layer: tuple[ManuscriptLayerUnmerged, ...] = Field(
        tuple(), max_length=0, exclude=True
    )
    ot_layer: tuple[ManuscriptLayerMerged, ...] = tuple()
    guest_layer: tuple[ManuscriptLayerMerged, ...] = tuple()
    uto: tuple[UndertextManuscriptLayerMerged, ...] = tuple()

    assoc_name: tuple[AssocNameItemMerged, ...] = tuple()
    assoc_place: tuple[AssocPlaceItemMerged, ...] = tuple()

    para: tuple[ParaItemMerged, ...] = tuple()
    reconstructed_from: tuple[ReconstructedFrom, ...] = tuple()
