from datetime import datetime as datetime
from typing import Optional

from sqlmodel import Field, ForeignKeyConstraint, SQLModel

from . import id as idg
from ._timestamps import CreatedAt, UpdatedAt
from ._users import CreatedBy
from .type import usage as usage_type


class user(SQLModel, table=True):  # type: ignore
    """User accounts.

    All data in this table is synched from the cloud user account to ensure a
    globally unique user identity.
    """

    id: str = Field(primary_key=True)
    """User ID. Typically 8-character base64."""  # noqa
    email: str = Field(index=True, unique=True)
    """Primary user email used for logging in."""
    handle: str = Field(nullable=False, index=True, unique=True)
    """User handle."""
    name: Optional[str] = Field(index=True)
    """Long display name."""
    created_at: datetime = CreatedAt
    updated_at: Optional[datetime] = UpdatedAt


class storage(SQLModel, table=True):  # type: ignore
    """Storage locations.

    A dobject or dtransform-associated file can be stored in any desired S3,
    GCP, Azure or local storage location. This table tracks these locations
    along with metadata.
    """

    id: str = Field(default_factory=idg.storage, primary_key=True)
    """Universal base62 ID, generated by :func:`~lnschema_core.id.storage`."""
    root: str = Field(index=True)
    """Semantic identifier to the root of the storage location, like an s3 path, a local path, etc."""  # noqa
    type: Optional[str] = None
    """Local vs. s3 vs. gcp etc."""
    region: Optional[str] = None
    """Cloud storage region if applicable."""
    created_at: datetime = CreatedAt
    updated_at: Optional[datetime] = UpdatedAt


class dobject(SQLModel, table=True):  # type: ignore
    """Data objects in storage & memory.

    Data objects (`dobjects`) always represent a dataset, a set of jointly measured
    observations of variables (features).

    A `dobject` might contain a single observation, for instance, a single image.

    Datasets typically have canonical on-disk and in-memory representations. If
    choices among these representations are made, a one-to-one mapping can be
    achieved, which means that any given `dobject` has a default in-memory and
    on-disk representation.

    LaminDB offers meaningful default representations. For instance,

    - It defaults to pandas DataFrames for in-memory representation of tables
      and allows you to configure loading tables into polars DataFrames.
    - It defaults to the `.parquet` format for tables, but allows you to
      configure `.csv` or `.ipc`.

    Some datasets do not have a canonical in-memory representation, for
    instance, `.fastq`, `.vcf`, or files describing QC of datasets.

    Examples for storage ⟷ memory correspondence:

    - Table: `.csv`, `.tsv`, `.parquet`, `.ipc` (`.feather`) ⟷
      `pandas.DataFrame`, `polars.DataFrame`
    - Annotated matrix: `.h5ad`, `.h5mu`, `.zarrad` ⟷ `anndata.AnnData`,
      `mudata.MuData`
    - Image: `.jpg`, `.png` ⟷ `np.ndarray`, or a dedicated imaging in-memory
      container
    - Tensor: zarr directory, TileDB store ⟷ zarr loader, TileDB loader
    - Fastq: `.fastq` ⟷ /
    - VCF: `.vcf` ⟷ /
    - QC: `.html` ⟷ /
    """

    id: str = Field(default_factory=idg.dobject, primary_key=True)
    """Universal base62 ID, generated by :func:`~lnschema_core.id.dobject`."""
    name: Optional[str] = Field(index=True)
    """Semantic name or title. Defaults to `None`."""
    suffix: Optional[str] = Field(default=None, index=True)
    """Suffix to construct the storage key. Defaults to `None`.

    This is a file extension if the `dobject` is stored in a file format.
    It's `None` if the storage format doesn't have a canonical extension.
    """
    size: Optional[float] = Field(default=None, index=True)
    """Size in bytes.

    Examples: 1KB is 1e3 bytes, 1MB is 1e6, 1GB is 1e9, 1TB is 1e12 etc.
    """
    dtransform_id: str = Field(foreign_key="dtransform.id", index=True)
    """Link to :class:`~lnschema_core.dtransform` that generated the `dobject`."""
    storage_id: str = Field(foreign_key="storage.id", index=True)
    """Link to :class:`~lnschema_core.storage` location that stores the `dobject`."""
    checksum: Optional[str] = Field(default=None, index=True)
    """Checksum of file (md5)."""
    created_at: datetime = CreatedAt
    """Time of creation."""
    updated_at: Optional[datetime] = UpdatedAt
    """Time of last update."""


class dtransform(SQLModel, table=True):  # type: ignore
    """Data transformations.

    A data transformation (`dtransform`) is _any_ transformation of a `dobject`.
    For instance:

    - Jupyter notebooks (`jupynb`)
    - Pipeline runs of software (workflows) and scripts (`pipeline_run`).
    - Physical instruments making measurements (needs to be configured).
    - Human decisions based on data visualizations (needs to be configured).

    It typically has inputs and outputs:

    - References to outputs are stored in the `dobject` table in the
      `dtransform_id` column, which stores a foreign key into the `dtransform`
      table. This is possible as every given `dobject` has a unique data source:
      the `dtransform` that produced the `dobject`. Note that a given
      `dtransform` may output several `dobjects`.
    - References to input `dobjects` are stored in the `dtransform_in` table, a
      many-to-many link table between the `dobject` and `dtransform` tables. Any
      `dobject` might serve as an input for many `dtransform`. Similarly, any
      `dtransform` might have many `dobjects` as inputs.
    """

    __table_args__ = (
        ForeignKeyConstraint(
            ["jupynb_id", "jupynb_v"],
            ["jupynb.id", "jupynb.v"],
            name="dtransform_jupynb",
        ),
    )
    id: str = Field(default_factory=idg.dtransform, primary_key=True)
    """Universal base62 ID & primary key, generated through :func:`~lnschema_core.id.dtransform`."""  # noqa
    jupynb_id: Optional[str] = Field(default=None, index=True)
    """Link to :class:`~lnschema_core.jupynb` that mediated the data transformation."""
    jupynb_v: Optional[str] = Field(default=None, index=True)
    """Second part of composite primary key to link to :class:`~lnschema_core.jupynb`."""  # noqa
    pipeline_run_id: Optional[str] = Field(
        default=None, foreign_key="pipeline_run.id", index=True
    )
    """Link to :class:`~lnschema_core.pipeline_run` that mediated the data transformation."""  # noqa


class dtransform_in(SQLModel, table=True):  # type: ignore
    """Input data for data transformations.

    This is a many-to-many link table for `dtransform` and `dobject` storing the
    inputs of data transformations.

    A data transformation can have an arbitrary number of data objects as inputs.

    - The same `dobject` can be used as input in many different `dtransforms`.
    - One `dtransform` can have several `dobjects` as inputs.
    """

    dtransform_id: str = Field(foreign_key="dtransform.id", primary_key=True)
    """Link to :class:`~lnschema_core.dtransform`."""
    dobject_id: str = Field(foreign_key="dobject.id", primary_key=True)
    """Link to :class:`~lnschema_core.dobject`."""


class jupynb(SQLModel, table=True):  # type: ignore
    """Jupyter notebooks.

    Jupyter notebooks (`jupynbs`) represent one type of data transformation
    (`dtransform`) and have a unique correspondence in `dtransform`.

    IDs for Jupyter notebooks are generated through nbproject.
    """

    id: str = Field(default=None, primary_key=True)
    """Universal base62 ID & primary key, generated by :func:`~lnschema_core.id.jupynb`."""  # noqa
    v: str = Field(default="1", primary_key=True)
    """Version identifier, defaults to `"1"`.

    Use this to label different versions of the same notebook.

    Consider using `semantic versioning <https://semver.org>`__
    with `Python versioning <https://peps.python.org/pep-0440/>`__.
    """
    name: str = Field(
        index=True
    )  # notebooks that are committed should always have a title = name  # noqa
    """Title of the notebook as generated by `nbproject.meta.title
    <https://lamin.ai/docs/nbproject/nbproject.dev.metalive#nbproject.dev.MetaLive.title>`__.
    """
    created_by: str = CreatedBy
    """Auto-populated link to :class:`~lnschema_core.user`."""
    created_at: datetime = CreatedAt
    """Time of creation."""
    updated_at: Optional[datetime] = UpdatedAt
    """Time of last update."""


class pipeline(SQLModel, table=True):  # type: ignore
    """Pipelines.

    A pipeline is typically versioned software that can perform a data
    transformation/processing workflow. This can be anything from typical
    workflow tools (Nextflow, Snakemake, Prefect, Apache Airflow, etc.) to
    simple (versioned) scripts.
    """

    id: str = Field(default_factory=idg.pipeline, primary_key=True)
    v: str = Field(default="1", primary_key=True)
    name: Optional[str] = Field(default=None, index=True)
    reference: Optional[str] = Field(default=None, index=True)
    created_by: str = CreatedBy
    """Auto-populated link to :class:`~lnschema_core.user`."""
    created_at: datetime = CreatedAt
    """Auto-populated time stamp."""
    updated_at: Optional[datetime] = UpdatedAt


class pipeline_run(SQLModel, table=True):  # type: ignore
    """Pipeline runs.

    Pipeline runs represent one type of data transformation (`dtransform`) and
    have a unique correspondence in `dtransform`.

    For instance, `lnbfx` stores references to bioinformatics workflow runs by
    linking to entries in this table.
    """

    __table_args__ = (
        ForeignKeyConstraint(
            ["pipeline_id", "pipeline_v"],
            ["pipeline.id", "pipeline.v"],
            name="pipeline",
        ),
    )
    id: Optional[str] = Field(default_factory=idg.pipeline_run, primary_key=True)
    pipeline_id: str = Field(index=True)
    pipeline_v: str = Field(index=True)
    name: Optional[str] = Field(default=None, index=True)
    created_by: str = CreatedBy
    """Auto-populated link to :class:`~lnschema_core.user`."""
    created_at: datetime = CreatedAt


class usage(SQLModel, table=True):  # type: ignore
    """Data usage log.

    Any API call in the `lamindb.db` API is logged here.
    """

    id: str = Field(default_factory=idg.usage, primary_key=True)
    """Universal base62 ID & primary key, generated by :func:`~lnschema_core.id.usage`."""  # noqa
    type: usage_type = Field(nullable=False, index=True)
    """Usage type."""
    user_id: str = Field(foreign_key="user.id", nullable=False, index=True)
    """Link to :class:`~lnschema_core.user`."""
    time: datetime = CreatedAt
    """Time of event."""
    dobject_id: str = Field(foreign_key="dobject.id", index=True)
    """Link to the affected :class:`~lnschema_core.dobject`."""


class version_yvzi(SQLModel, table=True):  # type: ignore
    """Core schema module versions deployed in a given instance.

    Migrations of the schema module add rows to this table, storing the schema
    module version to which we migrated along with the user who performed the
    migration.
    """

    v: Optional[str] = Field(primary_key=True)
    """Python package version of `lnschema_core`."""
    migration: Optional[str] = None
    """Migration script reference of the latest migration leading up to the Python package version."""  # noqa
    user_id: str = CreatedBy
    """Link to :class:`~lnschema_core.user`."""
    created_at: datetime = CreatedAt
    """Time of creation."""


class migration_yvzi(SQLModel, table=True):  # type: ignore
    """Latest migration.

    This stores the reference to the latest migration script deployed.
    """

    version_num: Optional[str] = Field(primary_key=True)
    """Reference to the last-run migration script."""
