"""Calculate Precision, Recall, and F1-Score for time series anomaly detection."""

__maintainer__ = []
__all__ = [
    "ts_precision",
    "ts_recall",
    "ts_fscore",
    "_flatten_ranges",
    "_ts_precision",
    "_ts_recall",
]

import numpy as np
from deprecated.sphinx import deprecated


def _flatten_ranges(ranges):
    """
    If the input is a list of lists, it flattens it into a single list.

    Parameters
    ----------
    ranges : list of tuples or list of lists of tuples
        The ranges to flatten. each tuple shoulod be in the format of (start, end).

    Returns
    -------
    list of tuples
        A flattened list of ranges.

    Examples
    --------
    >>> _flatten_ranges([[(1, 5), (10, 15)], [(20, 25)]])
    [(1, 5), (10, 15), (20, 25)]
    """
    if not ranges:
        return []
    if isinstance(ranges[0], list):
        flat = []
        for sublist in ranges:
            for pred in sublist:
                flat.append(pred)
        return flat
    return ranges


def _udf_gamma_def(overlap_count):
    """User-defined gamma function. Should return a gamma value > 1.

    Parameters
    ----------
    overlap_count : int
        The number of overlapping ranges.

    Returns
    -------
    float
        The user-defined gamma value (>1).
    """
    return_val = 1 + 0.1 * overlap_count  # modify this function as needed

    return return_val


def _calculate_bias(position, length, bias_type="flat"):
    """Calculate bias value based on position and length.

    Parameters
    ----------
    position : int
        Current position in the range
    length : int
        Total length of the range
    bias_type : str, default="flat"
        Type of bias to apply, Should be one of ["flat", "front", "middle", "back"].
    """
    if bias_type == "flat":
        return 1.0
    elif bias_type == "front":
        return 1.0 - (position - 1) / length
    elif bias_type == "middle":
        if length / 2 == 0:
            return 1.0
        if position <= length / 2:
            return position / (length / 2)
        else:
            return (length - position + 1) / (length / 2)
    elif bias_type == "back":
        return position / length
    else:
        raise ValueError(f"Invalid bias type: {bias_type}")


def _gamma_select(cardinality, gamma):
    """Select a gamma value based on the cardinality type.

    Parameters
    ----------
    cardinality : int
        The number of overlapping ranges.
    gamma : str
        Gamma to use. Should be one of ["one", "reciprocal", "udf_gamma"].

    Returns
    -------
    float
        The selected gamma value.

    Raises
    ------
    ValueError
        If an invalid `gamma` type is provided or if `udf_gamma` is required
        but not provided.
    """
    if gamma == "one":
        return 1.0
    elif gamma == "reciprocal":
        return 1 / cardinality if cardinality > 1 else 1.0
    elif gamma == "udf_gamma":
        if _udf_gamma_def(cardinality) is not None:
            return 1.0 / _udf_gamma_def(cardinality)
        else:
            raise ValueError("udf_gamma must be provided for 'udf_gamma' gamma type.")
    else:
        raise ValueError(
            "Invalid gamma type. Choose from ['one', 'reciprocal', 'udf_gamma']."
        )


def _calculate_overlap_reward_precision(pred_range, overlap_set, bias_type):
    """Overlap Reward for y_pred.

    Parameters
    ----------
    pred_range : tuple
        The predicted range.
    overlap_set : set
        The set of overlapping positions.
    bias_type : str
        Type of bias to apply, Should be one of ["flat", "front", "middle", "back"].

    Returns
    -------
    float
        The weighted value for overlapping positions only.
    """
    start, end = pred_range
    length = end - start + 1

    max_value = 0  # Total possible weighted value for all positions.
    my_value = 0  # Weighted value for overlapping positions only.

    for i in range(1, length + 1):
        global_position = start + i - 1
        bias_value = _calculate_bias(i, length, bias_type)
        max_value += bias_value

        if global_position in overlap_set:
            my_value += bias_value

    return my_value / max_value if max_value > 0 else 0.0


def _calculate_overlap_reward_recall(real_range, overlap_set, bias_type):
    """Overlap Reward for y_real.

    Parameters
    ----------
    real_range : tuple
        The real range.
    overlap_set : set
        The set of overlapping positions.
    bias_type : str
        Type of bias to apply, Should be one of ["flat", "front", "middle", "back"].

    Returns
    -------
    float
        The weighted value for overlapping positions only.
    """
    start, end = real_range
    length = end - start + 1

    max_value = 0.0  # Total possible weighted value for all positions.
    my_value = 0.0  # Weighted value for overlapping positions only.

    for i in range(1, length + 1):
        global_position = start + i - 1
        bias_value = _calculate_bias(i, length, bias_type)
        max_value += bias_value

        if global_position in overlap_set:
            my_value += bias_value

    return my_value / max_value if max_value > 0 else 0.0


def _binary_to_ranges(binary_sequence):
    """
    Convert a binary sequence to a list of anomaly ranges.

    Parameters
    ----------
    binary_sequence : list
        Binary sequence where 1 indicates anomaly and 0 indicates normal.

    Returns
    -------
    list of tuples
        List of anomaly ranges as (start, end) tuples.

    """
    ranges = []
    start = None

    for i, val in enumerate(binary_sequence):
        if val and start is None:
            start = i
        elif not val and start is not None:
            ranges.append((start, i - 1))
            start = None

    if start is not None:
        ranges.append((start, len(binary_sequence) - 1))

    return ranges


# TODO: remove in v1.3.0
@deprecated(
    version="1.2.0",
    reason="ts_precision is deprecated and will be removed in v1.3.0. "
    "Please use range_precision instead.",
    category=FutureWarning,
)
def ts_precision(y_pred, y_real, gamma="one", bias_type="flat", alpha=0.0):
    """
    Calculate Precision for time series anomaly detection.

    Precision measures the proportion of correctly predicted anomaly positions
    out of all all the predicted anomaly positions, aggregated across the entire time
    series.

    Parameters
    ----------
    y_pred : list of tuples or binary sequence
        The predicted anomaly ranges.
        - For range-based input, each tuple represents a range (start, end) of the
          anomaly where start is starting index (inclusive) and end is ending index
          (inclusive).
        - For binary inputs, the sequence should contain integers (0 or 1), where 1
          indicates an anomaly. In this case, y_pred and y_real must be of same length.
    y_real : list of tuples, list of lists of tuples or binary sequence
        The real/actual (ground truth) ranges.
        - For range-based input, each tuple represents a range (start, end) of the
          anomaly where start is starting index (inclusive) and end is ending index
          (inclusive).
        - If y_real is in the format of list of lists, they will be flattened into a
          single list of tuples bringing it to the above format.
        - For binary inputs, the sequence should contain integers (0 or 1), where 1
          indicates an anomaly. In this case, y_pred and y_real must be of same length.
    bias_type : str, default="flat"
        Type of bias to apply. Should be one of ["flat", "front", "middle", "back"].
    gamma : str, default="one"
        Cardinality type. Should be one of ["reciprocal", "one"].

    Returns
    -------
    float
        Precision

    Raises
    ------
    ValueError
        If an invalid `gamma` type is provided.
    ValueError
        If input sequence is binary and y_real and y_pred are of different lengths.

    References
    ----------
    .. [1] Tatbul, Nesime, Tae Jun Lee, Stan Zdonik, Mejbah Alam,and Justin Gottschlich.
       "Precision and Recall for Time Series." 32nd Conference on Neural Information
       Processing Systems (NeurIPS 2018), Montréal, Canada.
       http://papers.nips.cc/paper/7462-precision-and-recall-for-time-series.pdf
    """
    # Check if inputs are binary or range-based
    is_binary = False
    if isinstance(y_pred, (list, tuple, np.ndarray)) and isinstance(
        y_pred[0], (int, bool, np.integer)
    ):
        is_binary = True
    if isinstance(y_real, (list, tuple, np.ndarray)) and isinstance(
        y_real[0], (int, bool, np.integer)
    ):
        is_binary = True

    if is_binary:
        if not isinstance(y_pred, (list, tuple, np.ndarray)) or not isinstance(
            y_real, (list, tuple, np.ndarray)
        ):
            raise ValueError(
                "For binary inputs, y_pred and y_real should be list or tuple, "
                "or numpy array of integers."
            )
        if len(y_pred) != len(y_real):
            raise ValueError(
                "For binary inputs, y_pred and y_real must be of the same length."
            )

        y_pred_ranges = _binary_to_ranges(y_pred)
        y_real_ranges = _binary_to_ranges(y_real)
    else:
        y_pred_ranges = y_pred
        y_real_ranges = y_real

    # Flattening y_pred and y_real to resolve nested lists
    flat_y_pred = _flatten_ranges(y_pred_ranges)
    flat_y_real = _flatten_ranges(y_real_ranges)

    return _ts_precision(flat_y_pred, flat_y_real, gamma, bias_type, alpha=alpha)


def _ts_precision(
    y_pred_ranges, y_real_ranges, gamma="one", bias_type="flat", alpha=0.0
):
    """
    Implement range-based precision for time series anomaly detection.

    Parameters
    ----------
    y_pred : list of predicted anomaly ranges: each tuple in the list represents an
        interval [start, end) of a detected anomaly.
    y_real : list of true anomaly ranges: each tuple in the list represents an interval
        [start, end) of a true anomaly.
    bias_type : str, default="flat"
        Type of bias to apply. Should be one of ["flat", "front", "middle", "back"].
    gamma : str, default="one"
        Cardinality type. Should be one of ["reciprocal", "one"].

    Returns
    -------
    float
        Range-based precision
    """
    if gamma not in ["reciprocal", "one"]:
        raise ValueError("Invalid gamma type for precision. Use 'reciprocal' or 'one'.")

    if bias_type not in [
        "flat",
        "front",
        "middle",
        "back",
    ]:
        raise ValueError(
            "Invalid bias type. Choose from ['flat', 'front', 'middle', 'back']."
        )

    total_overlap_reward = 0.0
    total_cardinality = 0

    for pred_range in y_pred_ranges:
        overlap_set = set()
        cardinality = 0

        for real_start, real_end in y_real_ranges:
            overlap_start = max(pred_range[0], real_start)
            overlap_end = min(pred_range[1], real_end)

            if overlap_start <= overlap_end:
                overlap_set.update(range(overlap_start, overlap_end + 1))
                cardinality += 1

        overlap_reward = _calculate_overlap_reward_precision(
            pred_range, overlap_set, bias_type
        )

        existence_reward = 1.0 if overlap_set else 0.0
        gamma_value = _gamma_select(cardinality, gamma)

        score_i = alpha * existence_reward + (1 - alpha) * (
            gamma_value * overlap_reward
        )

        total_overlap_reward += score_i

        total_cardinality += 1

    precision = total_overlap_reward / total_cardinality if total_cardinality else 0.0

    return precision


# TODO: remove in v1.3.0
@deprecated(
    version="1.2.0",
    reason="ts_recall is deprecated and will be removed in v1.3.0. "
    "Please use range_recall instead.",
    category=FutureWarning,
)
def ts_recall(y_pred, y_real, gamma="one", bias_type="flat", alpha=0.0):
    """
    Calculate Recall for time series anomaly detection.

    Recall measures the proportion of correctly predicted anomaly positions
    out of all the real/actual (ground truth) anomaly positions, aggregated across the
    entire time series.

    Parameters
    ----------
    y_pred : list of tuples or binary sequence
        The predicted anomaly ranges.
        - For range-based input, each tuple represents a range (start, end) of the
          anomaly where start is starting index (inclusive) and end is ending index
          (inclusive).
        - For binary inputs, the sequence should contain integers (0 or 1), where 1
          indicates an anomaly. In this case, y_pred and y_real must be of same length.
    y_real : list of tuples, list of lists of tuples or binary sequence
        The real/actual (ground truth) ranges.
        - For range-based input, each tuple represents a range (start, end) of the
          anomaly where start is starting index (inclusive) and end is ending index
          (inclusive).
        - If y_real is in the format of list of lists, they will be flattened into a
          single list of tuples bringing it to the above format.
        - For binary inputs, the sequence should contain integers (0 or 1), where 1
          indicates an anomaly. In this case, y_pred and y_real must be of same length.
    gamma : str, default="one"
        Cardinality type. Should be one of ["reciprocal", "one", "udf_gamma"].
    bias_type : str, default="flat"
        Type of bias to apply. Should be one of ["flat", "front", "middle", "back"].
    alpha : float, default: 0.0
        Weight for existence reward in recall calculation.

    Returns
    -------
    float
        Recall

    Raises
    ------
    ValueError
        If input sequence is binary and y_real and y_pred are of different lengths.

    References
    ----------
    .. [1] Tatbul, Nesime, Tae Jun Lee, Stan Zdonik, Mejbah Alam,and Justin Gottschlich.
       "Precision and Recall for Time Series." 32nd Conference on Neural Information
       Processing Systems (NeurIPS 2018), Montréal, Canada.
       http://papers.nips.cc/paper/7462-precision-and-recall-for-time-series.pdf
    """
    is_binary = False
    if isinstance(y_pred, (list, tuple, np.ndarray)) and isinstance(
        y_pred[0], (int, bool, np.integer)
    ):
        is_binary = True
    if isinstance(y_real, (list, tuple, np.ndarray)) and isinstance(
        y_real[0], (int, bool, np.integer)
    ):
        is_binary = True

    if is_binary:
        if not isinstance(y_pred, (list, tuple, np.ndarray)) or not isinstance(
            y_real, (list, tuple, np.ndarray)
        ):
            raise ValueError(
                "For binary inputs, y_pred and y_real should be list or tuple, "
                "or numpy array of integers."
            )
        if len(y_pred) != len(y_real):
            raise ValueError(
                "For binary inputs, y_pred and y_real must be of the same length."
            )

        y_pred_ranges = _binary_to_ranges(y_pred)
        y_real_ranges = _binary_to_ranges(y_real)
    else:
        y_pred_ranges = y_pred
        y_real_ranges = y_real

    # Flattening y_pred and y_real to resolve nested lists
    flat_y_pred = _flatten_ranges(y_pred_ranges)
    flat_y_real = _flatten_ranges(y_real_ranges)
    return _ts_recall(flat_y_pred, flat_y_real, gamma, bias_type, alpha)


def _ts_recall(y_pred_ranges, y_real_ranges, gamma="one", bias_type="flat", alpha=0.0):
    """
    Implement range-based recall for time series anomaly detection.

    Parameters
    ----------
    y_pred : list of predicted anomaly ranges: each tuple in the list represents an
        interval [start, end) of a detected anomaly.
    y_real : list of true anomaly ranges: each tuple in the list represents an interval
        [start, end) of a true anomaly.
    gamma : str, default="one"
        Cardinality type. Should be one of ["reciprocal", "one", "udf_gamma"].
    bias_type : str, default="flat"
        Type of bias to apply. Should be one of ["flat", "front", "middle", "back"].
    alpha : float, default: 0.0
        Weight for existence reward in recall calculation.

    Returns
    -------
    float
        Range-based recall
    """
    if gamma not in ["reciprocal", "one", "udf_gamma"]:
        raise ValueError("Invalid gamma type for precision. Use 'reciprocal' or 'one'.")

    if bias_type not in [
        "flat",
        "front",
        "middle",
        "back",
    ]:
        raise ValueError(
            "Invalid bias type. Choose from ['flat', 'front', 'middle', 'back']."
        )

    total_overlap_reward = 0.0

    for real_range in y_real_ranges:
        overlap_set = set()
        cardinality = 0

        for pred_range in y_pred_ranges:
            overlap_start = max(real_range[0], pred_range[0])
            overlap_end = min(real_range[1], pred_range[1])

            if overlap_start <= overlap_end:
                overlap_set.update(range(overlap_start, overlap_end + 1))
                cardinality += 1

        existence_reward = 1.0 if overlap_set else 0.0

        if overlap_set:
            overlap_reward = _calculate_overlap_reward_recall(
                real_range, overlap_set, bias_type
            )
            gamma_value = _gamma_select(cardinality, gamma)
            overlap_reward *= gamma_value
        else:
            overlap_reward = 0.0

        recall_score = alpha * existence_reward + (1 - alpha) * overlap_reward
        total_overlap_reward += recall_score

    recall = total_overlap_reward / len(y_real_ranges) if y_real_ranges else 0.0
    return recall


# TODO: remove in v1.3.0
@deprecated(
    version="1.2.0",
    reason="ts_fscore is deprecated and will be removed in v1.3.0. "
    "Please use range_f_score instead.",
    category=FutureWarning,
)
def ts_fscore(
    y_pred,
    y_real,
    gamma="one",
    p_bias="flat",
    r_bias="flat",
    p_alpha=0.0,
    r_alpha=0.0,
    beta=1.0,
):
    """
    Calculate F1-Score for time series anomaly detection.

    F-1 Score is the harmonic mean of Precision and Recall, providing
    a single metric to evaluate the performance of an anomaly detection model.

    Parameters
    ----------
    y_pred : list of tuples or binary sequence
        The predicted anomaly ranges.
        - For range-based input, each tuple represents a range (start, end) of the
          anomaly where start is starting index (inclusive) and end is ending index
          (inclusive).
        - For binary inputs, the sequence should contain integers (0 or 1), where 1
          indicates an anomaly. In this case, y_pred and y_real must be of same length.
    y_real : list of tuples, list of lists of tuples or binary sequence
        The real/actual (ground truth) ranges.
        - For range-based input, each tuple represents a range (start, end) of the
          anomaly where start is starting index (inclusive) and end is ending index
          (inclusive).
        - If y_real is in the format of list of lists, they will be flattened into a
          single list of tuples bringing it to the above format.
        - For binary inputs, the sequence should contain integers (0 or 1), where 1
          indicates an anomaly. In this case, y_pred and y_real must be of same length.
    gamma : str, default="one"
        Cardinality type. Should be one of ["reciprocal", "one", "udf_gamma"].
    p_bias : str, default="flat"
        Type of bias to apply for precision.
        Should be one of ["flat", "front", "middle", "back"].
    r_bias : str, default="flat"
        Type of bias to apply for recall.
        Should be one of ["flat", "front", "middle", "back"].
    p_alpha : float, default=0.0
        Weight for existence reward in Precision calculation.
    r_alpha : float, default=0.0
        Weight for existence reward in Recall calculation.
    beta : float, default=1.0
        F-score beta determines the weight of recall in the combined score.
        beta < 1 lends more weight to precision, while beta > 1 favors recall.

    Returns
    -------
    float
        F1-Score

    References
    ----------
    .. [1] Tatbul, Nesime, Tae Jun Lee, Stan Zdonik, Mejbah Alam,and Justin Gottschlich.
       "Precision and Recall for Time Series." 32nd Conference on Neural Information
       Processing Systems (NeurIPS 2018), Montréal, Canada.
       http://papers.nips.cc/paper/7462-precision-and-recall-for-time-series.pdf
    """
    precision = ts_precision(y_pred, y_real, gamma, p_bias, alpha=p_alpha)
    recall = ts_recall(y_pred, y_real, gamma, r_bias, r_alpha)

    if precision + recall > 0:
        fscore = ((1 + beta**2) * (precision * recall)) / (beta**2 * precision + recall)
    else:
        fscore = 0.0

    return fscore
