#  Copyright (c) yibocat 2025 All Rights Reserved
#  Python: 3.12.7
#  Date: 2025/8/22 15:50
#  Author: yibow
#  Email: yibocat@yeah.net
#  Software: AxisFuzzy

"""
Defines the Pandas Accessor for fuzzy analysis.

This module provides the `.fuzzy` accessor on pandas DataFrames, serving as the
primary user-facing entry point for the entire fuzzy analysis toolkit.
"""
from __future__ import annotations

import inspect
from typing import TYPE_CHECKING, Dict, Any, Union, Tuple, Optional

from axisfuzzy.fuzzifier import Fuzzifier

# 延迟导入策略：避免在模块级别直接导入 pandas
# 这样可以确保核心包安装时不会因为缺少 pandas 而导致导入错误
try:
    import pandas as pd
    _PANDAS_AVAILABLE = True
except ImportError:
    pd = None
    _PANDAS_AVAILABLE = False


from .pipeline import FuzzyPipeline
from .dataframe import FuzzyDataFrame
from .app.model import Model


if _PANDAS_AVAILABLE:
    @pd.api.extensions.register_dataframe_accessor("fuzzy")
    class FuzzyAccessor:
        """
        A pandas DataFrame accessor for fuzzy analysis tools.

        This accessor provides a ``.fuzzy`` namespace on any pandas DataFrame,
        allowing users to seamlessly integrate fuzzy logic and analysis capabilities
        into their standard data analysis workflows.

        It serves as a facade, providing convenient entry points to the more
        complex underlying components like `FuzzyDataFrame`, `FuzzyPipeline`, and `Model`.

        Parameters
        ----------
        pandas_obj : pd.DataFrame
            The pandas DataFrame instance this accessor is bound to.
        """
        def __init__(self, pandas_obj: pd.DataFrame):
            self._df = pandas_obj
            self._validate_obj()

        def _validate_obj(self):
            """Internal validation to ensure the accessor is bound to a DataFrame."""
            if not _PANDAS_AVAILABLE or pd is None:
                raise ImportError(
                    "pandas is not installed. The '.fuzzy' accessor requires "
                    "pandas to be installed. Please install with: pip install 'axisfuzzy[analysis]'"
                )
            if not isinstance(self._df, pd.DataFrame):
                # This case is unlikely to happen due to how accessors are registered,
                # but it's good practice for robustness.
                raise AttributeError(
                    "FuzzyAccessor can only be used with a pandas DataFrame."
                )

        def info(self) -> str:
            """
            Provides a brief summary of the accessor's state.

            Returns
            -------
            str
                A string containing information about the bound DataFrame.
            """
            return (
                f"<FuzzyAccessor>\n"
                f"Bound to: pandas.DataFrame\n"
                f"Shape: {self._df.shape}\n"
                f"Index: {self._df.index.__class__.__name__}\n"
                f"Columns: {list(self._df.columns)}"
            )

        def to_fuzz_dataframe(self,
                              fuzzifier: Optional[Fuzzifier] = None) -> 'FuzzyDataFrame':
            """
            Converts the pandas DataFrame into a high-performance FuzzyDataFrame.

            This method serves as a bridge from the crisp data world of pandas to
            the fuzzy data world of AxisFuzzy. It uses the `FuzzyDataFrame.from_pandas`
            class method for the conversion.

            Parameters
            ----------
            fuzzifier: Fuzzifier
                A fuzzifier with pre-set parameters, used to perform fuzzification
                on data in a pd.DataFrame

            Returns
            -------
            FuzzyDataFrame
                A new FuzzyDataFrame instance containing the fuzzy representation
                of the original data.
            """

            from .dataframe import FuzzyDataFrame  # Lazy import
            # We need to add a `from_pandas` class method to FuzzyDataFrame
            return FuzzyDataFrame.from_pandas(self._df, fuzzifier)

        def run(self,
                executable: Union['FuzzyPipeline', 'Model'],
                return_intermediate: bool = False,
                **kwargs
                ) -> Union[Dict[str, Any], Tuple[Dict[str, Any], Dict[str, Any]]]:
            """
            Executes a fuzzy analysis pipeline or model using the bound DataFrame as initial data.

            This is the main entry point for running a pre-defined `FuzzyPipeline`
            graph or a high-level `Model`. The accessor automatically injects the bound
            DataFrame as input data, and other inputs can be provided as keyword arguments.

            For a `FuzzyPipeline` or `Model`:
            - If there is one input, the DataFrame is passed to it.
            - If there are multiple inputs, the DataFrame is passed to the input named
              'init_data' (preferred) or 'data'.
            - Other required inputs must be provided as keyword arguments (`**kwargs`).
            - For a `Model`, if it's not built, it will be built automatically.

            Parameters
            ----------
            executable : FuzzyPipeline or Model
                A `FuzzyPipeline` or `Model` instance to execute.
            return_intermediate : bool, default False
                If True, returns both final and intermediate results.
            **kwargs :
                Additional keyword arguments to be passed as inputs to the model or pipeline.

            Returns
            -------
            dict[str, Any] or tuple[dict, dict]
                The result of the execution.

            Raises
            ------
            ValueError
                If input injection is ambiguous or a required input is missing.
            """
            # Lazy import to avoid circular dependencies
            from .pipeline import FuzzyPipeline
            from .app.model import Model

            if isinstance(executable, Model):
                model = executable
                if not model.built:
                    model.build()

                sig = inspect.signature(model.forward)
                input_names = [p.name for p in sig.parameters.values() if p.name != 'self']

                run_kwargs = kwargs.copy()

                if len(input_names) == 1:
                    df_input_name = input_names[0]
                    if df_input_name in run_kwargs:
                        raise ValueError(
                            f"Ambiguous input: '{df_input_name}' was provided via both the "
                            f"DataFrame accessor and keyword arguments."
                        )
                    run_kwargs[df_input_name] = self._df
                else:  # multiple inputs
                    # By convention, inject DataFrame into 'init_data' or 'data'
                    if 'init_data' in input_names:
                        df_input_name = 'init_data'
                    elif 'data' in input_names:
                        df_input_name = 'data'
                    else:
                        raise ValueError(
                            f"The model's 'forward' method has multiple inputs ({input_names}), "
                            f"but no 'init_data' or 'data' input was found to inject the DataFrame. "
                            f"Please name one of your inputs 'init_data' or 'data'."
                        )

                    if df_input_name in run_kwargs:
                        raise ValueError(
                            f"Ambiguous input: '{df_input_name}' was provided via both the "
                            f"DataFrame accessor and keyword arguments."
                        )

                    run_kwargs[df_input_name] = self._df

                return model.run(return_intermediate=return_intermediate, **run_kwargs)

            elif isinstance(executable, FuzzyPipeline):
                pipeline = executable
                input_names = list(pipeline._input_nodes.keys())
                num_inputs = len(input_names)

                initial_data = kwargs.copy()

                if num_inputs == 0:
                    if kwargs:
                        raise ValueError("The pipeline has no defined inputs, but keyword arguments were provided.")
                elif num_inputs == 1:
                    df_input_name = input_names[0]
                    if df_input_name in initial_data:
                        raise ValueError(
                            f"Ambiguous input: '{df_input_name}' was provided via both the "
                            f"DataFrame accessor and keyword arguments."
                        )
                    initial_data[df_input_name] = self._df
                else:  # num_inputs > 1
                    if 'init_data' in input_names:
                        df_input_name = 'init_data'
                    elif 'data' in input_names:
                        df_input_name = 'data'
                    else:
                        raise ValueError(
                            f"The pipeline has multiple inputs ({input_names}), but no 'init_data' "
                            f"or 'data' input was found to inject the DataFrame. "
                            f"Please name one of your inputs 'init_data' or 'data'."
                        )

                    if df_input_name in initial_data:
                        raise ValueError(
                            f"Ambiguous input: '{df_input_name}' was provided via both the "
                            f"DataFrame accessor and keyword arguments."
                        )

                    initial_data[df_input_name] = self._df

                return pipeline.run(initial_data, return_intermediate)

            else:
                raise TypeError(
                    f"Expected a FuzzyPipeline or Model object, but got {type(executable).__name__}."
                )