from __future__ import annotations

from typing import List, Optional, Sequence

from metricflow.instances import DataSourceReference
from metricflow.model.objects.common import Metadata
from metricflow.model.objects.elements.dimension import Dimension
from metricflow.model.objects.elements.identifier import Identifier
from metricflow.model.objects.elements.measure import Measure
from metricflow.model.objects.base import ModelWithMetadataParsing, HashableBaseModel
from metricflow.object_utils import ExtendedEnum
from metricflow.specs import LinkableElementReference, MeasureReference


class DataSourceOrigin(ExtendedEnum):
    """Describes how data sources were created

    Impacts determination of validity and duration of storage
    """

    SOURCE = "source"  # "input" data sources
    DERIVED = "derived"  # generated by the semantic layer originating (perhaps indirectly) from sources


class MutabilityType(ExtendedEnum):
    """How data at the physical layer is expected to behave"""

    UNKNOWN = "UNKNOWN"
    IMMUTABLE = "IMMUTABLE"  # never changes
    APPEND_ONLY = "APPEND_ONLY"  # appends along an orderable column
    DS_APPEND_ONLY = "DS_APPEND_ONLY"  # appends along daily column
    FULL_MUTATION = "FULL_MUTATION"  # no guarantees, everything may change


class MutabilityTypeParams(HashableBaseModel):
    """Type params add additional context to mutability"""

    min: Optional[str]
    max: Optional[str]
    update_cron: Optional[str]
    along: Optional[str]


class Mutability(HashableBaseModel):
    """Describes the mutability properties of a data source"""

    type: MutabilityType
    type_params: Optional[MutabilityTypeParams]


class DataSource(HashableBaseModel, ModelWithMetadataParsing):
    """Describes a data source"""

    name: str
    description: Optional[str]
    sql_table: Optional[str]
    sql_query: Optional[str]
    dbt_model: Optional[str]

    identifiers: Sequence[Identifier] = []
    measures: Sequence[Measure] = []
    dimensions: Sequence[Dimension] = []

    mutability: Mutability = Mutability(
        type=MutabilityType.FULL_MUTATION, type_params=MutabilityTypeParams(update_cron="0 0,12 * * *")
    )

    origin: DataSourceOrigin = DataSourceOrigin.SOURCE
    metadata: Optional[Metadata]

    @property
    def identifier_references(self) -> List[LinkableElementReference]:  # noqa: D
        return [i.reference for i in self.identifiers]

    @property
    def dimension_references(self) -> List[LinkableElementReference]:  # noqa: D
        return [i.reference for i in self.dimensions]

    @property
    def measure_references(self) -> List[MeasureReference]:  # noqa: D
        return [i.reference for i in self.measures]

    def get_measure(self, measure_reference: MeasureReference) -> Measure:  # noqa: D
        for measure in self.measures:
            if measure.reference == measure_reference:
                return measure

        raise ValueError(
            f"No dimension with name ({measure_reference.element_name}) in data source with name ({self.name})"
        )

    def get_dimension(self, dimension_reference: LinkableElementReference) -> Dimension:  # noqa: D
        for dim in self.dimensions:
            if dim.reference == dimension_reference:
                return dim

        raise ValueError(f"No dimension with name ({dimension_reference}) in data source with name ({self.name})")

    def get_identifier(self, identifier_reference: LinkableElementReference) -> Identifier:  # noqa: D
        for ident in self.identifiers:
            if ident.reference == identifier_reference:
                return ident

        raise ValueError(f"No identifier with name ({identifier_reference}) in data source with name ({self.name})")

    @property
    def partitions(self) -> List[Dimension]:  # noqa: D
        return [dim for dim in self.dimensions or [] if dim.is_partition]

    @property
    def partition(self) -> Optional[Dimension]:  # noqa: D
        partitions = self.partitions
        if not partitions:
            return None
        if len(partitions) > 1:
            raise ValueError(f"too many partitions for data source {self.name}")
        return partitions[0]

    @property
    def reference(self) -> DataSourceReference:  # noqa: D
        return DataSourceReference(data_source_name=self.name)
