from __future__ import annotations
from typing import Dict, Text, Union, List, Tuple
from rasa.utils.io import WriteRow
from pathlib import Path
import csv

import numpy as np


from rasa.core.evaluation.marker_base import EventMetaData


def compute_statistics(
    values: List[Union[float, int]]
) -> Dict[Text, Union[int, np.float]]:
    """Computes some statistics over the given numbers."""
    return {
        "count": len(values) if values else 0,
        "mean": np.mean(values) if values else np.nan,
        "median": np.median(values) if values else np.nan,
        "min": min(values) if values else np.nan,
        "max": max(values) if values else np.nan,
    }


class MarkerStatistics:
    """Computes some statistics on marker extraction results.

    (1) Number of sessions where markers apply:

    For each marker, we compute the total number (as well as the percentage) of
    sessions in which a marker applies at least once.
    Moreover, we output the total number of sessions that were parsed.

    (2) Number of user turns preceding relevant events - per sessions:

    For each marker, we consider all relevant events where that marker applies.
    Everytime a marker applies, we check how many user turns precede that event.
    We collect all these numbers and compute basic statistics (e.g. count and mean)
    on them.

    This means, per session, we compute how often a marker applies and how many
    user turns precede a relevant marker application on average, in that session.

    (3) Number of user turns preceding relevant events - over all sessions:

    Here, for each marker, we consider all relevant events where a marker applies
    *in any of the sessions*. Then, we again calculate basic statistics over the
    respective number of user turns that precede each of these events.

    This means, we compute how many events the marker applies in total and we
    compute an estimate of the expected number of user turns preceding that
    precede an (relevant) event where a marker applies.
    """

    NO_MARKER = "-"
    STAT_NUM_SESSIONS = "total_number_of_sessions"
    STAT_NUM_SESSIONS_WHERE_APPLIES = (
        "number_of_sessions_where_marker_applied_at_least_once"
    )
    STAT_PERCENTAGE_SESSIONS_WHERE_APPLIES = (
        "percentage_of_sessions_where_marker_applied_at_least_once"
    )

    @staticmethod
    def _add_num_user_turns_str_to(stat_name: Text) -> Text:
        return f"{stat_name}(number of preceding user turns)"

    ALL_SESSIONS = np.nan
    ALL_SENDERS = "all"

    def __init__(self) -> None:
        """Creates a new marker statistics object."""
        # to ensure consistency of processed rows
        self._marker_names = []

        # (1) For collecting the per-session analysis:
        # NOTE: we could stream / compute them later instead of collecting them...
        self.session_results: Dict[Text, Dict[Text, List[Union[int, float]]]] = {}
        self.session_identifier: List[Tuple[Text, int]] = []

        # (2) For the overall statistics:
        self.num_preceding_user_turns_collected: Dict[Text, List[int]] = {}
        self.count_if_applied_at_least_once: Dict[Text, int] = {}
        self.num_sessions = 0

    def process(
        self,
        sender_id: Text,
        session_idx: int,
        meta_data_on_relevant_events_per_marker: Dict[Text, List[EventMetaData]],
    ) -> None:
        """Processes the meta data that was extracted from a single session.

        Internally, this method ..
        1. computes some statistics for the given meta data and saves it for later
        2. keeps track of the total number of sessions processed and the
           collects all metadata to be able to compute meta data over *all*

        Args:
            sender_id: an id that, together with the `session_idx` identifies
                the session from which the markers where extracted
            session_idx: an index that, together with the `sender_id` identifies
                the session from which the markers where extracted
            meta_data_on_relevant_events_per_marker: marker extraction results,
                i.e. a dictionary mapping
                marker names to the meta data describing relevant events
                for those markers
        """
        if len(self._marker_names) == 0:
            # sort and initialise here once so our result tables are sorted
            self._marker_names = sorted(meta_data_on_relevant_events_per_marker.keys())
            self.count_if_applied_at_least_once = {
                marker_name: 0 for marker_name in self._marker_names
            }
            self.num_preceding_user_turns_collected = {
                marker_name: [] for marker_name in self._marker_names
            }
            # NOTE: we could stream / compute them later instead of collecting them...
            stat_names = sorted(compute_statistics([]).keys())
            self.session_results = {
                marker_name: {stat_name: [] for stat_name in stat_names}
                for marker_name in self._marker_names
            }
        else:
            given_markers = meta_data_on_relevant_events_per_marker.keys()
            if set(given_markers) != set(self._marker_names):
                raise RuntimeError(
                    f"Expected all processed extraction results to contain information"
                    f"for the same set of markers. But found "
                    f"{sorted(given_markers)} which differs from "
                    f"the marker extracted so far (i.e. {sorted(self._marker_names)})."
                )

        # update session identifiers / count
        self.num_sessions += 1
        self.session_identifier.append((sender_id, session_idx))

        for marker_name, meta_data in meta_data_on_relevant_events_per_marker.items():

            num_preceding_user_turns = [
                event_meta_data.preceding_user_turns for event_meta_data in meta_data
            ]