"""
Module to call typer commands to process Date Predictability from a backlog of work items.
"""

import contextlib
from ast import literal_eval
from inspect import stack
from logging import Logger, getLogger
from typing import Annotated, Any

from click import BadParameter
from numpy import int64
from pandas import DataFrame
from rich import print as rich_print
from typer import Argument, Context, Option

from genov.predictability.dp_date_predictability.dp_date_predictability import DpDatePredictability
from genov.utils.typer.context_obj.context_obj import GContextObj


class _ColumnsLabelsForMeasures:
    """
    Internal class instantiated when parsing the command "dp_set_data" using the parser function
    "parse_columns_labels_for_measures", as the parameter "x_measure_column" could be either a String (SPOT MEASURES)
    or a tuple of two strings (RANGE MEASURES).
    This is to be abstracted into a class for Typer to work.
    """

    def __init__(self, value: str | tuple[str, str]):
        self.x_column_label_for_measures: str | tuple[str, str] = value


def _parse_columns_labels_for_measures(value: str | _ColumnsLabelsForMeasures) -> _ColumnsLabelsForMeasures:
    """
    Internal function to parse parameter "x_measure_column" for command "dp_set_data", as the parameter
    "x_measure_column" could be either a String (SPOT MEASURES) or a tuple of two strings (RANGE MEASURES).
    This is abstracted through the class _ColumnsLabelsForMeasures.
    """
    if isinstance(value, str):
        _the_interpreted_value: Any | None = None

        # First, we try to parse a tuple of strings
        with contextlib.suppress(ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
            _the_interpreted_value = literal_eval(value)

        # If the value could be interpreted, it has to be a tuple of 2 strings
        if _the_interpreted_value:
            if isinstance(_the_interpreted_value, tuple):
                if len(_the_interpreted_value) != 2:  # noqa: PLR2004
                    _str_msg: str = (
                        f"The parameter Columns Labels For Measures is a tuple, but contains "
                        f"'{len(_the_interpreted_value)}' records, while 2 are expected."
                    )
                    raise BadParameter(_str_msg)
                if isinstance(_the_interpreted_value[0], str) and isinstance(_the_interpreted_value[1], str):
                    rich_print(
                        f"Data Predictability instance configured as RANGE MEASURES, with columns "
                        f"'{_the_interpreted_value[0]}' (from) and '{_the_interpreted_value[1]}' (to)."
                    )
                    _the_return: _ColumnsLabelsForMeasures = _ColumnsLabelsForMeasures(
                        (_the_interpreted_value[0], _the_interpreted_value[1])
                    )
                else:
                    _str_msg: str = (
                        f"The parameter Columns Labels For Measures is a tuple of 2 records, but should "
                        f"only contain string, while record 0 is of type "
                        f"'{type(_the_interpreted_value[0])}' and record 1 is of type "
                        f"'{type(_the_interpreted_value[1])}'."
                    )
                    raise BadParameter(_str_msg)

            else:
                _str_msg: str = (
                    f"The parameter Columns Labels For Measures is of type "
                    f"'{type(_the_interpreted_value)}', while 'string' or 'tuple of 2 strings' is "
                    f"expected."
                )
                raise BadParameter(_str_msg)

        else:
            rich_print(f"Data Predictability instance configured as SPOT MEASURES, with column '{value}'.")
            _the_return: _ColumnsLabelsForMeasures = _ColumnsLabelsForMeasures(value)

    # Else, we expect the string value to be the column label
    else:
        # This happens when the default value is already a typed instance (aka not a string)
        _the_return: _ColumnsLabelsForMeasures = value

    return _the_return


def dp_set_data(
    ctx_context: Context,
    str_df_alias: Annotated[
        str,
        Argument(
            help="The alias for the dataframe stored in context, used to compute predictability upon.",
            metavar="df_alias",
            callback=GContextObj.check_alias_name_to_get,
        ),
    ],
    str_dp_alias: Annotated[
        str | None,
        Option(
            "--as",
            "-a",
            help="Alias for the Date Predictability instance that is persisted in context. The Date Predictability "
            "instance is later used to execute commands such as COMPUTE, DECOMPOSE or SLIDE.",
            metavar="dp_alias",
            show_default="If not provided, defaulted to _dp",
            callback=GContextObj.check_alias_name_to_set,
        ),
    ] = "_dp",
    _str_key_column: Annotated[
        str | None,
        Option(
            "--key",
            "-k",
            help="Column in the dataframe that contains the KEYs.",
            metavar="key",
            show_default="If not provided, defaulted to 'key'.",
        ),
    ] = "key",
    _str_label_column: Annotated[
        str | None,
        Option(
            "--label",
            "-l",
            help="Column in the dataframe that contains the LABELs.",
            metavar="label",
            show_default="If not provided, defaulted to 'label'.",
        ),
    ] = "label",
    _x_measure_column: Annotated[
        _ColumnsLabelsForMeasures,
        Option(
            "--measure",
            "-m",
            help="Column(s) in the dataframe that contain (s) the MEASUREs, either SPOT (e.g. eta) or RANGE "
            '(e.g. \'("from", "to")\', surrounded with simple quotes).',
            metavar="measure",
            show_default="If not provided, defaulted to 'measure', aka SPOT.",
            parser=_parse_columns_labels_for_measures,
        ),
    ] = "measure",
    _str_status_column: Annotated[
        str | None,
        Option(
            "--status",
            "-s",
            help="Column in the dataframe that contains the STATUSes.",
            metavar="status",
            show_default="If not provided, defaulted to 'status'.",
        ),
    ] = "status",
):
    """
    The command instantiates an instance of Date Predictability that is then staged in context under DP ALIAS:
    - It is initialized with the backlog of work items retrieved from context as a DataFrame aliased DF ALIAS.
    - The columns' labels from the work items DataFrame are configured: KEY, LABEL, MEASURE, STATUS.
    """
    _obj_logger: Logger = getLogger(__name__)
    _obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

    _obj_the_context_obj: GContextObj = ctx_context.obj

    _df_data: DataFrame = _obj_the_context_obj.get_alias_value(
        str_alias=str_df_alias, typ_type=DataFrame, e_strict=GContextObj.Strictness.STRICT
    )

    _dp: DpDatePredictability = DpDatePredictability(
        str_column_label_for_keys=_str_key_column,
        str_column_label_for_labels=_str_label_column,
        x_column_label_for_measures=_x_measure_column.x_column_label_for_measures,
        str_column_label_for_status=_str_status_column,
    )

    _dp.set_data(df_data=_df_data)

    ctx_context.obj[str_dp_alias] = _dp

    rich_print(f"[green]DataPredictability instance staged under alias '{str_dp_alias}'.[/green]")

    _obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)


def dp_compute(
    ctx_context: Context,
    str_dp_alias: Annotated[
        str,
        Option(
            "--dp_instance",
            "-i",
            help="The Date Predictability instance to use.",
            metavar="dp_alias",
            show_default="If not provided, defaulted to '_dp'.",
            callback=GContextObj.check_alias_name_to_get,
        ),
    ] = "_dp",
    str_df_alias: Annotated[
        str,
        Option(
            "--df_output",
            "-o",
            help="Alias for the computed Date Frame persisted in context.",
            metavar="Df Alias",
            show_default="If not provided, defaulted to '_df'.",
            callback=GContextObj.check_alias_name_to_set,
        ),
    ] = "_df",
):
    """
    The command computes the Date Predictability by key, from the Date Predictability instance DP ALIAS that is
    retrieved in context.
    Eventually, computed Date Predictability is persisted in context as a DataFrame aliased DF ALIAS.
    """
    _obj_logger: Logger = getLogger(__name__)
    _obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

    _obj_the_context_obj: GContextObj = ctx_context.obj

    _dp_instance: DpDatePredictability = _obj_the_context_obj.get_alias_value(
        str_alias=str_dp_alias, typ_type=DpDatePredictability, e_strict=GContextObj.Strictness.STRICT
    )

    _df_the_result: DataFrame = _dp_instance.compute()

    ctx_context.obj[str_df_alias] = _df_the_result

    rich_print(f"[green]Data Predictability computed and staged under alias '{str_df_alias}'.[/green]")

    _obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)


def dp_decompose(
    ctx_context: Context,
    str_key: Annotated[
        str,
        Option(
            "--key",
            "-k",
            help="The KEY to slide. Could be ignored if and only if only one key is in data.",
            metavar="key",
        ),
    ]
    | None = None,
    str_dp_alias: Annotated[
        str,
        Option(
            "--dp_instance",
            "-i",
            help="The Date Predictability instance to use.",
            metavar="Dp Alias",
            show_default="If not provided, defaulted to '_dp'.",
            callback=GContextObj.check_alias_name_to_get,
        ),
    ] = "_dp",
    str_df_alias: Annotated[
        str,
        Option(
            "--df_output",
            "-o",
            help="Alias for the computed Date Frame persisted in context.",
            metavar="Df Alias",
            show_default="If not provided, defaulted to '_df'.",
            callback=GContextObj.check_alias_name_to_set,
        ),
    ] = "_df",
):
    """
    The command decomposes the Date Predictability for a given KEY, from the Date Predictability instance DP ALIAS
    that is retrieved in context.
    Eventually, decomposed Date Predictability is persisted in context as a DataFrame aliased DF ALIAS.
    """
    _obj_logger: Logger = getLogger(__name__)
    _obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

    _obj_the_context_obj: GContextObj = ctx_context.obj

    _dp_instance: DpDatePredictability = _obj_the_context_obj.get_alias_value(
        str_alias=str_dp_alias, typ_type=DpDatePredictability, e_strict=GContextObj.Strictness.STRICT
    )

    _df_the_result: DataFrame = _dp_instance.decompose(str_key=str_key)

    ctx_context.obj[str_df_alias] = _df_the_result

    rich_print(
        f"[green]Data Predictability decomposed for key '{str_key}' and staged under alias '{str_df_alias}'.[/green]"
    )

    _obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)


def dp_slide(
    ctx_context: Context,
    str_key: Annotated[
        str,
        Option(
            "--key",
            "-k",
            help="The KEY to slide. Could be ignored if and only if only one key is in data.",
            metavar="key",
        ),
    ]
    | None = None,
    str_dp_alias: Annotated[
        str,
        Option(
            "--dp_instance",
            "-i",
            help="The Date Predictability instance to use.",
            metavar="dp_lias",
            show_default="If not provided, defaulted to '_dp'.",
            callback=GContextObj.check_alias_name_to_get,
        ),
    ] = "_dp",
    str_df_alias: Annotated[
        str,
        Option(
            "--df_output",
            "-o",
            help="Alias for the computed Date Frame persisted in context.",
            metavar="df_alias",
            show_default="If not provided, defaulted to '_df'.",
            callback=GContextObj.check_alias_name_to_set,
        ),
    ] = "_df",
):
    """
    The command slides the Date Predictability for a given KEY, from the Date Predictability instance DP ALIAS
    that is retrieved in context.
    Eventually, slid Date Predictability is persisted in context as a DataFrame aliased DF ALIAS.
    """
    _obj_logger: Logger = getLogger(__name__)
    _obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

    _obj_the_context_obj: GContextObj = ctx_context.obj

    _dp_instance: DpDatePredictability = _obj_the_context_obj.get_alias_value(
        str_alias=str_dp_alias, typ_type=DpDatePredictability, e_strict=GContextObj.Strictness.STRICT
    )

    _df_the_result: DataFrame = _dp_instance.slide(str_key=str_key)

    ctx_context.obj[str_df_alias] = _df_the_result

    rich_print(
        f"[green]Data Predictability decomposed for key '{str_key}' and staged under alias '{str_df_alias}'.[/green]"
    )

    _obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)


def dp_describe(
    ctx_context: Context,
    str_dp_alias: Annotated[
        str,
        Option(
            "--dp_instance",
            "-i",
            help="The Date Predictability instance to use.",
            metavar="Dp Alias",
            show_default="If not provided, defaulted to '_dp'.",
            callback=GContextObj.check_alias_name_to_get,
        ),
    ] = "_dp",
    str_df_alias: Annotated[
        str,
        Option(
            "--dict_output",
            "-o",
            help="Alias for the computed statistics persisted in context.",
            metavar="dict_alias",
            show_default="If not provided, defaulted to '_dict'.",
            callback=GContextObj.check_alias_name_to_set,
        ),
    ] = "_dict",
):
    """
    The command describes the Date Predictability data, from the Date Predictability instance DP ALIAS
    that is retrieved in context.
    Eventually, Date Predictability statistics are persisted in context as a dict aliased DICT_ALIAS.
    """
    _obj_logger: Logger = getLogger(__name__)
    _obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

    _obj_the_context_obj: GContextObj = ctx_context.obj

    _dp_instance: DpDatePredictability = _obj_the_context_obj.get_alias_value(
        str_alias=str_dp_alias, typ_type=DpDatePredictability, e_strict=GContextObj.Strictness.STRICT
    )

    _dict_the_result: dict = _dp_instance.describe()

    ctx_context.obj[str_df_alias] = _dict_the_result

    rich_print(f"[green]Data Predictability described and staged under alias '{str_df_alias}'.[/green]")

    _obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)


def dp_as_of_label(
    ctx_context: Context,
    int_asof: Annotated[
        int,
        Argument(help="The LABEL to truncate posterior observation.", metavar="int_asof"),
    ],
    str_dp_alias: Annotated[
        str,
        Option(
            "--dp_instance",
            "-i",
            help="The Date Predictability instance to use.",
            metavar="Dp Alias",
            show_default="If not provided, defaulted to '_dp'.",
            callback=GContextObj.check_alias_name_to_get,
        ),
    ] = "_dp",
):
    """ """
    _obj_logger: Logger = getLogger(__name__)
    _obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

    _obj_the_context_obj: GContextObj = ctx_context.obj

    _dp_instance: DpDatePredictability = _obj_the_context_obj.get_alias_value(
        str_alias=str_dp_alias, typ_type=DpDatePredictability, e_strict=GContextObj.Strictness.STRICT
    )

    _dict_the_result: dict = _dp_instance.as_of_label(int_as_of_label=int64(int_asof))

    rich_print(f"[green]Data Predictability set as of label '{int_asof}'.[/green]")

    _obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)
