from typing import Dict
from typing import List
from typing import Optional
from typing import Union

import attrs
from typeguard import typechecked

from tecton._internals import display
from tecton._internals import metadata_service
from tecton._internals import sdk_decorators
from tecton.unified import common as unified_common
from tecton.unified import utils as unified_utils
from tecton_core import feature_definition_wrapper
from tecton_core import id_helper
from tecton_core import specs
from tecton_proto.args import basic_info_pb2
from tecton_proto.args import entity_pb2 as entity__args_pb2
from tecton_proto.data import entity_pb2 as entity__data_pb2
from tecton_proto.metadataservice import metadata_service_pb2


@attrs.define
class Entity(unified_common.BaseTectonObject):
    """Entity class, used to organize and join features.

    Entities are used to index and organize features - a :class:`FeatureView`
    contains at least one Entity.

    Entities contain metadata about *join keys*, which represent the columns
    that are used to join features together.

    Example of an Entity declaration:

    .. code-block:: python

        from tecton import Entity

        customer = Entity(
            name='churned_customer',
            join_keys=['customer_id'],
            description='A customer subscribing to a Sports TV subscription service',
            owner='matt@tecton.ai',
            tags={'release': 'development'})
    """

    _spec: Optional[specs.EntitySpec] = attrs.field(repr=False)
    _args: Optional[entity__args_pb2.EntityArgs] = attrs.field(repr=False, on_setattr=attrs.setters.frozen)

    @typechecked
    def __init__(
        self,
        *,
        name: str,
        description: Optional[str] = None,
        tags: Optional[Dict[str, str]] = None,
        owner: Optional[str] = None,
        join_keys: Optional[Union[str, List[str]]] = None,
    ):
        """
        Declare a new Entity.

        :param name: Unique name for the new entity.
        :param description: Short description of the new entity.
        :param tags: Tags associated with this Tecton Object (key-value pairs of arbitrary metadata).
        :param owner: Owner name (typically the email of the primary maintainer).
        :param join_keys: Names of columns that uniquely identify the entity in FeatureView's SQL statement
            for which features should be aggregated. Defaults to using ``name`` as the entity's join key.

        :raises TectonValidationError: if the input non-parameters are invalid.
        """
        from tecton.cli import common as cli_common

        if not join_keys:
            resolved_join_keys = [name]
        elif isinstance(join_keys, str):
            resolved_join_keys = [join_keys]
        else:
            resolved_join_keys = join_keys

        args = entity__args_pb2.EntityArgs(
            entity_id=id_helper.IdHelper.generate_id(),
            info=basic_info_pb2.BasicInfo(name=name, description=description, tags=tags, owner=owner),
            join_keys=join_keys,
            version=feature_definition_wrapper.FrameworkVersion.FWV5.value,
        )
        info = unified_common.TectonObjectInfo.from_args_proto(args.info, args.entity_id)
        source_info = cli_common.get_fco_source_info()
        self.__attrs_init__(info=info, spec=None, args=args, source_info=source_info)

    @classmethod
    @typechecked
    def _create_from_data_proto(cls, proto: entity__data_pb2.Entity) -> "Entity":
        """Create a new Entity object from a data proto."""
        spec = specs.EntitySpec.from_data_proto(proto)
        info = unified_common.TectonObjectInfo.from_data_proto(proto.fco_metadata, proto.entity_id)
        obj = cls.__new__(cls)
        obj.__attrs_init__(info=info, spec=spec, args=None, source_info=None)
        return obj

    @property
    def _is_valid(self):
        return self._spec is not None

    @property
    def join_keys(self) -> List[str]:
        """
        Join keys of the entity.
        """
        if self._spec is None:
            return self._args.join_keys
        return self._spec.join_keys

    @sdk_decorators.sdk_public_method
    def validate(self):
        """
        Validate a local Entity object.
        If this object has already been applied (i.e. not a locally defined object), validate() does not need to be called.
        """
        if self._is_valid:
            return
        # TODO: add validation
        self._spec = specs.EntitySpec.from_args_proto(self._args)

    @sdk_decorators.sdk_public_method
    @unified_utils.requires_remote_object
    def summary(self) -> display.Displayable:
        """
        Displays a human readable summary of this Entity.
        Note: this method is not supported for local objects.
        """
        request = metadata_service_pb2.GetEntitySummaryRequest()
        request.fco_locator.id.CopyFrom(self._spec.id)
        request.fco_locator.workspace = self.workspace

        response = metadata_service.instance().GetEntitySummary(request)
        return display.Displayable.from_fco_summary(response.fco_summary)
