"""
Feature API v2 - Declarative Feature Definitions.

This module provides the Feature API v2, which offers both a fluent builder pattern
for defining features and backward compatibility with the legacy API. It consolidates
all feature definition functionality into a single cohesive module.

Public API:
    - FeatureBuilder: The builder class for declarative feature definitions
    - Feature(): Factory function for legacy API compatibility
    - AggregateFunction: Enum of aggregation functions (re-exported from enums)

Supporting Classes:
    - AggregateConfig: Configuration for aggregations
    - BackfillConfig: Configuration for stream backfill
    - BuiltFeatures: Abstract base class for built features
    - AttributeBuiltFeature: Built feature for single attribute features
    - AggregateBuiltFeatures: Built features for time-windowed aggregate features
    - MultiFeatureBuiltFeatures: Built features for multi-column feature registration
    - WindowAccessor: Helper for accessing windowed features by time window
    - FeatureSourceType: Enum for batch vs stream sources

Example Usage:
    ```python
    import featureform as ff
    from datetime import timedelta

    @ff.entity
    class User:
        # Single-value feature (attribute)
        account_balance = (
            ff.Feature()
            .from_dataset(
                user_data,
                entity="user_id",
                values="balance",
                timestamp="updated_at",
            )
        )

        # Single-value feature with aggregation
        transaction_count = (
            ff.Feature()
            .from_dataset(
                transactions,
                entity="user_id",
                values="amount",
                timestamp="ts",
            )
            .aggregate(
                function=ff.AggregateFunction.COUNT,
                windows=[timedelta(days=7), timedelta(days=30)]
            )
        )

        # Multi-value feature (multiple columns)
        user_metrics = (
            ff.Feature()
            .from_dataset(
                user_data,
                entity="user_id",
                values=["balance", "age", ff.alias("total_spend", "spend")],
                timestamp="updated_at",
            )
        )
        # Access: User.user_metrics["balance"], User.user_metrics["age"]

        # Multi-value aggregate feature
        transaction_stats = (
            ff.Feature()
            .from_dataset(
                transactions,
                entity="user_id",
                values=["amount", "count"],
                timestamp="ts",
            )
            .aggregate(
                function=ff.AggregateFunction.SUM,
                windows=[timedelta(days=7), timedelta(days=30)]
            )
        )
        # Access: User.transaction_stats["amount"][timedelta(days=7)]

        # Legacy API (still supported)
        legacy_feature = ff.Feature(
            transformation[["CustomerID", "Amount", "Timestamp"]],
            type=ff.Float32,
            variant="v1",
        )
    ```
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple, Union

from ..enums import AggregateFunction, ResourceType, ScalarType
from ..resources import (
    AggregateFeature,
    AttributeFeature,
    FeatureVariant,
    ResourceColumnMapping,
)
from .column_spec import Col, ValuesSpec

__all__ = [
    # Main API
    "Feature",
    "FeatureBuilder",
    # Re-exported enum
    "AggregateFunction",
    # Supporting classes and enums
    "FeatureSourceType",
    "FeatureType",
    "AggregateConfig",
    "BackfillConfig",
    "BuiltFeatures",
    "AggregateBuiltFeatures",
    "AttributeBuiltFeature",
    "MultiFeatureBuiltFeatures",
    "WindowAccessor",
    # Constants
    "LIFETIME_WINDOW",
    # Utility functions
    "format_window_suffix",
]


class _LifetimeWindow:
    """
    Sentinel class representing a lifetime window for aggregate features.

    A lifetime window computes the aggregate over ALL records for an entity,
    without any time boundary. For training sets with point-in-time (PIT)
    correctness, it still respects the "strict less than" semantics,
    computing the aggregate over all records before each label's timestamp.

    Usage:
        Use the singleton constant `LIFETIME_WINDOW` instead of instantiating
        this class directly.

    Example:
        ```python
        import featureform as ff

        @ff.entity
        class User:
            # Aggregate without timestamp - only LIFETIME_WINDOW allowed
            total_purchases = (
                ff.Feature()
                .from_dataset(purchases, entity="user_id", values="amount")
                .aggregate(
                    function=ff.AggregateFunction.SUM,
                    windows=[ff.LIFETIME_WINDOW]
                )
            )

            # Aggregate with timestamp - can mix with timedelta windows
            transaction_stats = (
                ff.Feature()
                .from_dataset(transactions, entity="user_id", values="amount", timestamp="ts")
                .aggregate(
                    function=ff.AggregateFunction.COUNT,
                    windows=[timedelta(days=7), ff.LIFETIME_WINDOW]
                )
            )
        ```
    """

    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __repr__(self) -> str:
        return "LIFETIME_WINDOW"

    def __str__(self) -> str:
        return "lifetime"

    def __hash__(self) -> int:
        return hash("LIFETIME_WINDOW")

    def __eq__(self, other) -> bool:
        return isinstance(other, _LifetimeWindow)


# Singleton constant for lifetime window
LIFETIME_WINDOW = _LifetimeWindow()


# =============================================================================
# Supporting Classes
# =============================================================================


class FeatureSourceType(str, Enum):
    """Type of source for a feature."""

    BATCH = "batch"
    STREAM = "stream"


class FeatureType(str, Enum):
    """Type of feature.

    This enum distinguishes between different kinds of features,
    which determines how they can be accessed and used.
    """

    AGGREGATE = (
        "aggregate"  # Time-windowed aggregate features (e.g., COUNT over 7 days)
    )
    ATTRIBUTE = "attribute"  # Simple point-in-time attribute features
    REALTIME = "realtime"  # Realtime features computed at serving time


def format_window_suffix(window: Union[timedelta, _LifetimeWindow]) -> str:
    """Format a window as a string suffix (e.g., '7d', '1h', 'lifetime').

    Args:
        window: Either a timedelta for time-bounded windows or LIFETIME_WINDOW
                for unbounded lifetime aggregation.

    Returns:
        A string suffix representing the window (e.g., '7d', '1h', 'lifetime').
    """
    if isinstance(window, _LifetimeWindow):
        return "lifetime"
    total_seconds = int(window.total_seconds())
    if total_seconds >= 86400 and total_seconds % 86400 == 0:
        return f"{total_seconds // 86400}d"
    elif total_seconds >= 3600 and total_seconds % 3600 == 0:
        return f"{total_seconds // 3600}h"
    elif total_seconds >= 60 and total_seconds % 60 == 0:
        return f"{total_seconds // 60}m"
    else:
        return f"{total_seconds}s"


# Type alias for window specifications (timedelta or LIFETIME_WINDOW)
WindowSpec = Union[timedelta, _LifetimeWindow]


class WindowAccessor:
    """
    Helper for accessing windowed features within a multi-feature set.

    This class provides bracket-notation access to FeatureVariant objects
    by their time window. It is used by MultiFeatureBuiltFeatures to allow
    accessing specific windows for aggregate features.

    Example:
        # Within a multi-feature context
        accessor = WindowAccessor(windows_dict, "transaction_count")
        feature_7d = accessor[timedelta(days=7)]
        feature_lifetime = accessor[LIFETIME_WINDOW]
    """

    def __init__(self, windows: Dict[WindowSpec, FeatureVariant], feature_name: str):
        """
        Initialize a WindowAccessor.

        Args:
            windows: Dictionary mapping window specs (timedelta or LIFETIME_WINDOW)
                     to FeatureVariant objects
            feature_name: The base name of the feature (used in error messages)
        """
        self._windows = windows
        self._feature_name = feature_name

    def __getitem__(self, window: WindowSpec) -> FeatureVariant:
        """
        Get the FeatureVariant for a specific time window.

        Args:
            window: The time window duration (e.g., timedelta(days=7)) or
                    LIFETIME_WINDOW for unbounded aggregation.

        Returns:
            FeatureVariant for the specified window

        Raises:
            KeyError: If the specified window doesn't exist
            TypeError: If window is not a timedelta or LIFETIME_WINDOW
        """
        if not isinstance(window, (timedelta, _LifetimeWindow)):
            raise TypeError(
                f"Expected a timedelta or LIFETIME_WINDOW for window access, got {type(window).__name__}. "
                f"Use feature['{self._feature_name}'][timedelta(days=7)] or "
                f"feature['{self._feature_name}'][LIFETIME_WINDOW] to access a specific window."
            )
        if window not in self._windows:
            available = [format_window_suffix(w) for w in self._windows.keys()]
            raise KeyError(
                f"Window {format_window_suffix(window)} not found for '{self._feature_name}'. "
                f"Available: {available}"
            )
        return self._windows[window]


@dataclass
class SelectConfig:
    """Configuration for selected columns in a feature definition."""

    entity: str
    values: List[Col]
    timestamp: str = ""

    @staticmethod
    def normalize_values(values: Any) -> List["Col"]:
        """
        Normalize a values specification to a list of Col objects.

        This method converts various input formats to a consistent List[Col] format:
        - Single string: "amount" → [Col(column="amount")]
        - Single Col: Col(...) → [Col(...)]
        - List of strings: ["col1", "col2"] → [Col("col1"), Col("col2")]
        - Mixed list: ["col1", Col("col2", "c2")] → [Col("col1"), Col("col2", "c2")]

        Args:
            values: A column specification that can be:
                - A string (column name)
                - A Col instance
                - A list of strings and/or Col instances

        Returns:
            A list of Col objects.

        Raises:
            TypeError: If values is not a string, Col, or list of strings/Cols.
        """
        from .column_spec import Col

        def normalize_single(value: Any) -> Col:
            """Convert a single value to a Col."""
            if isinstance(value, str):
                return Col(column=value)
            elif isinstance(value, Col):
                return value
            else:
                raise TypeError(
                    f"Expected str or Col, got {type(value).__name__}. "
                    f"Values must be column names (str) or Col instances."
                )

        if isinstance(values, str):
            return [Col(column=values)]
        elif isinstance(values, Col):
            return [values]
        elif isinstance(values, list):
            return [normalize_single(v) for v in values]
        else:
            raise TypeError(
                f"Expected str, Col, or list, got {type(values).__name__}. "
                f"Values must be a column name, Col instance, or list of column specs."
            )


@dataclass
class AggregateConfig:
    """Configuration for feature aggregation."""

    function: AggregateFunction
    windows: List[WindowSpec]


@dataclass
class BackfillConfig:
    """Configuration for backfilling stream features from batch data."""

    source: Any
    entity: str
    values: str
    timestamp: str


class BuiltFeatures(ABC):
    """
    Abstract base class for built features.

    This is the result of building a FeatureBuilder. Concrete implementations are:
    - AggregateBuiltFeatures: For features with time windows (e.g., COUNT over 7 days)
    - AttributeBuiltFeature: For simple attribute features without time windows

    Each FeatureVariant has its own variant field that can be updated during
    equivalence resolution.

    This wrapper is set as the entity attribute (e.g., User.transaction_count)
    and provides access to individual features.

    Example:
        # Aggregate features - use bracket indexing
        User.transaction_count[timedelta(days=7)]

        # Attribute features - use name_variant()
        User.account_age.name_variant()

        # Both types - get all features
        User.transaction_count.get_all_features()
    """

    def __init__(self, base_name: str, base_variant: str):
        self._base_name = base_name
        self._base_variant = base_variant

    @property
    @abstractmethod
    def feature_type(self) -> FeatureType:
        """Return the type of this built feature."""
        ...

    @abstractmethod
    def get_all_features(self) -> List[FeatureVariant]:
        """Return all FeatureVariant objects."""
        ...

    @abstractmethod
    def name_variant(self) -> Tuple[str, str]:
        """Return the (name, variant) tuple.

        For aggregate features, raises an error directing users to use bracket indexing.
        For attribute features, returns the feature's name and variant.
        """
        ...

    @abstractmethod
    def __getitem__(self, window: timedelta) -> FeatureVariant:
        """Get the FeatureVariant for a specific time window.

        For aggregate features, returns the FeatureVariant for the specified window.
        For attribute features, raises TypeError.
        """
        ...

    def get_resource_type(self) -> ResourceType:
        return ResourceType.FEATURE_VARIANT

    @abstractmethod
    def __repr__(self) -> str: ...


class AggregateBuiltFeatures(BuiltFeatures):
    """
    Built features for aggregate features with time windows.

    Contains multiple FeatureVariant objects, one per time window (e.g., 1d, 7d, lifetime).
    Access specific windows using bracket notation: `feature[timedelta(days=7)]` or
    `feature[ff.LIFETIME_WINDOW]`.

    Example:
        @ff.entity
        class User:
            transaction_count = (
                ff.Feature()
                .from_dataset(
                    transactions,
                    entity="user_id",
                    values="amount",
                    timestamp="ts",
                )
                .aggregate(
                    function=ff.AggregateFunction.COUNT,
                    windows=[timedelta(days=1), timedelta(days=7), ff.LIFETIME_WINDOW]
                )
            )

        # Access specific window
        User.transaction_count[timedelta(days=7)]
        User.transaction_count[ff.LIFETIME_WINDOW]

        # Get all windows
        User.transaction_count.get_all_features()
    """

    def __init__(
        self,
        features: Dict[WindowSpec, FeatureVariant],
        base_name: str,
        base_variant: str,
    ):
        super().__init__(base_name, base_variant)
        self._features = features

    @property
    def feature_type(self) -> FeatureType:
        return FeatureType.AGGREGATE

    def __getitem__(self, window: WindowSpec) -> FeatureVariant:
        """Get the FeatureVariant for a specific time window.

        Args:
            window: The time window duration (e.g., timedelta(days=7)) or
                    LIFETIME_WINDOW for unbounded aggregation.

        Returns:
            FeatureVariant for the specified window

        Raises:
            KeyError: If the specified window doesn't exist
            TypeError: If window is not a timedelta (e.g., if a string column name is passed)
        """
        if isinstance(window, str):
            raise TypeError(
                f"Expected a timedelta for window access, got string '{window}'. "
                f"For single-value aggregate features, access windows directly: "
                f"feature[timedelta(days=7)], not feature['{window}'][timedelta(days=7)]. "
                f"The column-level indexing is only needed for multi-value features."
            )
        if not isinstance(window, (timedelta, _LifetimeWindow)):
            raise TypeError(
                f"Expected a timedelta or LIFETIME_WINDOW for window access, got {type(window).__name__}. "
                f"Use feature[timedelta(days=7)] or feature[LIFETIME_WINDOW] to access a specific window."
            )
        if window not in self._features:
            available = [format_window_suffix(w) for w in self._features.keys()]
            raise KeyError(
                f"Window {format_window_suffix(window)} not found. "
                f"Available windows: {available}"
            )
        return self._features[window]

    def name_variant(self) -> Tuple[str, str]:
        """Raises ValueError - use bracket indexing for aggregate features."""
        raise ValueError(
            "Use bracket indexing (e.g., feature[timedelta(days=7)] or "
            "feature[ff.LIFETIME_WINDOW]) to access specific windows for aggregate features."
        )

    def get_all_features(self) -> List[FeatureVariant]:
        """Return all FeatureVariant objects for all time windows."""
        return list(self._features.values())

    def __repr__(self) -> str:
        windows = [format_window_suffix(w) for w in self._features.keys()]
        return f"AggregateBuiltFeatures({self._base_name}, windows={windows})"


class AttributeBuiltFeature(BuiltFeatures):
    """
    Built feature for an attribute feature without time windows.

    Contains a single FeatureVariant representing a simple point-in-time attribute.

    Example:
        @ff.entity
        class User:
            account_age = (
                ff.Feature()
                .from_dataset(
                    user_profiles,
                    entity="user_id",
                    values="age_days",
                    timestamp="updated_at",
                )
            )

        # Get name and variant
        User.account_age.name_variant()

        # Get the feature
        User.account_age.get_all_features()
    """

    def __init__(
        self,
        feature: FeatureVariant,
        base_name: str,
        base_variant: str,
    ):
        super().__init__(base_name, base_variant)
        self._feature = feature

    @property
    def feature_type(self) -> FeatureType:
        return FeatureType.ATTRIBUTE

    def __getitem__(self, window: timedelta) -> FeatureVariant:
        """Raises TypeError - attribute features cannot be indexed."""
        raise TypeError(
            f"'{self._base_name}' is not an aggregate feature and cannot be indexed. "
            f"Use name_variant() or get_all_features() instead."
        )

    def name_variant(self) -> Tuple[str, str]:
        """Return the (name, variant) tuple for this attribute feature."""
        return self._feature.name_variant()

    def get_all_features(self) -> List[FeatureVariant]:
        """Return the single FeatureVariant as a list."""
        return [self._feature]

    def __repr__(self) -> str:
        return f"AttributeBuiltFeature({self._base_name}, variant={self._base_variant})"


class MultiFeatureBuiltFeatures(BuiltFeatures):
    """
    Built features for multiple features from a single FeatureBuilder.

    This class is used when a FeatureBuilder has multiple values (columns),
    resulting in multiple features being built at once. It supports both
    attribute features (no time windows) and aggregate features (with time windows).

    For attribute features:
        - Access by column name: `built["col1"]` returns `FeatureVariant`

    For aggregate features:
        - Access by column name: `built["col1"]` returns `WindowAccessor`
        - Chained access: `built["col1"][timedelta(days=7)]` returns `FeatureVariant`

    Example:
        @ff.entity
        class User:
            # Multi-value attribute features
            user_metrics = (
                ff.Feature()
                .from_dataset(
                    user_data,
                    entity="user_id",
                    values=["balance", "age", "score"],
                    timestamp="updated_at",
                )
            )

            # Access individual features
            User.user_metrics["balance"]
            User.user_metrics["age"]

        @ff.entity
        class User:
            # Multi-value aggregate features
            transaction_stats = (
                ff.Feature()
                .from_dataset(
                    transactions,
                    entity="user_id",
                    values=["amount", "count"],
                    timestamp="ts",
                )
                .aggregate(
                    function=ff.AggregateFunction.SUM,
                    windows=[timedelta(days=7), timedelta(days=30)]
                )
            )

            # Access individual features
            User.transaction_stats["amount"][timedelta(days=7)]
            User.transaction_stats["count"][timedelta(days=30)]
    """

    def __init__(
        self,
        features: Union[
            Dict[str, FeatureVariant], Dict[str, Dict[timedelta, FeatureVariant]]
        ],
        base_name: str,
        base_variant: str,
        feature_type: FeatureType,
    ):
        """
        Initialize MultiFeatureBuiltFeatures.

        Args:
            features: Dictionary of features. For attribute features, maps column
                names to FeatureVariant objects. For aggregate features, maps
                column names to dictionaries of window -> FeatureVariant.
            base_name: The base name for this feature set.
            base_variant: The base variant for this feature set.
            feature_type: The type of features (ATTRIBUTE or AGGREGATE).
        """
        super().__init__(base_name, base_variant)
        self._features = features
        self._feature_type = feature_type

    @property
    def feature_type(self) -> FeatureType:
        """Return the type of this built feature."""
        return self._feature_type

    def __getitem__(self, column: str) -> Union[FeatureVariant, WindowAccessor]:
        """
        Get the feature(s) for a specific column.

        For attribute features, returns the FeatureVariant directly.
        For aggregate features, returns a WindowAccessor for further indexing.

        Args:
            column: The column name to access.

        Returns:
            FeatureVariant for attribute features, WindowAccessor for aggregate features.

        Raises:
            KeyError: If the specified column doesn't exist.
        """
        if column not in self._features:
            available = list(self._features.keys())
            raise KeyError(
                f"Column '{column}' not found. Available columns: {available}"
            )

        if self._feature_type == FeatureType.ATTRIBUTE:
            return self._features[column]
        else:
            # For aggregate features, return a WindowAccessor
            windows_dict = self._features[column]
            return WindowAccessor(windows_dict, column)

    def name_variant(self) -> Tuple[str, str]:
        """Raises ValueError - use bracket indexing for multi-feature access."""
        raise ValueError(
            "MultiFeatureBuiltFeatures contains multiple features. "
            "Use bracket indexing (e.g., feature['column_name']) to access "
            "specific features."
        )

    def get_all_features(self) -> List[FeatureVariant]:
        """Return all FeatureVariant objects."""
        all_features = []
        if self._feature_type == FeatureType.ATTRIBUTE:
            # For attribute features, values are FeatureVariant objects
            all_features = list(self._features.values())
        else:
            # For aggregate features, values are dicts of window -> FeatureVariant
            for windows_dict in self._features.values():
                all_features.extend(windows_dict.values())
        return all_features

    def __repr__(self) -> str:
        columns = list(self._features.keys())
        return (
            f"MultiFeatureBuiltFeatures({self._base_name}, "
            f"columns={columns}, type={self._feature_type.value})"
        )


# =============================================================================
# FeatureBuilder Class
# =============================================================================


class FeatureBuilder:
    """
    Builder class for declaratively defining features using a fluent API.

    This class provides a builder pattern for defining features with method chaining.
    Features are automatically validated and registered when used with the @ff.entity
    decorator.

    Example:
        ```python
        @ff.entity
        class User:
            # Simple feature
            account_balance = (
                ff.Feature()
                .from_dataset(
                    batch_features,
                    entity="CustomerID",
                    values="Balance",
                    timestamp="Timestamp",
                )
            )

            # Aggregated feature with multiple time windows
            transaction_count = (
                ff.Feature()
                .from_dataset(
                    batch_features,
                    entity="CustomerID",
                    values="Amount",
                    timestamp="Timestamp",
                )
                .aggregate(
                    function=ff.AggregateFunction.COUNT,
                    windows=[timedelta(days=1), timedelta(days=7)]
                )
            )
        ```
    """

    def __init__(self):
        """Initialize an empty FeatureBuilder for the builder pattern."""
        # Feature identification
        # base_name: The base feature name (e.g., "transaction_count")
        #   For aggregate features, window suffixes are appended (e.g., "transaction_count_7d")
        self.base_name: Optional[str] = None

        # initial_variant: The user's proposed variant for all generated features.
        #
        # This field is named "initial" because the value represents a proposed variant
        # that may be changed during the registration flow. When `create_all()` is called,
        # the server's equivalence resolution checks if an existing feature with matching
        # semantics already exists. If an equivalent is found, the actual
        # `FeatureVariant.variant` will be updated to match the existing equivalent variant,
        # overriding the value specified here.
        #
        # Flow:
        #   1. User sets initial_variant via variant() builder method
        #   2. FeatureVariant objects are created with variant=initial_variant
        #   3. During create_all(), _get_and_set_equivalent_variant() queries the server
        #   4. If equivalent found: FeatureVariant.variant is updated to the existing variant
        #   5. If no equivalent: FeatureVariant.variant remains as initial_variant
        self.initial_variant: Optional[str] = None
        self.entity: Optional[Any] = None

        # Feature metadata (underscore prefix to avoid collision with builder methods)
        self._owner: str = ""
        self._description: str = ""
        self._tags: List[str] = []
        self._properties: Dict[str, str] = {}
        self._value_type: Optional[Union[ScalarType, str]] = None

        # Source configuration
        self.source: Optional[Any] = None
        self.source_type: Optional[FeatureSourceType] = None

        # Column selection
        self.select_config: Optional[SelectConfig] = None

        # Aggregation configuration
        self.aggregate_config: Optional[AggregateConfig] = None

        # Backfill configuration (for stream features)
        self.backfill_config: Optional[BackfillConfig] = None

        # Built features (set after build() is called)
        self._built_features: Optional[BuiltFeatures] = None

    # =========================================================================
    # Builder Pattern Methods
    # =========================================================================

    def from_dataset(
        self,
        source: Any,
        entity: str,
        values: ValuesSpec,
        timestamp: str = "",
    ) -> "FeatureBuilder":
        """
        Set the batch dataset source for this feature and select columns.

        This method configures the source table and column mappings for features.
        It supports both single-value and multi-value specifications.

        **Single-value (backward compatible):**
            Specifying a single column creates one feature. Build returns
            `AttributeBuiltFeature` or `AggregateBuiltFeatures` depending on
            whether aggregation is configured.

        **Multi-value:**
            Specifying multiple columns creates one feature per column.
            Build returns `MultiFeatureBuiltFeatures` with access via bracket
            notation: `built["column_name"]`.

        Args:
            source: A registered source (transformation result or table source)
            entity: Column name containing the entity identifier
            values: Column name(s) containing the feature value(s). Can be:
                - A string: "amount" (single feature)
                - A Col instance: Col(column="amount", name="amt") (with alias)
                - A list of strings and/or Col instances for multi-feature:
                  ["col1", "col2"] or ["col1", alias("col2", "c2")]
            timestamp: Column name containing the timestamp (optional)

        Returns:
            self: For method chaining

        Raises:
            ValueError: If a source has already been set

        Example:
            ```python
            # Single feature
            ff.Feature().from_dataset(source, entity="user_id", values="amount")

            # Multiple features
            ff.Feature().from_dataset(
                source,
                entity="user_id",
                values=["balance", "age", alias("transaction_amount", "amt")],
                timestamp="updated_at",
            )
            ```
        """
        if self.source is not None:
            raise ValueError(
                "Source already set. from_dataset() or from_stream() can only be called once."
            )
        self.source = source
        self.source_type = FeatureSourceType.BATCH
        self.select_config = SelectConfig(
            entity=entity,
            values=SelectConfig.normalize_values(values),
            timestamp=timestamp,
        )
        return self

    def from_stream(
        self,
        source: Any,
        entity: str,
        values: ValuesSpec,
        timestamp: str = "",
    ) -> "FeatureBuilder":
        """
        Set the streaming source for this feature and select columns.

        This method configures the streaming source and column mappings for features.
        It supports both single-value and multi-value specifications (see from_dataset
        for detailed multi-value documentation).

        Args:
            source: A registered streaming source
            entity: Column name containing the entity identifier
            values: Column name(s) containing the feature value(s). Can be:
                - A string: "amount" (single feature)
                - A Col instance: Col(column="amount", name="amt") (with alias)
                - A list of strings and/or Col instances for multi-feature:
                  ["col1", "col2"] or ["col1", alias("col2", "c2")]
            timestamp: Column name containing the timestamp (optional)

        Returns:
            self: For method chaining

        Raises:
            ValueError: If a source has already been set
        """
        if self.source is not None:
            raise ValueError(
                "Source already set. from_dataset() or from_stream() can only be called once."
            )
        self.source = source
        self.source_type = FeatureSourceType.STREAM
        self.select_config = SelectConfig(
            entity=entity,
            values=SelectConfig.normalize_values(values),
            timestamp=timestamp,
        )
        return self

    def aggregate(
        self,
        function: AggregateFunction,
        windows: List[WindowSpec],
    ) -> "FeatureBuilder":
        """
        Configure time-windowed aggregation for this feature.

        This method must be called after from_dataset() or from_stream() to specify
        aggregation over the selected columns.

        Args:
            function: The aggregation function (COUNT, MEAN, SUM, MIN, MAX, etc.)
            windows: List of time windows to aggregate over. Can include timedelta
                     values (e.g., timedelta(days=7)) and/or LIFETIME_WINDOW for
                     unbounded lifetime aggregation.

        Returns:
            self: For method chaining

        Raises:
            ValueError: If from_dataset() or from_stream() has not been called first,
                       if aggregate() has already been called, if no windows specified,
                       or if using timedelta windows without a timestamp column.

        Note:
            When no timestamp column is specified in from_dataset()/from_stream(),
            only LIFETIME_WINDOW is allowed since time-bounded windows require
            a timestamp to define the window boundaries.
        """
        if self.select_config is None:
            raise ValueError(
                "from_dataset() or from_stream() must be called before aggregate(). "
                "Use .from_dataset(source, entity=..., values=..., timestamp=...).aggregate(...)"
            )
        if self.aggregate_config is not None:
            raise ValueError("aggregate() can only be called once per FeatureBuilder.")
        if not windows:
            raise ValueError("At least one window must be specified")

        # Validate window types
        for window in windows:
            if not isinstance(window, (timedelta, _LifetimeWindow)):
                raise TypeError(
                    f"Windows must be timedelta or LIFETIME_WINDOW, got {type(window).__name__}"
                )
            # Reject timedelta(0) to avoid ambiguity with LIFETIME_WINDOW
            if isinstance(window, timedelta) and window.total_seconds() == 0:
                raise ValueError(
                    "Use LIFETIME_WINDOW instead of timedelta(0) for unbounded aggregation. "
                    "This ensures clear intent and prevents ambiguity in the feature definition."
                )

        # Validate timestamp/window compatibility
        has_timestamp = self.select_config.timestamp != ""
        has_timedelta_windows = any(isinstance(w, timedelta) for w in windows)

        if not has_timestamp and has_timedelta_windows:
            raise ValueError(
                "Aggregate features without a timestamp column can only use LIFETIME_WINDOW. "
                "Time-bounded windows (timedelta) require a timestamp column to define "
                "the window boundaries. Either:\n"
                "  1. Add a timestamp column: .from_dataset(..., timestamp='your_ts_column')\n"
                "  2. Use only LIFETIME_WINDOW: .aggregate(..., windows=[ff.LIFETIME_WINDOW])"
            )

        self.aggregate_config = AggregateConfig(
            function=function,
            windows=windows,
        )
        return self

    def backfill_from(
        self,
        source: Any,
        entity: str,
        values: str,
        timestamp: str,
    ) -> "FeatureBuilder":
        """
        Configure backfill from a batch source (for stream features).

        This is used to populate historical data for stream features
        from an existing batch dataset with potentially different column names.

        Args:
            source: The batch source to backfill from
            entity: Entity column name in the backfill source
            values: Value column name in the backfill source
            timestamp: Timestamp column name in the backfill source

        Returns:
            self: For method chaining

        Raises:
            ValueError: If not a stream feature, or if backfill already configured
        """
        if self.source_type != FeatureSourceType.STREAM:
            raise ValueError("backfill_from() can only be used with stream features")
        if self.backfill_config is not None:
            raise ValueError(
                "backfill_from() can only be called once per FeatureBuilder."
            )
        self.backfill_config = BackfillConfig(
            source=source,
            entity=entity,
            values=values,
            timestamp=timestamp,
        )
        return self

    def name(self, name: str) -> "FeatureBuilder":
        """
        Set the base name for this feature.

        This method allows manually specifying the feature name instead of
        deriving it from the entity attribute name.

        Args:
            name: The base name for the feature

        Returns:
            self: For method chaining

        Example:
            ```python
            feature = (
                ff.Feature()
                .name("custom_feature_name")
                .variant("v1")
                .from_dataset(source, entity="user_id", values="amount", timestamp="ts")
            )
            ```
        """
        self.base_name = name
        return self

    def variant(self, variant: str) -> "FeatureBuilder":
        """
        Set the initial/proposed variant for this feature.

        This method allows manually specifying the feature variant instead of
        using an auto-generated or default variant. The value provided here is
        considered "initial" or "proposed" because it may be changed during the
        registration process.

        **Equivalence Resolution Behavior:**

        During `client.apply()` (which calls `create_all()`), the server performs
        equivalence resolution to detect if an existing feature with matching
        semantics already exists. This affects the final variant used:

        - **If an equivalent feature is found:** The `FeatureVariant.variant` will
          be updated to match the existing equivalent variant, overriding the value
          specified here. This ensures semantic deduplication across the system.

        - **If no equivalent is found:** The variant specified here will be used
          as-is when creating the new feature.

        This means that calling `.variant("my_v1")` does not guarantee the final
        feature will have variant `"my_v1"` - it sets the proposed variant that
        will be used if no equivalent exists.

        Args:
            variant: The proposed variant string for the feature

        Returns:
            self: For method chaining

        Example:
            ```python
            feature = (
                ff.Feature()
                .name("transaction_count")
                .variant("production_v2")  # Proposed variant; may change if equivalent exists
                .from_dataset(source, entity="user_id", values="amount", timestamp="ts")
            )
            ```
        """
        self.initial_variant = variant
        return self

    def description(self, desc: str) -> "FeatureBuilder":
        """
        Set the feature description.

        Args:
            desc: A human-readable description of the feature

        Returns:
            self: For method chaining

        Example:
            ```python
            feature = (
                ff.Feature()
                .description("Average transaction amount over the past 7 days")
                .from_dataset(source, entity="user_id", values="amount", timestamp="ts")
            )
            ```
        """
        self._description = desc
        return self

    def owner(self, owner: Any) -> "FeatureBuilder":
        """
        Set the feature owner.

        Args:
            owner: Owner name (string) or UserRegistrar-like object with .name() method

        Returns:
            self: For method chaining

        Raises:
            ValueError: If owner type is not recognized

        Example:
            ```python
            feature = (
                ff.Feature()
                .owner("data_team")
                .from_dataset(source, entity="user_id", values="amount", timestamp="ts")
            )
            ```
        """
        if isinstance(owner, str):
            self._owner = owner
        elif hasattr(owner, "name"):
            name_attr = getattr(owner, "name")
            self._owner = name_attr() if callable(name_attr) else name_attr
        else:
            raise ValueError(f"Cannot resolve owner from type {type(owner)}")
        return self

    def tags(self, tags: List[str]) -> "FeatureBuilder":
        """
        Set the feature tags.

        Args:
            tags: A list of string tags for categorization

        Returns:
            self: For method chaining

        Example:
            ```python
            feature = (
                ff.Feature()
                .tags(["ml", "production", "fraud"])
                .from_dataset(source, entity="user_id", values="amount", timestamp="ts")
            )
            ```
        """
        self._tags = tags
        return self

    def properties(self, props: Dict[str, str]) -> "FeatureBuilder":
        """
        Set the feature properties.

        Args:
            props: A dictionary of string key-value pairs for metadata

        Returns:
            self: For method chaining

        Example:
            ```python
            feature = (
                ff.Feature()
                .properties({"team": "data", "source": "transactions"})
                .from_dataset(source, entity="user_id", values="amount", timestamp="ts")
            )
            ```
        """
        self._properties = props
        return self

    def type(self, value_type: Union[ScalarType, str]) -> "FeatureBuilder":
        """
        Set the feature value type.

        Args:
            value_type: The scalar type of the feature value (e.g., ScalarType.FLOAT64)

        Returns:
            self: For method chaining

        Example:
            ```python
            feature = (
                ff.Feature()
                .type(ff.Float64)
                .from_dataset(source, entity="user_id", values="amount", timestamp="ts")
            )
            ```
        """
        self._value_type = value_type
        return self

    # =========================================================================
    # Resource Interface Methods
    # =========================================================================

    def name_variant(self) -> Tuple[str, str]:
        """Return the (name, variant) tuple for this feature."""
        if self.base_name is None:
            raise ValueError("Feature base_name not set")
        if self.initial_variant is None:
            raise ValueError("Feature initial_variant not set")
        return (self.base_name, self.initial_variant)

    def get_resource_type(self) -> ResourceType:
        """Return the resource type for this feature."""
        return ResourceType.FEATURE_VARIANT

    def to_key(self) -> Tuple[ResourceType, str, str]:
        """Return the unique key for this resource."""
        if self.base_name is None:
            raise ValueError("Feature base_name not set")
        if self.initial_variant is None:
            raise ValueError("Feature initial_variant not set")
        return self.get_resource_type(), self.base_name, self.initial_variant

    def _has_aggregation(self) -> bool:
        """Check if this builder has aggregation configured."""
        return self.aggregate_config is not None

    def _is_stream(self) -> bool:
        """Check if this is a stream feature."""
        return self.source_type == FeatureSourceType.STREAM

    def get_windows(self) -> List[timedelta]:
        """Get the list of configured windows, or an empty list if not aggregated."""
        if self.aggregate_config is None:
            return []
        return self.aggregate_config.windows

    # =========================================================================
    # Build Method
    # =========================================================================

    def build(self) -> BuiltFeatures:
        """
        Build and return BuiltFeatures containing FeatureVariant(s).

        This method validates the configuration and creates the appropriate
        FeatureVariant resources. For aggregated features with multiple windows,
        one FeatureVariant is created per window.

        Each FeatureVariant has its own variant field that can be updated
        during equivalence resolution.

        Note: Before calling build(), the following fields should be normalized:
        - entity: Should be a string (entity name), not an EntityRegistrar
        - owner: Should have a default value if not set
        - value_type: Should be a ScalarType, not None or string

        Returns:
            BuiltFeatures: Wrapper containing FeatureVariant(s)

        Raises:
            ValueError: If required configuration is missing
        """
        # Validate required fields
        if self.source is None:
            raise ValueError(
                "Feature source not set. Use from_dataset() or from_stream()"
            )
        if self.select_config is None:
            raise ValueError(
                "Feature columns not set. "
                "Provide entity, values, and timestamp parameters to from_dataset() or from_stream()"
            )
        if self.base_name is None:
            raise ValueError("Feature base_name not set")

        # Dispatch based on the number of values
        if len(self.select_config.values) == 1:
            built_features = self._build_single_value_feature()
        else:
            built_features = self._build_multi_value_features()

        # Store reference for bracket indexing access
        self._built_features = built_features

        return built_features

    def _build_single_value_feature(self) -> BuiltFeatures:
        """
        Build a single-value feature (either attribute or aggregate).

        This private method handles the case where exactly one value column
        is specified in the SelectConfig. It creates either an AttributeBuiltFeature
        or an AggregateBuiltFeatures depending on whether aggregation is configured.

        Returns:
            BuiltFeatures: Wrapper containing FeatureVariant(s)
        """
        source_name_variant = self._get_source_name_variant()

        # Get column configuration
        entity_column = self.select_config.entity
        timestamp_column = self.select_config.timestamp
        value_column = self.select_config.values[0].source_column

        has_aggregation = self._has_aggregation()

        if has_aggregation:
            # Create one FeatureVariant per window
            features: Dict[WindowSpec, FeatureVariant] = {}
            for window in self.aggregate_config.windows:
                window_suffix = format_window_suffix(window)
                feature_name = f"{self.base_name}_{window_suffix}"

                # Convert LIFETIME_WINDOW to timedelta(0) for proto serialization
                # The Go provider layer interprets TimeWindowSeconds=0 as unbounded/lifetime
                proto_time_window = (
                    timedelta(0) if isinstance(window, _LifetimeWindow) else window
                )

                aggregate_def = AggregateFeature(
                    name=feature_name,
                    input_column=value_column,
                    input_type=self._value_type,
                    function=self.aggregate_config.function,
                    time_window=proto_time_window,
                )

                feature_variant = FeatureVariant(
                    created=None,
                    name=feature_name,
                    variant=self.initial_variant,
                    source=source_name_variant,
                    value_type=self._value_type,
                    entity=self.entity,
                    owner=self._owner,
                    provider="",
                    description=self._description,
                    schedule="",
                    location=ResourceColumnMapping(
                        entity=entity_column,
                        value=value_column,
                        timestamp=timestamp_column,
                    ),
                    tags=self._tags,
                    properties=self._properties,
                    feature_definition=aggregate_def,
                )
                features[window] = feature_variant

            built_features: BuiltFeatures = AggregateBuiltFeatures(
                features=features,
                base_name=self.base_name,
                base_variant=self.initial_variant,
            )
        else:
            # Create a single attribute feature
            attribute_def = AttributeFeature(
                name=self.base_name,
                input_column=value_column,
                input_type=self._value_type,
            )

            feature_variant = FeatureVariant(
                created=None,
                name=self.base_name,
                variant=self.initial_variant,
                source=source_name_variant,
                value_type=self._value_type,
                entity=self.entity,
                owner=self._owner,
                provider="",
                description=self._description,
                schedule="",
                location=ResourceColumnMapping(
                    entity=entity_column,
                    value=value_column,
                    timestamp=timestamp_column,
                ),
                tags=self._tags,
                properties=self._properties,
                feature_definition=attribute_def,
            )

            built_features = AttributeBuiltFeature(
                feature=feature_variant,
                base_name=self.base_name,
                base_variant=self.initial_variant,
            )

        return built_features

    def _build_multi_value_features(self) -> BuiltFeatures:
        """
        Build multiple features from multiple value columns.

        This private method handles the case where multiple value columns
        are specified in the SelectConfig. It creates a MultiFeatureBuiltFeatures
        containing either:
        - For attribute features: one FeatureVariant per column
        - For aggregate features: one FeatureVariant per column per window

        Returns:
            MultiFeatureBuiltFeatures: Wrapper containing all FeatureVariant objects
        """
        source_name_variant = self._get_source_name_variant()

        # Get column configuration
        entity_column = self.select_config.entity
        timestamp_column = self.select_config.timestamp

        has_aggregation = self._has_aggregation()

        if has_aggregation:
            # Create features for each column × window combination
            # Structure: { column_name: { window: FeatureVariant } }
            features: Dict[str, Dict[timedelta, FeatureVariant]] = {}

            for col_spec in self.select_config.values:
                feature_name = col_spec.feature_name
                value_column = col_spec.source_column

                windows_dict: Dict[timedelta, FeatureVariant] = {}
                for window in self.aggregate_config.windows:
                    window_suffix = format_window_suffix(window)
                    full_feature_name = f"{feature_name}_{window_suffix}"

                    aggregate_def = AggregateFeature(
                        name=full_feature_name,
                        input_column=value_column,
                        input_type=self._value_type,
                        function=self.aggregate_config.function,
                        time_window=window,
                    )

                    feature_variant = FeatureVariant(
                        created=None,
                        name=full_feature_name,
                        variant=self.initial_variant,
                        source=source_name_variant,
                        value_type=self._value_type,
                        entity=self.entity,
                        owner=self._owner,
                        provider="",
                        description=self._description,
                        schedule="",
                        location=ResourceColumnMapping(
                            entity=entity_column,
                            value=value_column,
                            timestamp=timestamp_column,
                        ),
                        tags=self._tags,
                        properties=self._properties,
                        feature_definition=aggregate_def,
                    )
                    windows_dict[window] = feature_variant

                features[feature_name] = windows_dict

            built_features: BuiltFeatures = MultiFeatureBuiltFeatures(
                features=features,
                base_name=self.base_name,
                base_variant=self.initial_variant,
                feature_type=FeatureType.AGGREGATE,
            )
        else:
            # Create one attribute feature per column
            # Structure: { column_name: FeatureVariant }
            features_attr: Dict[str, FeatureVariant] = {}

            for col_spec in self.select_config.values:
                feature_name = col_spec.feature_name
                value_column = col_spec.source_column

                attribute_def = AttributeFeature(
                    name=feature_name,
                    input_column=value_column,
                    input_type=self._value_type,
                )

                feature_variant = FeatureVariant(
                    created=None,
                    name=feature_name,
                    variant=self.initial_variant,
                    source=source_name_variant,
                    value_type=self._value_type,
                    entity=self.entity,
                    owner=self._owner,
                    provider="",
                    description=self._description,
                    schedule="",
                    location=ResourceColumnMapping(
                        entity=entity_column,
                        value=value_column,
                        timestamp=timestamp_column,
                    ),
                    tags=self._tags,
                    properties=self._properties,
                    feature_definition=attribute_def,
                )
                features_attr[feature_name] = feature_variant

            built_features = MultiFeatureBuiltFeatures(
                features=features_attr,
                base_name=self.base_name,
                base_variant=self.initial_variant,
                feature_type=FeatureType.ATTRIBUTE,
            )

        return built_features

    # =========================================================================
    # Registration Methods
    # =========================================================================

    def register(self) -> None:
        """
        Register this feature with the global registrar.

        This method is called by the @ff.entity decorator to register
        the feature with the Featureform backend.
        """
        from . import global_registrar

        global_registrar.register_feature_builder(self)

    # =========================================================================
    # Helper Methods
    # =========================================================================

    def _get_source_name_variant(self) -> Tuple[str, str]:
        """Get the (name, variant) tuple from the source."""
        if hasattr(self.source, "name_variant"):
            return self.source.name_variant()
        elif isinstance(self.source, tuple) and len(self.source) == 2:
            return self.source
        else:
            raise ValueError(
                f"Source must have a 'name_variant()' method or be a (name, variant) tuple. "
                f"Got: {type(self.source).__name__}"
            )


# =============================================================================
# Feature Factory Function
# =============================================================================


def Feature(*args, **kwargs):
    """
    Create a Feature definition.

    This is the main entry point for defining features. It supports both
    the legacy API and the new builder pattern.

    **Legacy API** (pass transformation_args as first positional argument):
        ```python
        @ff.entity
        class User:
            avg_transactions = ff.Feature(
                transformation[["CustomerID", "Amount", "Timestamp"]],
                type=ff.Float32,
                variant="quickstart",
            )
        ```

    **Builder Pattern** (no arguments, use method chaining):
        ```python
        @ff.entity
        class User:
            account_balance = (
                ff.Feature()
                .from_dataset(
                    batch_features,
                    entity="CustomerID",
                    values="Balance",
                    timestamp="Timestamp",
                )
            )

            transaction_count = (
                ff.Feature()
                .from_dataset(
                    batch_features,
                    entity="CustomerID",
                    values="Amount",
                    timestamp="Timestamp",
                )
                .aggregate(
                    function=ff.AggregateFunction.COUNT,
                    windows=[timedelta(days=1), timedelta(days=7)]
                )
            )
        ```

    Both patterns can be used together in the same entity class, allowing
    for seamless migration from the legacy API to the builder pattern.

    Returns:
        FeatureBuilder (no args) or FeatureColumnResource (with args)
    """
    if args or kwargs:
        # Legacy API - delegate to FeatureColumnResource
        from .column_resources import FeatureColumnResource

        return FeatureColumnResource(*args, **kwargs)
    else:
        # Builder Pattern - return new FeatureBuilder
        return FeatureBuilder()
