"""
DataProducts.Reference metadata model.

The primary class is `Product`, which defines the common metadata for a data
product.
"""

import uuid
from enum import StrEnum, auto, nonmember
from typing import ClassVar, Self

from astropydantic import AstroPydanticTime
from pydantic import AnyUrl, EmailStr, model_validator

from ..._core import AstroField, ModelBase, doc, enum_doc
from ...version import __version__
from . import ident
from .acquisition import Acquisition
from .observation import Observation
from .sdc import ScienceDataChallenge

_PRODUCT_NAMESPACE = "CTAO.DataProducts"
_NAMESPACE = "CTAO.DataProducts.Reference"
EXAMPLE_TIMES = ["2023-10-15 05:45:12.21", "2023-10-15T05:45:12"]

__all__ = [
    "Product",
    "Contact",
    "DataModelName",
    "DataModel",
    "Software",
    "ObservatoryProcess",
    "Activity",
    "ExternalDataProduct",
    "Curation",
]


class Contact(ModelBase):
    """Contact information for this data product."""

    _namespace: ClassVar[str] = _NAMESPACE

    name: str = AstroField(
        description="Contact name for this data product.", fits_keyword="AUTHOR"
    )
    organization: str = AstroField(
        description="Contact organization name of this data product.",
        fits_keyword="ORIGIN",  # fits v4
    )
    email: EmailStr = AstroField(
        description="Contact's email address",
        fits_keyword="EMAIL",  # nonstandard
        ucd="meta.curation;meta.email",
    )


class DataModelName(StrEnum):
    """Allowed data models/format specifications."""

    _namespace = nonmember(_NAMESPACE)

    GADF = auto()  #: GADF
    VODF = auto()  #: VODF
    CTAO = auto()  #: CTAO internal model


class DataModel(ModelBase):
    """Describes the data model and/or format of the data product."""

    _namespace: ClassVar[str] = _NAMESPACE

    name: DataModelName | str = AstroField(
        description=(
            "Name of the overall data model, which may contain multiple data product"
            " specifications. For CTAO models/formats, this should be the fully"
            " qualified name of the data model as defined by the top-level model."
        ),
        fits_keyword="MODEL",
    )
    version: str = AstroField(
        description="Version number of the data model",
        fits_keyword="MODELVER",
    )
    url: AnyUrl | None = AstroField(
        description="URL or DOI linking to more detail.",
        fits_keyword="MODELURL",
    )


class Software(ModelBase):
    """Describes the software package that created the data product."""

    _namespace: ClassVar[str] = _NAMESPACE

    name: str = AstroField(
        description="Descriptive name of the software.",
        examples=["datapipe"],
        fits_keyword="SOFTWARE",
        ucd="meta.id;meta.software",
    )
    version: str = AstroField(
        description="Version number",
        examples=["v1.0.1"],
        fits_keyword="SOFTVER",
        ucd="meta.version;meta.software",
    )
    url: AnyUrl | None = AstroField(
        description="URL or DOI linking to more detail.",
        fits_keyword="SOFTURL",
        ucd="meta.ref.url;meta.software",
    )


class ObservatoryProcess(StrEnum):
    """Observatory operational Process.

    These are the top-level processes
    """

    _namespace = nonmember(_NAMESPACE)

    PROPOSAL_HANDLING = auto()  #: Proposal handling
    OBSERVATION_PLANNING = auto()  #: Observation planning
    ACQUISITION = auto()  #: Data Acquistion
    SIMULATION = auto()  #: Simulation
    DATA_PROCESSING = auto()  #: Data Processing
    SCIENCE_DATA_CHALLENGE = auto()  #: Science Data Challenge
    SCIENCE_ANALYSIS = auto()  #: High-level Science Analysis (DL4-DL6)
    INSTRUMENT = auto()  #: Instrumental engineering or maintenance related activity

    USER = auto()  #: Process was not an observatory activity


class ExternalDataProduct(ModelBase):
    """Another data product used as input to the Activity."""

    _namespace: ClassVar[str] = _NAMESPACE

    uri: AnyUrl = AstroField(
        description=(
            "URI of the data product, can be a https address, DOI, "
            "Bulk Archive LFN, or a file:// if only known locally."
        ),
        examples=[
            "https://doi.org/10.5281/zenodo.6218687",
            "file://myfile.fits.gz",
            "lfn://ctao/dpps/",
        ],
        ucd="meta.ref.uri;meta.file",
    )
    role: str = AstroField(
        description="context of the data product",
        examples=["reconstruction-model", "configuration", "camera-calibration"],
    )
    id: uuid.UUID = AstroField(
        default=None,
        description="Unique ``instance.id`` of the data product, if known.",
        ucd="meta.id;meta.file",
    )


class Activity(ModelBase):
    """The activity (software or human) that produced this data product."""

    _namespace: ClassVar[str] = _NAMESPACE

    process: ObservatoryProcess | None = AstroField(
        doc(ObservatoryProcess),
        default=ObservatoryProcess.USER,
        fits_keyword="ACTPROC",
    )
    name: str = AstroField(
        description="Name of the activity that produced this data product.",
        examples=["observation data processing", "lab calibration"],
        fits_keyword="ACTIVITY",
    )
    description: str | None = AstroField(
        description=(
            "Human-readable details of the activity, if more specificity than the "
            "`name` is required."
        ),
        default=None,
        fits_keyword="ACTDESC",
    )
    id: uuid.UUID = AstroField(
        description=(
            "Unique identifier of this process, which is used to link multiple "
            "data products produced together.  If this is a human process, this can "
            "be generated with the `uuidgen` linux command for example."
        ),
        fits_keyword="ACTID",
        ucd="meta.id.parent",
    )
    start: AstroPydanticTime = AstroField(
        description="Start time of the activity", fits_keyword="ACTSTART"
    )
    end: AstroPydanticTime | None = AstroField(
        description=(
            "End time of the activity, which is generally not known when the data"
            " product is being written and is therefore optional. "
        ),
        default=None,
        fits_keyword="ACTSTOP",
    )
    software: Software | None = AstroField(
        description="if the activity involved software, describes it.",
        default=None,
    )

    configuration_id: str = AstroField(
        description=(
            "Identifier for the configuration for the software used to produce this"
            " data product. This is normally a unique identifier of the configuration"
            " used to produce this data product, It should be possible for a user"
            " knowing this identifier and details of how and where configurations are"
            " stored, to find the full original configuration. It is recommended to use"
            " a URI when possible, particularly for REST API endpoints or  git"
            " repositories."
        ),
        fits_keyword="ANAMODE",
        ivoa_keyword="analysis_mode",
        examples=[
            "dl3/hillas-standard/v1",
            "dl5/quicklook-followup/v1",
            "https://myobservatory.org/API/v1/config/data-processing/standard-v1.2",
        ],
    )

    inputs: list[ExternalDataProduct] | None = AstroField(
        default=None,
        description=(
            "Describes the input data products to the activity that produced this data"
            " product."
        ),
    )


class DataRights(StrEnum):
    """Availability of the data product, if known at time of creation."""

    _namespace = nonmember(_NAMESPACE)
    PUBLIC = auto()
    SECURE = auto()
    PROPRIETARY = auto()


class Curation(ModelBase):
    """Information related to the release and usage of data products."""

    _namespace: ClassVar[str] = _NAMESPACE
    release: str | None = AstroField(
        description=(
            "Name of the data release (data collection) that this data product"
            " belongs to."
        ),
        default=None,
        fits_keyword="RELEASE",
        ivoa_keyword="obs_collection",
        examples=["CTAO/DL3-DR1", "CTAO/DL3-SDC1"],
        ucd="meta.dataset;meta.curation",
    )

    reference: AnyUrl | None = AstroField(
        description=(
            "a reference to where the data product is published. It is recommended that"
            " either the 19-digit bibliographic identifier used in the Astrophysics"
            " Data System bibliographic databases (http://adswww.harvard.edu/) or the"
            " Digital Object Identifier (http://doi.org) be included in the value"
            " string, when available."
        ),
        default=None,
        examples=["1994A&AS..103..135A", "doi:10.1006/jmbi.1998.2354"],
        fits_keyword="REFERENC",
        ucd="meta.ref;meta.curation",
    )

    license: str = AstroField(
        description="License for this data product",
        default="CC-BY-SA-4.0",
        examples=["CC-BY-SA-4.0"],
        fits_keyword="LICENSE",
    )
    license_url: str = AstroField(
        description=(
            "URL pointing to the details of the license described by ``license``"
        ),
        default="https://creativecommons.org/licenses/by-sa/4.0/",
        examples=["https://creativecommons.org/licenses/by-sa/4.0/"],
        fits_keyword="LICENURL",
    )
    copyright: str | None = AstroField(
        description="Copyright holder(s) of this data product",
        examples=["CTAO 2025"],
        default="CTAO 2025",
        fits_keyword="COPYRIGH",
    )
    rights: DataRights | None = AstroField(
        enum_doc(DataRights),
        default=None,
        ivoa_keyword="data_rights",
        fits_keyword="RIGHTS",
    )
    release_date: AstroPydanticTime | None = AstroField(
        "Date of the public release of this data product, if different from the"
        " creation_time, e.g. if there is a proprietary period this may be set to the"
        " date when the data product is no longer private.",
        default=None,
        fits_keyword="PUBDATE",
        ivoa_keyword="obs_release_date",
        ucd="time.release",
    )
    valid_from: AstroPydanticTime | None = AstroField(
        description=(
            "Starting UTC date-time where the contents of this data product are valid,"
            " if known. This is meant to be able to time-order data products by"
            " scientific validity, rather than creation_date. "
        ),
        default=None,
        examples=EXAMPLE_TIMES,
    )
    valid_to: AstroPydanticTime | None = AstroField(
        description=(
            "Ending UTC date-time where the contents of this data product are valid,"
            " if known. This is meant to be able to time-order data products by"
            " scientific validity, rather than creation_date. "
        ),
        default=None,
        examples=EXAMPLE_TIMES,
    )


class Product(ModelBase):
    """Common metadata for all CTAO data products."""

    _namespace: ClassVar[str] = _PRODUCT_NAMESPACE

    ctao_metadata_version: str = AstroField(
        description="CTAO DataProducts Metadata Version",
        default=__version__,
        fits_keyword="CTAOMETA",
    )

    description: str = AstroField(
        description="Human-readable description of this data product",
        ivoa_keyword="obs_title",
        fits_keyword="TITLE",
    )

    data: ident.ProductType = AstroField(doc(ident.ProductType))
    instance: ident.InstanceIdentifier = AstroField(doc(ident.InstanceIdentifier))

    curation: Curation = AstroField(doc(Curation))

    model: DataModel = AstroField(doc(DataModel))

    disclaimer: str | None = AstroField(
        description=(
            "Text describing e.g. the allowed usage of this data product, if specifics"
            " are required."
        ),
        default=None,
        fits_keyword="COMMENT",
    )

    # note `DATE` (FITS v4, p32) is HDU creation date, IVOA recommends CREATED
    # for the product
    creation_time: AstroPydanticTime = AstroField(
        description="UTC Date-time the data product was created",
        examples=EXAMPLE_TIMES,
        fits_keyword="CREATED",
        ivoa_keyword="obs_creation_date",
        ucd="time.creation",
    )

    contact: Contact = AstroField(description=doc(Contact))

    # provenance
    activity: Activity | None = AstroField(doc(Activity), default=None)
    observation: Observation | None = AstroField(doc(Observation), default=None)
    acquisition: Acquisition | None = AstroField(doc(Acquisition), default=None)
    sdc: ScienceDataChallenge | None = AstroField(
        doc(ScienceDataChallenge), default=None
    )

    @model_validator(mode="after")
    def _check_simulation(self) -> Self:
        """Check some fields to be consistent with simulated data."""
        # Check that if this has SDC information, that we use the right facility name.
        if self.sdc:
            if self.instance.facility_name != ident.FacilityName.SIMULATED_CTAO:
                raise ValueError(
                    "This data product is simulated (has SDC context info), so the"
                    " instance.facility_name should be set to SIMULATED_CTAO."
                )

        return self
