import json
from pathlib import Path


# ================== CONFIGURAZIONE ==================
INPUT_FILE = "telemetry_useful__grouped_ordered_filtered_merged.json"
OUTPUT_FILE = "telemetry_useful__grouped_ordered_filtered_merged_cleaned.json"
# ====================================================


def _get_event_time(ev):
    """
    Restituisce il timestamp principale dell'evento.
    Preferisce `timestamp_game` se disponibile, altrimenti `time`.
    """
    ts_game = ev.get("timestamp_game")
    if ts_game is not None:
        return ts_game
    return ev.get("time")


def _merge_intervals(intervals):
    """
    Unisce intervalli sovrapposti o adiacenti.

    Intervalli sono coppie (start, end) con start < end.
    """
    if not intervals:
        return []

    intervals_sorted = sorted(intervals, key=lambda x: x[0])
    merged = []
    cur_start, cur_end = intervals_sorted[0]

    for start, end in intervals_sorted[1:]:
        if start <= cur_end:
            # Sovrapposto o adiacente -> estendiamo
            cur_end = max(cur_end, end)
        else:
            merged.append((cur_start, cur_end))
            cur_start, cur_end = start, end

    merged.append((cur_start, cur_end))
    return merged


def _compute_menu_intervals(events):
    """
    Dato l'elenco di eventi ordinati, costruisce gli intervalli di menu.

    Regole:
    - Intervalli base: (MenuOpen, MenuClose) usando il timestamp principale.
    - Scarta i MenuClose che arrivano dopo almeno una SCENE_TRANSITION
      successiva al corrispondente MenuOpen.
    - Se un MenuOpen non ha un MenuClose valido, utilizza la durata
      dell'ultimo intervallo menu completo per chiuderlo in maniera
      sintetica (se esiste).
    - Se si incontra un MenuClose senza MenuOpen, crea un intervallo
      sintetico usando la durata dell'ultimo intervallo completo (se esiste).
    """
    intervals = []
    current_open_start = None
    has_scene_transition_after_open = False
    last_valid_duration = None

    for ev in events:
        action_name = ev.get("actionName")
        ts = _get_event_time(ev)
        if ts is None:
            continue

        if action_name == "MenuOpen":
            current_open_start = ts
            has_scene_transition_after_open = False

        elif action_name == "SCENE_TRANSITION":
            if current_open_start is not None:
                has_scene_transition_after_open = True

        elif action_name == "MenuClose":
            # Caso 1: MenuClose con corrispondente MenuOpen "aperto"
            if current_open_start is not None:
                if has_scene_transition_after_open:
                    # Scartiamo questo close: l'intervallo rimane aperto e
                    # verrà eventualmente chiuso in modo sintetico.
                    continue

                # Intervallo valido
                if ts > current_open_start:
                    duration = float(ts) - float(current_open_start)
                    intervals.append((current_open_start, ts))
                    last_valid_duration = duration

                current_open_start = None
                has_scene_transition_after_open = False

            # Caso 2: MenuClose senza MenuOpen corrente -> usa ultimo intervallo valido
            elif last_valid_duration is not None:
                start = ts - last_valid_duration
                if start < ts:
                    intervals.append((start, ts))

    # Fine eventi: se abbiamo un MenuOpen rimasto aperto, chiudiamo in modo sintetico
    if current_open_start is not None and last_valid_duration is not None:
        end = current_open_start + last_valid_duration
        if end > current_open_start:
            intervals.append((current_open_start, end))

    return _merge_intervals(intervals)


def _filter_and_add_actual_timestamp(events, menu_intervals):
    """
    Rimuove tutte le azioni all'interno degli intervalli di menu e aggiunge
    `actual_timestamp` alle azioni rimanenti, sottraendo il tempo di menu
    precedente.

    Convenzione:
    - Un'azione è considerata "dentro il menu" se il suo timestamp è
      strettamente compreso tra start e end (start < t < end).
      Gli eventi esattamente a start o end vengono mantenuti.
    - SCENE_TRANSITION viene sempre preservato anche se cade dentro un
      intervallo di menu, in quanto evento critico di sistema.
    - `actual_timestamp` = timestamp_originale - somma_durata_intervalli
      completamente terminati PRIMA di quell'azione.
    """
    if not menu_intervals:
        # Nessun intervallo: copiamo solo gli eventi aggiungendo actual_timestamp = timestamp
        cleaned = []
        for ev in events:
            ts = _get_event_time(ev)
            if ts is None:
                cleaned.append(dict(ev))
            else:
                new_ev = dict(ev)
                new_ev["actual_timestamp"] = float(ts)
                cleaned.append(new_ev)
        return cleaned

    cleaned_events = []

    intervals = menu_intervals
    idx_interval = 0
    cumulative_removed = 0.0

    for ev in events:
        ts = _get_event_time(ev)
        if ts is None:
            # Non possiamo posizionare temporalmente questo evento: lo teniamo senza correzione
            cleaned_events.append(dict(ev))
            continue

        ts = float(ts)

        # Aggiorna la quantità di tempo "rimossa" considerando tutti gli intervalli
        # completamente terminati prima del timestamp corrente.
        while idx_interval < len(intervals) and intervals[idx_interval][1] <= ts:
            start, end = intervals[idx_interval]
            cumulative_removed += float(end) - float(start)
            idx_interval += 1

        # Verifica se l'evento cade dentro l'intervallo corrente (se esiste):
        # consideriamo "dentro" solo se start < ts < end.
        # Eccezione: SCENE_TRANSITION viene sempre preservato anche se dentro al menu.
        if idx_interval < len(intervals):
            start, end = intervals[idx_interval]
            if start < ts < end:
                action_name = ev.get("actionName")
                if action_name == "SCENE_TRANSITION":
                    # SCENE_TRANSITION è un evento critico di sistema: lo preserviamo sempre
                    pass
                else:
                    # Azione dentro al menu: la scartiamo.
                    continue

        new_ev = dict(ev)
        new_ev["actual_timestamp"] = ts - cumulative_removed
        cleaned_events.append(new_ev)

    return cleaned_events


def _extract_quiz_answers(events):
    """
    Estrae le risposte al questionario in ordine, assegnando gli identificatori:
    tech_issues, severe_issues, challenge, continue.
    """
    identifiers = ["tech_issues", "severe_issues", "challenge", "continue"]
    answers = []
    idx = 0
    for ev in events:
        if ev.get("actionName") == "QuizAnswer":
            if idx < len(identifiers):
                answers.append(
                    {
                        "identifier": identifiers[idx],
                        "value": ev.get("value"),
                    }
                )
                idx += 1
            else:
                answers.append({"value": ev.get("value")})
    return answers


def _normalize_yes_no(value):
    if value is None:
        return None
    v = str(value).strip().lower()
    if v in {"yes", "y", "true", "1"}:
        return True
    if v in {"no", "n", "false", "0"}:
        return False
    return None


def _normalize_continue(value):
    # Stessa logica di _normalize_yes_no, ma nominata in modo esplicito per chiarezza.
    return _normalize_yes_no(value)


def _normalize_challenge(value):
    if value is None:
        return None
    v = str(value).strip().lower().replace(" ", "")

    if "too" in v and "easy" in v:
        return "tooeasy"
    if "too" in v and ("difficult" in v or "hard" in v):
        return "toodifficult"
    if "optimal" in v or "justright" in v or "justrightdifficulty" in v:
        return "optimal"

    return None


def _has_complete_valid_quiz(events):
    """
    Ritorna True solo se il giocatore ha tutte e 4 le risposte al quiz
    con valori riconosciuti.
    """
    quiz_answers = _extract_quiz_answers(events)
    identifiers = ["tech_issues", "severe_issues", "challenge", "continue"]

    # Deve esserci almeno una risposta per ciascun identificatore previsto.
    answers_by_id = {
        ans.get("identifier"): ans.get("value")
        for ans in quiz_answers
        if ans.get("identifier") in identifiers
    }

    if any(key not in answers_by_id for key in identifiers):
        return False

    # Validazione dei valori
    if _normalize_yes_no(answers_by_id["tech_issues"]) is None:
        return False
    if _normalize_yes_no(answers_by_id["severe_issues"]) is None:
        return False
    if _normalize_challenge(answers_by_id["challenge"]) is None:
        return False
    if _normalize_continue(answers_by_id["continue"]) is None:
        return False

    return True


def clean_menu_intervals():
    """
    Carica `telemetry_useful__grouped_ordered_filtered_merged.json` e,
    per ciascun utente:

    - costruisce gli intervalli di menu (MenuOpen/MenuClose), scartando
      i MenuClose successivi a SCENE_TRANSITION
    - chiude eventuali intervalli incompleti usando la durata
      dell'ultimo intervallo completo, se disponibile
    - rimuove tutte le azioni all'interno degli intervalli di menu
    - aggiunge ad ogni azione rimanente il campo `actual_timestamp`,
      pari al timestamp originale con il tempo di menu precedente sottratto
    """
    script_dir = Path(__file__).parent
    processed_dir = script_dir / "processed"
    input_file = processed_dir / INPUT_FILE

    if not input_file.exists():
        raise FileNotFoundError(f"File non trovato: {input_file}")

    output_file = processed_dir / OUTPUT_FILE
    output_file.parent.mkdir(parents=True, exist_ok=True)

    with input_file.open("r", encoding="utf-8") as f:
        grouped = json.load(f)

    cleaned_grouped = {}

    for user_id, events in grouped.items():
        if not events:
            continue

        menu_intervals = _compute_menu_intervals(events)
        cleaned_events = _filter_and_add_actual_timestamp(events, menu_intervals)

        # Filtra i giocatori che non hanno tutte e 4 le risposte al quiz
        # oppure hanno valori non riconosciuti.
        if not _has_complete_valid_quiz(cleaned_events):
            continue

        cleaned_grouped[user_id] = cleaned_events

    with output_file.open("w", encoding="utf-8") as f:
        json.dump(cleaned_grouped, f, indent=2)

    print(f"File con timeline senza menu salvato come: {output_file}")
    return output_file


if __name__ == "__main__":
    clean_menu_intervals()



