
"""
Analisi qualitativa dei DIARI DI BORDO (eTwinning vs Controllo) con GUI Tkinter.

Cosa fa questo programma (end-to-end):
1) Ti chiede:
   - Cartella RADICE (contiene le 15 cartelle dei docenti; in ognuna: gennaio…maggio; in ogni mese 1 .docx)
   - Cartella di ESPORTAZIONE (dove salvare tutti i csv, grafici, KWIC, ecc.)
   - (Opzionale) File ZIP RISORSE (es. "2.zip" dai questionari) con liste/JSON di parole e pesi
   - (Opzionale) File STOPWORDS (es. "stopwords_it_derived_from_zip.txt")

2) Estrae da ciascun .docx *le sezioni* indicate dall'utente (eTwinning, Controllo, Riflessioni Docente)
   con ancore robuste (spazi/accapo variabili). Se un file non ha testo utile in nessun punto → escluso.

3) Calcola indicatori qualitativi per classe (etwinning/controllo):
   - Valenza positiva/negativa (lessici; supporta pesi se forniti nel 2.zip)
   - Tassonomia eventi (assenze/tecnico/relazionale/logistico) sui campi “Eventi…”
   - Evidenze per le 5 COMPETENZE CHIAVE (personale, cittadinanza, imprenditoriale,
     espressione culturale, digitale) – a livello “competenza”, NON aree
     (lessici modificabili via JSON; se nel 2.zip sono presenti liste/pesi, vengono integrate)
   - Profondità riflessiva della sezione “Riflessioni” (euristica 0–4)
   - Leggibilità Gulpease (testo totale classe e sola riflessione)

4) Produce dataset:
   - diari_parsed.csv     (testi estratti per docente×mese×classe_tipo)
   - features_diari.csv   (indicatori per riga)
   - diff_within_teacher.csv   (differenze etwinning–controllo entro docente×mese)
   - did_summary.csv           (Difference-in-Differences gennaio→maggio)
   - topics.csv / doc_topics.csv (se scikit-learn presente)

5) Crea GRAFICI e KWIC (e ti chiede dove salvarli):
   - Slope (mese) per valenza positiva e profondità riflessiva (media per gruppo)
   - Radar differenze evidenze competenze (eTwinning – Controllo)
   - Trend mensile tassonomia EVENTI (separati per gruppo)
   - Rete CO-OCCORRENZE (opzionale; se networkx presente)
   - KWIC per competenza (per gruppo), + “citazioni esemplari” (selezione euristica)

Requisiti consigliati:
  pip install python-docx pandas numpy scikit-learn matplotlib networkx
  (opzionale) per tokenizzazione/stopwords migliori:
  pip install spacy
  python -m spacy download it_core_news_md

Autore: 2025
"""

import os
import re
import io
import json
import math
import zipfile
import traceback
from pathlib import Path
from datetime import datetime
from collections import defaultdict, Counter

import tkinter as tk
from tkinter import filedialog, messagebox, ttk


import pandas as pd
import numpy as np


try:
    from docx import Document
except ImportError as e:
    raise SystemExit("Manca 'python-docx'. Installa con: pip install python-docx") from e


try:
    import matplotlib
    matplotlib.use("Agg")  
    import matplotlib.pyplot as plt
except Exception as e:
    raise SystemExit("Manca 'matplotlib'. Installa con: pip install matplotlib") from e


_SCIKIT_OK = True
try:
    from sklearn.feature_extraction.text import CountVectorizer
    from sklearn.decomposition import LatentDirichletAllocation
except Exception:
    _SCIKIT_OK = False


try:
    import spacy
    _NLP = spacy.load("it_core_news_md")
    IT_STOPWORDS = set(_NLP.Defaults.stop_words)
except Exception:
    _NLP = None
    IT_STOPWORDS = {
        "a","ad","al","allo","ai","agli","all","agl","alla","alle","con","col","coi","da","dal","dallo","dai","dagli","dall","dagl",
        "dalla","dalle","di","del","dello","dei","degli","dell","degl","della","delle","in","nel","nello","nei","negli","nell","negl",
        "nella","nelle","su","sul","sullo","sui","sugli","sull","sugl","sulla","sulle","per","tra","fra","io","tu","lui","lei","noi",
        "voi","loro","mi","ti","ci","vi","lo","la","li","le","gli","ne","il","un","uno","una","ma","ed","se","perché","anche","come",
        "dove","che","chi","cui","non","più","meno","o","e","i","gli","le","del","della","dei","delle","degli","dall","dalla","dai"
    }


try:
    import networkx as nx
    _NETWORKX_OK = True
except Exception:
    _NETWORKX_OK = False





MESI = ["gennaio", "febbraio", "marzo", "aprile", "maggio"]
ORDINE_MESE = {m:i for i,m in enumerate(MESI)}


RX = {
    "ETW_HEAD": re.compile(r"CLASSE\s+ETWINNING\s*:?", re.IGNORECASE),
    "CTRL_HEAD": re.compile(r"CLASSE\s+DI\s+CONTROLLO\s*:?", re.IGNORECASE),
    "DOC_HEAD": re.compile(r"Riflessione\s+personale\s+del\s+docente\s*:?", re.IGNORECASE),

    "BREVE": re.compile(r"Breve\s+descrizione\s+delle\s+attivit[aà]\s+svolte\s+nel\s+mese\s*:", re.IGNORECASE),
    "OBIETTIVI": re.compile(r"Obiettivi\s+raggiun[tT][iI]\s*\(o\s+in\s+via\s+di\s+sviluppo\)\s*:", re.IGNORECASE),
    "EVENTI": re.compile(r"Eventi\s+con\s+impatto\s+sulla\s+formazione\s+degli\s+studenti.*?:", re.IGNORECASE | re.DOTALL),
    "RIFL_CC": re.compile(r"Riflessioni\s+sullo\s+sviluppo\s+delle\s+Competenze\s+Chiave\s*\(.*?\)", re.IGNORECASE | re.DOTALL),

    "C1": re.compile(r"1\)\s*Competenza\s+Personale,\s*Sociale,\s*e\s*Imparare\s+ad\s+Imparare", re.IGNORECASE),
    "C2": re.compile(r"2\)\s*Competenza\s+Sociale\s+e\s+Civica\s+in\s+Ambito\s+di\s+Cittadinanza", re.IGNORECASE),
    
    "C3": re.compile(r"3\)\s*Competenze?\s+Imprenditoriale", re.IGNORECASE),
    "C4": re.compile(r"4\)\s*Competenza\s+nella\s+Consapevolezza\s+e\s+nell.?Espressione\s+Culturale", re.IGNORECASE),
    "C5": re.compile(r"5\)\s*Competenza\s+Digitale", re.IGNORECASE),
    
    "C6": re.compile(r"6\)\s*Competenza\s*Multilinguistica", re.IGNORECASE),

    "DOC_Q1": re.compile(r"Come\s+ti\s+senti\s+rispetto\s+al\s+percorso\s+svolto\s+finora\?.*?esperienza\?", re.IGNORECASE | re.DOTALL),
    "DOC_Q2": re.compile(r"In\s+che\s+modo\s+queste\s+attivit[aà]\s+stanno\s+influenzando\s+il\s+tuo\s+approccio\s+dida[tT]tico\s+e\s+il\s+tuo\s+sviluppo\s+professionale\?", re.IGNORECASE)
}


VALENZA_POS_STEMS = ["miglior", "cresc", "aument", "potenzi", "svilupp", "raggiunt", "superat", "consolid", "rinforz", "progred"]
VALENZA_NEG_STEMS = ["difficolt", "critic", "problem", "ritard", "ostacol", "cal", "riduz", "mancanz", "fatica", "conflitt"]

EVENT_TAXONOMY = {
    "eventi_assenze": ["assenza", "assent", "giustificat", "ingiustificat", "malatt"],
    "eventi_tecnico": ["connession", "piattaform", "hardware", "software", "tecnic", "rete", "login"],
    "eventi_relazionale": ["conflitt", "collaboraz", "grupp", "motivaz", "coinvolg", "partecipaz", "relazion"],
    "eventi_logistico": ["ritardo", "orari", "spazio", "laboratorio", "viaggio", "trasfer", "logistic"]
}


DEFAULT_CODEBOOK = {
    "version": "2.0",
    "note": "Mappatura stems → 5 competenze chiave utilizzate nei diari/questionari.",
    "competenze": {
        "personale": ["autonom", "autoregol", "organizz", "pianific", "responsab", "metacogn", "autostim", "resilien"],
        "cittadinanza": ["cittadin", "collabor", "cooper", "grupp", "ruol", "regol", "partecip", "decision", "inclus", "legalit"],
        "imprenditoriale": ["iniziativ", "intraprend", "progett", "scaden", "obiettivo", "problem", "soluz", "leadership", "risch", "pianific"],
        "culturale": ["espress", "presentaz", "narraz", "multimodal", "creativ", "patrimonio", "interpret", "consapevolezz"],
        "digitale": ["piattaform", "strument", "app", "software", "multimedia", "document", "presentaz", "sicurezz", "privacy", "copyright", "fonte", "dato", "ricerc"]
    }
}





def read_docx_text(filepath: Path) -> str:
    """Legge un .docx e concatena paragrafi e celle di tabella separati da newline."""
    doc = Document(str(filepath))
    paras = [p.text for p in doc.paragraphs]
    
    for table in doc.tables:
        for row in table.rows:
            for cell in row.cells:
                paras.append(cell.text)
    return "\n".join(paras)

def find_once(pattern: re.Pattern, text: str, start: int = 0):
    m = pattern.search(text, start)
    if m:
        return m.start(), m.end()
    return None, None

def slice_between(text: str, start_pat: re.Pattern, end_pat: re.Pattern = None, start_idx: int = 0, fallback_end: int = None):
    s0, s1 = find_once(start_pat, text, start=start_idx)
    if s0 is None:
        return "", start_idx
    if end_pat is not None:
        e0, e1 = find_once(end_pat, text, start=s1)
        if e0 is None:
            end = fallback_end if fallback_end is not None else len(text)
        else:
            end = e0
    else:
        end = fallback_end if fallback_end is not None else len(text)
    return text[s1:end].strip(), end

def normalize_ws(s: str) -> str:
    return re.sub(r"\s+", " ", s).strip()

def tokenize_it(s: str, extra_stopwords=None):
    """Tokenizza in minuscolo, lettere accentate; rimuove stopwords."""
    if not s:
        return []
    s = s.lower().replace("’", "'")
    tokens = re.findall(r"[a-zA-ZÀ-ÖØ-öø-ÿ']+", s)
    stop = set(IT_STOPWORDS)
    if extra_stopwords:
        stop |= set(extra_stopwords)
    return [t for t in tokens if t not in stop]

def gulpease(text: str) -> float:
    if not text:
        return float("nan")
    letters = sum(1 for c in text if c.isalpha())
    words = max(1, len(re.findall(r"\b\w+\b", text)))
    sentences = max(1, len(re.findall(r"[\.!?…]+", text)))
    return 89 + (300 * sentences - 10 * letters) / words

def reflective_depth_score(text: str) -> int:
    if not text or not text.strip():
        return 0
    tl = text.lower()
    levels = {
        1: [r"\b(ho|abbiamo)\s+fatto\b", r"\bdescriv"],
        2: [r"\bperch[eé]\b", r"caus", r"motivo", r"osserv"],
        3: [r"ha\s+funzionato", r"non\s+ha\s+funzionato", r"valut", r"efficac", r"risultat"],
        4: [r"modific", r"prossim[ei]\s+pass[ii]|prossime\s+azioni", r"implicaz", r"trasferibil"]
    }
    score = 0
    for lvl, kws in levels.items():
        if any(re.search(p, tl) for p in kws):
            score = max(score, lvl)
    return score





def load_resources_zip(zip_path: Path):
    """
    Carica RES nel formato libero (heuristics):
      - JSON/TXT con parole/pesi per:
          * valenza_positive, valenza_negative
          * eventi: {categoria: [parole]} o {categoria: {"parole": [...], "peso": ...}}
          * competenze: {personale: [...], cittadinanza: [...], imprenditoriale: [...], culturale: [...], digitale: [...]}
    Torna un dict con chiavi: val_pos, val_neg, eventi (dict), competenze (dict), extra_stopwords (list)
    I pesi (se presenti) sono dict parola->peso (float). In mancanza → peso 1.0.
    """
    if not zip_path or not zip_path.exists():
        return {
            "val_pos": {},
            "val_neg": {},
            "eventi": {},
            "competenze": {},
            "extra_stopwords": []
        }

    val_pos = {}
    val_neg = {}
    eventi = {}
    competenze = {}
    extra_stop = []

    def merge_weighted(target: dict, words):
        
        if isinstance(words, dict):
            for w, wt in words.items():
                w = str(w).strip().lower()
                try:
                    wt = float(wt)
                except Exception:
                    wt = 1.0
                target[w] = target.get(w, 0.0) + wt
        elif isinstance(words, list):
            for w in words:
                w = str(w).strip().lower()
                if not w:
                    continue
                target[w] = target.get(w, 0.0) + 1.0

    def merge_list(target: dict, key: str, words):
        if key not in target:
            target[key] = {}
        merge_weighted(target[key], words)

    with zipfile.ZipFile(zip_path, 'r') as zf:
        for name in zf.namelist():
            try:
                with zf.open(name) as f:
                    data = f.read()
                stem = name.lower()
                if stem.endswith(".json"):
                    obj = json.loads(data.decode("utf-8"))
                    
                    if isinstance(obj, dict):
                        
                        for k in obj.keys():
                            kl = k.lower()
                            if "valenza" in kl or "sentiment" in kl or "positivo" in kl or "negativo" in kl:
                                
                                if "pos" in kl or "positivo" in kl:
                                    merge_weighted(val_pos, obj[k])
                                elif "neg" in kl or "negativo" in kl:
                                    merge_weighted(val_neg, obj[k])
                        
                        if "eventi" in obj or "events" in obj:
                            evmap = obj.get("eventi", obj.get("events", {}))
                            if isinstance(evmap, dict):
                                for cat, words in evmap.items():
                                    merge_list(eventi, str(cat), words)
                        
                        if "competenze" in obj or "competences" in obj:
                            cmap = obj.get("competenze", obj.get("competences", {}))
                            if isinstance(cmap, dict):
                                for comp, words in cmap.items():
                                    comp = comp.strip().lower()
                                    if comp.startswith("pers"): comp = "personale"
                                    elif comp.startswith("citt"): comp = "cittadinanza"
                                    elif comp.startswith("impr"): comp = "imprenditoriale"
                                    elif comp.startswith("cult"): comp = "culturale"
                                    elif comp.startswith("digit"): comp = "digitale"
                                    merge_list(competenze, comp, words)
                        
                        if "stopwords" in obj:
                            if isinstance(obj["stopwords"], list):
                                extra_stop.extend([str(x).strip().lower() for x in obj["stopwords"]])
                    elif isinstance(obj, list):
                        
                        if "pos" in stem:
                            merge_weighted(val_pos, obj)
                        elif "neg" in stem:
                            merge_weighted(val_neg, obj)
                elif stem.endswith(".txt"):
                    
                    txt = data.decode("utf-8", errors="ignore")
                    lines = [ln.strip() for ln in txt.splitlines() if ln.strip()]
                    if "stopword" in stem:
                        extra_stop.extend([ln.lower() for ln in lines])
                    elif any(k in stem for k in ["pos","positivo","positive"]):
                        merge_weighted(val_pos, lines)
                    elif any(k in stem for k in ["neg","negativo","negative"]):
                        merge_weighted(val_neg, lines)
                    elif any(k in stem for k in ["event"]):
                        
                        merge_list(eventi, "eventi_generico", lines)
                    elif any(k in stem for k in ["comp", "persona", "cittad", "impren", "cultur", "digit"]):
                        
                        comp_key = "misc"
                        if "persona" in stem: comp_key = "personale"
                        elif "cittad" in stem: comp_key = "cittadinanza"
                        elif "impren" in stem: comp_key = "imprenditoriale"
                        elif "cultur" in stem: comp_key = "culturale"
                        elif "digit" in stem: comp_key = "digitale"
                        merge_list(competenze, comp_key, lines)
            except Exception:
                
                pass

    return {
        "val_pos": val_pos,
        "val_neg": val_neg,
        "eventi": eventi,
        "competenze": competenze,
        "extra_stopwords": extra_stop
    }

def load_stopwords_file(path: Path):
    if not path or not path.exists():
        return []
    txt = Path(path).read_text(encoding="utf-8", errors="ignore")
    return [ln.strip().lower() for ln in txt.splitlines() if ln.strip()]





def extract_sections_from_text(full_text: str) -> dict:
    text = full_text

    _, etw_end = find_once(RX["ETW_HEAD"], text, 0)
    ctrl_start, ctrl_end = find_once(RX["CTRL_HEAD"], text, 0)
    doc_start, doc_end = find_once(RX["DOC_HEAD"], text, 0)

    etw_section = ""
    ctrl_section = ""
    doc_section = ""

    if etw_end is not None and ctrl_start is not None:
        etw_section = text[etw_end:ctrl_start]
    if ctrl_end is not None and (doc_start is not None):
        ctrl_section = text[ctrl_end:doc_start]
    elif ctrl_end is not None:
        ctrl_section = text[ctrl_end:]
    if doc_end is not None:
        doc_section = text[doc_end:]
    elif doc_start is not None:
        doc_section = text[doc_start:]

    def extract_class_section(sec_text: str) -> dict:
        out = {}
        if not sec_text:
            out["attivita"] = ""
            out["obiettivi"] = ""
            out["problemi"] = ""
            out["riflessioni"] = ""
            for i in range(1,7):
                out[f"c{i}"] = ""
            return out

        cur = 0
        out["attivita"], cur = slice_between(sec_text, RX["BREVE"], RX["OBIETTIVI"], start_idx=cur)
        out["obiettivi"], cur = slice_between(sec_text, RX["OBIETTIVI"], RX["EVENTI"], start_idx=cur)
        out["problemi"], cur = slice_between(sec_text, RX["EVENTI"], RX["RIFL_CC"], start_idx=cur)
        out["riflessioni"], cur = slice_between(sec_text, RX["RIFL_CC"], RX["C1"], start_idx=cur)

        out["c1"], cur = slice_between(sec_text, RX["C1"], RX["C2"], start_idx=cur)
        out["c2"], cur = slice_between(sec_text, RX["C2"], RX["C3"], start_idx=cur)
        out["c3"], cur = slice_between(sec_text, RX["C3"], RX["C4"], start_idx=cur)
        out["c4"], cur = slice_between(sec_text, RX["C4"], RX["C5"], start_idx=cur)
        out["c5"], cur = slice_between(sec_text, RX["C5"], RX["C6"], start_idx=cur)
        c6_txt, _ = slice_between(sec_text, RX["C6"], None, start_idx=cur, fallback_end=len(sec_text))
        out["c6"] = c6_txt
        return out

    etw = extract_class_section(etw_section)
    ctrl = extract_class_section(ctrl_section)

    
    rifl_doc = ""
    impatto_doc = ""
    if doc_section:
        s0, s1 = find_once(RX["DOC_Q1"], doc_section, 0)
        s2, s3 = find_once(RX["DOC_Q2"], doc_section, 0)
        if s1 is not None and s2 is not None:
            rifl_doc = doc_section[s1:s2].strip()
            impatto_doc = doc_section[s3:].strip() if s3 is not None else doc_section[s2:].strip()
        elif s1 is not None:
            rifl_doc = doc_section[s1:].strip()
            impatto_doc = ""
        else:
            rifl_doc = doc_section.strip()

    all_texts = list(etw.values()) + list(ctrl.values()) + [rifl_doc, impatto_doc]
    empty = all(len(t.strip()) == 0 for t in all_texts)

    return {
        "__empty__": empty,
        "etw": etw,
        "ctrl": ctrl,
        "docente_riflessione": rifl_doc,
        "docente_impatto": impatto_doc
    }





def stems_or_words_to_weighted_dict(obj):
    """
    Converte un oggetto 'obj' (list[str] o dict[str->peso]) in dict[str->float]
    """
    out = {}
    if isinstance(obj, dict):
        for w, wt in obj.items():
            w = str(w).strip().lower()
            if not w:
                continue
            try:
                wt = float(wt)
            except Exception:
                wt = 1.0
            out[w] = out.get(w, 0.0) + wt
    elif isinstance(obj, list):
        for w in obj:
            w = str(w).strip().lower()
            if not w:
                continue
            out[w] = out.get(w, 0.0) + 1.0
    return out

def count_weighted_stems(tokens, weighted_dict):
    """
    Conta occorrenze pesate: se un token inizia con uno stem, somma il peso.
    weighted_dict: dict[str(stem)->float(peso)]
    """
    if not weighted_dict:
        return 0.0
    stems = list(weighted_dict.keys())
    total = 0.0
    for tok in tokens:
        for st in stems:
            if tok.startswith(st):
                total += weighted_dict[st]
                break
    return float(total)

def compute_features_for_row(text_total: str, text_rifl: str, event_text: str,
                             comp_lexicon_weighted: dict,
                             val_pos_w: dict, val_neg_w: dict,
                             eventi_w: dict,
                             extra_stopwords=None) -> dict:
    """
    text_total: tutto il testo della classe
    text_rifl: riflessioni classe
    event_text: solo sezione eventi (per tassonomia)
    comp_lexicon_weighted: dict{competenza->[stems->peso]}
    val_pos_w/val_neg_w: dict stem->peso
    eventi_w: dict{categoria->dict stem->peso}
    """
    toks_total = tokenize_it(text_total, extra_stopwords)
    toks_event = tokenize_it(event_text or "", extra_stopwords)

    feats = {}
    
    feats["valenza_pos"] = count_weighted_stems(toks_total, val_pos_w)
    feats["valenza_neg"] = count_weighted_stems(toks_total, val_neg_w)

    
    for cat, stems_w in eventi_w.items():
        feats[cat] = count_weighted_stems(toks_event, stems_w)

    
    for comp in ["personale","cittadinanza","imprenditoriale","culturale","digitale"]:
        stems_w = comp_lexicon_weighted.get(comp, {})
        feats[f"ev_{comp}"] = count_weighted_stems(toks_total, stems_w)

    
    feats["profondita_rifl_classe"] = reflective_depth_score(text_rifl or "")

    
    feats["gulpease_total"] = gulpease(text_total)
    feats["gulpease_rifl"] = gulpease(text_rifl or "")
    return feats





def make_stopwords_for_vectorizer(extra_stopwords=None):
    stop = set(IT_STOPWORDS)
    if extra_stopwords:
        stop |= set(extra_stopwords)
    
    stop |= {"l","dell","nell","all","sul","dalla","dalle","dai","dagli","agli"}
    return list(stop)

def topic_modeling(corpus_texts, extra_stopwords=None, n_topics=8, random_state=42):
    if not _SCIKIT_OK or len(corpus_texts) == 0:
        return [], None
    stopw = make_stopwords_for_vectorizer(extra_stopwords)
    cv = CountVectorizer(min_df=3, max_df=0.8, ngram_range=(1,2), stop_words=stopw)
    X = cv.fit_transform(corpus_texts)
    if X.shape[0] == 0 or X.shape[1] == 0:
        return [], None
    lda = LatentDirichletAllocation(n_components=n_topics, random_state=random_state)
    W = lda.fit_transform(X)
    H = lda.components_
    terms = np.array(cv.get_feature_names_out())
    top_terms = []
    for k in range(n_topics):
        top_idx = H[k].argsort()[-12:][::-1]
        top_terms.append(terms[top_idx].tolist())
    return top_terms, W





def kwic_one(text, keyword, window=40):
    if not text or not keyword:
        return []
    low = text.lower()
    k = keyword.lower()
    idxs = [m.start() for m in re.finditer(re.escape(k), low)]
    out = []
    for i in idxs:
        left = text[max(0, i-window): i]
        match = text[i: i+len(keyword)]
        right = text[i+len(keyword): i+len(keyword)+window]
        out.append((left.strip(), match, right.strip()))
    return out

def generate_kwic_exports(df_text, comp_lexicon_weighted: dict, export_dir: Path, window_chars=40, max_examples=500):
    """
    Crea CSV KWIC per ciascuna competenza (per gruppo).
    comp_lexicon_weighted: {competenza: {stem->peso}} ; scegliamo come 'keywords' gli stems (come stringhe intere).
    """
    export_dir.mkdir(parents=True, exist_ok=True)
    rows = []
    
    for comp, stems_w in comp_lexicon_weighted.items():
        if comp not in {"personale","cittadinanza","imprenditoriale","culturale","digitale"}:
            continue
        keywords = list(stems_w.keys())
        if not keywords:
            continue
        for _, r in df_text.iterrows():
            full_text = " ".join([
                str(r.get("attivita","")), str(r.get("obiettivi","")), str(r.get("problemi","")),
                str(r.get("riflessioni","")), str(r.get("c1_personale","")), str(r.get("c2_cittadinanza","")),
                str(r.get("c3_imprenditoriale","")), str(r.get("c4_culturale","")),
                str(r.get("c5_digitale","")), str(r.get("c6_multilinguistica",""))
            ])
            full_text = normalize_ws(full_text)
            for kw in keywords:
                
                
                
                pattern = re.compile(r"\b" + re.escape(kw) + r"\w*", re.IGNORECASE)
                for m in pattern.finditer(full_text):
                    i = m.start()
                    left = full_text[max(0, i-window_chars): i]
                    match = full_text[m.start(): m.end()]
                    right = full_text[m.end(): m.end()+window_chars]
                    rows.append({
                        "docente": r["docente"],
                        "mese": r["mese"],
                        "classe_tipo": r["classe_tipo"],
                        "competenza": comp,
                        "keyword": kw,
                        "left": left.strip(),
                        "match": match,
                        "right": right.strip()
                    })
                    if len(rows) >= max_examples:
                        break
                if len(rows) >= max_examples:
                    break
            if len(rows) >= max_examples:
                break
        
        if rows:
            out_df = pd.DataFrame(rows)
            out_df.to_csv(export_dir / f"kwic_{comp}.csv", index=False, encoding="utf-8-sig")
        rows = []

def select_exemplary_quotes(df_text, comp_lexicon_weighted: dict, val_pos_w: dict, val_neg_w: dict,
                            export_dir: Path, per_competenza=10, window_sentences=1, extra_stopwords=None):
    """
    Crea un CSV di "citazioni esemplari" per ciascuna competenza (per gruppo).
    Strategia semplice: split in frasi, scoring = ev_comp(stems) + (val_pos - val_neg).
    Prende le top-N per competenza (massimo per gruppo).
    """
    try:
        import nltk  
        nltk_available = True
    except Exception:
        nltk_available = False

    def sentence_split(text):
        if nltk_available:
            try:
                from nltk.tokenize import sent_tokenize
                return sent_tokenize(text, language="italian")
            except Exception:
                pass
        
        return re.split(r"(?<=[\.\!\?…])\s+", text)

    rows = []
    for comp, stems_w in comp_lexicon_weighted.items():
        if comp not in {"personale","cittadinanza","imprenditoriale","culturale","digitale"}:
            continue
        for _, r in df_text.iterrows():
            full_text = " ".join([
                str(r.get("attivita","")), str(r.get("obiettivi","")), str(r.get("problemi","")),
                str(r.get("riflessioni","")), str(r.get("c1_personale","")), str(r.get("c2_cittadinanza","")),
                str(r.get("c3_imprenditoriale","")), str(r.get("c4_culturale","")),
                str(r.get("c5_digitale","")), str(r.get("c6_multilinguistica",""))
            ])
            sentences = [s.strip() for s in sentence_split(normalize_ws(full_text)) if s.strip()]
            for s in sentences:
                toks = tokenize_it(s, extra_stopwords)
                score_comp = count_weighted_stems(toks, stems_w)
                score_val = count_weighted_stems(toks, val_pos_w) - count_weighted_stems(toks, val_neg_w)
                score = score_comp + 0.5 * score_val
                if score > 0:
                    rows.append({
                        "docente": r["docente"],
                        "mese": r["mese"],
                        "classe_tipo": r["classe_tipo"],
                        "competenza": comp,
                        "quote": s,
                        "score": score,
                        "score_comp": score_comp,
                        "score_val": score_val
                    })
    if not rows:
        return
    df_all = pd.DataFrame(rows)
    
    exemplars = []
    for (comp, gruppo), grp in df_all.groupby(["competenza","classe_tipo"]):
        g = grp.sort_values("score", ascending=False).head(per_competenza)
        exemplars.append(g)
    if exemplars:
        df_ex = pd.concat(exemplars, ignore_index=True)
        df_ex.to_csv(export_dir / "citazioni_esemplari.csv", index=False, encoding="utf-8-sig")





def plot_slope_valenza_profondita(df_feat, export_dir: Path):
    """
    Crea due grafici linea (mese) per media per gruppo:
     - valenza_pos
     - profondita_rifl_classe
    """
    export_dir.mkdir(parents=True, exist_ok=True)

    for measure, title, fname in [
        ("valenza_pos", "Valenza positiva media per mese", "slope_valenza_pos.png"),
        ("profondita_rifl_classe", "Profondità riflessiva media per mese", "slope_profondita.png"),
    ]:
        try:
            tmp = (df_feat.groupby(["mese","classe_tipo"])[measure]
                   .mean().reset_index())
            
            tmp["mese_idx"] = tmp["mese"].map(ORDINE_MESE)
            tmp = tmp.dropna(subset=["mese_idx"]).sort_values("mese_idx")

            plt.figure(figsize=(8,4.5))
            for gruppo, gdf in tmp.groupby("classe_tipo"):
                plt.plot(gdf["mese_idx"], gdf[measure], marker="o", label=gruppo)
            plt.xticks(range(len(MESI)), MESI, rotation=0)
            plt.title(title)
            plt.xlabel("Mese")
            plt.ylabel(measure)
            plt.legend()
            plt.tight_layout()
            plt.savefig(export_dir / fname, dpi=160)
            plt.close()
        except Exception:
            pass

def plot_radar_competenze(df_feat, export_dir: Path):
    """
    Radar delle differenze (ETW - CTRL) per evidenze di 5 competenze, medie su tutte le righe.
    """
    export_dir.mkdir(parents=True, exist_ok=True)
    comps = ["ev_personale","ev_cittadinanza","ev_imprenditoriale","ev_culturale","ev_digitale"]

    try:
        
        means = df_feat.groupby("classe_tipo")[comps].mean().to_dict(orient="index")
        if not {"etwinning","controllo"}.issubset(means.keys()):
            return
        values = [means["etwinning"][c] - means["controllo"][c] for c in comps]

        
        labels = ["Personale","Cittadinanza","Imprenditoriale","Culturale","Digitale"]
        angles = np.linspace(0, 2*np.pi, len(labels), endpoint=False).tolist()
        values += values[:1]
        angles += angles[:1]

        fig = plt.figure(figsize=(6,6))
        ax = plt.subplot(111, polar=True)
        ax.plot(angles, values, marker="o")
        ax.fill(angles, values, alpha=0.25)
        ax.set_thetagrids(np.degrees(np.array(angles[:-1])), labels)
        ax.set_title("Differenza evidenze competenze (ETW - CTRL)")
        plt.tight_layout()
        plt.savefig(export_dir / "radar_competenze.png", dpi=160)
        plt.close()
    except Exception:
        pass

def plot_event_trends(df_feat, export_dir: Path):
    """
    Trend mensile per categorie evento, distinti per gruppo.
    """
    export_dir.mkdir(parents=True, exist_ok=True)
    ev_cols = [c for c in df_feat.columns if c.startswith("eventi_")]
    if not ev_cols:
        return
    tmp = (df_feat.groupby(["mese","classe_tipo"])[ev_cols]
            .mean().reset_index())
    tmp["mese_idx"] = tmp["mese"].map(ORDINE_MESE)
    tmp = tmp.dropna(subset=["mese_idx"]).sort_values("mese_idx")

    for gruppo, gdf in tmp.groupby("classe_tipo"):
        try:
            plt.figure(figsize=(9,5))
            for c in ev_cols:
                plt.plot(gdf["mese_idx"], gdf[c], marker="o", label=c.replace("eventi_",""))
            plt.xticks(range(len(MESI)), MESI)
            plt.title(f"Trend eventi – {gruppo}")
            plt.xlabel("Mese")
            plt.ylabel("media occorrenze")
            plt.legend()
            plt.tight_layout()
            plt.savefig(export_dir / f"trend_eventi_{gruppo}.png", dpi=160)
            plt.close()
        except Exception:
            pass

def build_cooc_network(df_text, comp_lexicon_weighted: dict, export_dir: Path, top_edges=30):
    """
    Crea una semplice rete competenza <-> token (parola) basata su co-occorrenze:
    - Per ogni riga, estrai tokens; per ciascuna competenza conta occorrenze di stems,
      e per ciascuna parola (non stopword) aumenti peso degli edges (competenza, parola).
    - Alla fine, tieni i top N edges per peso.
    """
    if not _NETWORKX_OK:
        return
    export_dir.mkdir(parents=True, exist_ok=True)

    edge_w = Counter()
    for _, r in df_text.iterrows():
        full_text = " ".join([
            str(r.get("attivita","")), str(r.get("obiettivi","")), str(r.get("problemi","")),
            str(r.get("riflessioni","")), str(r.get("c1_personale","")), str(r.get("c2_cittadinanza","")),
            str(r.get("c3_imprenditoriale","")), str(r.get("c4_culturale","")),
            str(r.get("c5_digitale","")), str(r.get("c6_multilinguistica",""))
        ])
        toks = tokenize_it(full_text)
        if not toks:
            continue
        uniq_tokens = set(toks)
        for comp, stems_w in comp_lexicon_weighted.items():
            if comp not in {"personale","cittadinanza","imprenditoriale","culturale","digitale"}:
                continue
            
            if any(any(tok.startswith(st) for st in stems_w.keys()) for tok in uniq_tokens):
                for t in uniq_tokens:
                    if len(t) < 3:
                        continue
                    edge_w[(comp, t)] += 1

    if not edge_w:
        return
    
    edges = edge_w.most_common(top_edges)
    G = nx.Graph()
    for (comp, tok), w in edges:
        G.add_node(comp, bipartite=0)
        G.add_node(tok, bipartite=1)
        G.add_edge(comp, tok, weight=w)

    plt.figure(figsize=(10,7))
    pos = nx.spring_layout(G, k=0.6, seed=42)
    nx.draw_networkx_nodes(G, pos, nodelist=[n for n in G.nodes if n in comp_lexicon_weighted.keys()], node_size=800)
    nx.draw_networkx_nodes(G, pos, nodelist=[n for n in G.nodes if n not in comp_lexicon_weighted.keys()], node_size=300)
    nx.draw_networkx_edges(G, pos, width=[max(1, G[u][v]["weight"]/2) for u,v in G.edges])
    nx.draw_networkx_labels(G, pos, font_size=8)
    plt.title("Rete co-occorrenze (competenza–token)")
    plt.axis("off")
    plt.tight_layout()
    plt.savefig(export_dir / "rete_cooccorrenze.png", dpi=160)
    plt.close()





def process_root(root_dir: Path, export_dir: Path,
                 resources_zip: Path = None,
                 stopwords_file: Path = None,
                 n_topics=8, do_topics=True, do_cooc=True, kwic_window=40,
                 log_fn=print):
    """
    Attraversa docente×mese ed elabora.
    """
    export_dir.mkdir(parents=True, exist_ok=True)

    
    res = load_resources_zip(resources_zip) if resources_zip else {
        "val_pos": {}, "val_neg": {}, "eventi": {}, "competenze": {}, "extra_stopwords": []
    }
    extra_stopwords = []
    if stopwords_file and Path(stopwords_file).exists():
        extra_stopwords = load_stopwords_file(stopwords_file)
    if res.get("extra_stopwords"):
        extra_stopwords += res["extra_stopwords"]

    
    
    val_pos_w = stems_or_words_to_weighted_dict(VALENZA_POS_STEMS)
    val_neg_w = stems_or_words_to_weighted_dict(VALENZA_NEG_STEMS)
    
    if res["val_pos"]:
        for w, wt in res["val_pos"].items():
            val_pos_w[w] = val_pos_w.get(w, 0.0) + float(wt)
    if res["val_neg"]:
        for w, wt in res["val_neg"].items():
            val_neg_w[w] = val_neg_w.get(w, 0.0) + float(wt)

    
    eventi_w = {}
    
    for cat, lst in EVENT_TAXONOMY.items():
        eventi_w[cat] = stems_or_words_to_weighted_dict(lst)
    
    if res["eventi"]:
        for cat, words in res["eventi"].items():
            cat_key = str(cat).strip().lower()
            if not cat_key.startswith("eventi_"):
                cat_key = "eventi_" + cat_key
            if cat_key not in eventi_w:
                eventi_w[cat_key] = {}
            for w, wt in words.items():
                eventi_w[cat_key][w] = eventi_w[cat_key].get(w, 0.0) + float(wt)

    
    comp_lexicon_weighted = {}
    
    for comp, lst in DEFAULT_CODEBOOK["competenze"].items():
        comp_lexicon_weighted[comp] = stems_or_words_to_weighted_dict(lst)
    
    if res["competenze"]:
        for comp, words in res["competenze"].items():
            comp = comp.strip().lower()
            if comp.startswith("pers"): comp = "personale"
            elif comp.startswith("citt"): comp = "cittadinanza"
            elif comp.startswith("impr"): comp = "imprenditoriale"
            elif comp.startswith("cult"): comp = "culturale"
            elif comp.startswith("digit"): comp = "digitale"
            if comp not in comp_lexicon_weighted:
                comp_lexicon_weighted[comp] = {}
            for w, wt in words.items():
                comp_lexicon_weighted[comp][w] = comp_lexicon_weighted[comp].get(w, 0.0) + float(wt)

    
    rows_text = []
    rows_feats = []
    n_files_total = 0
    n_files_used = 0

    for docente_dir in sorted([p for p in root_dir.iterdir() if p.is_dir()]):
        docente = docente_dir.name.strip()
        if docente.lower() in {"outputs", "export", "exports"}:
            continue

        for mese in MESI:
            mese_dir = docente_dir / mese
            if not mese_dir.exists() or not mese_dir.is_dir():
                continue

            docx_files = list(mese_dir.glob("*.docx"))
            if not docx_files:
                continue
            filepath = docx_files[0]
            n_files_total += 1

            try:
                txt = read_docx_text(filepath)
                sections = extract_sections_from_text(txt)
                if sections.get("__empty__", True):
                    log_fn(f"[{docente} / {mese}] Nessun testo utile → escluso.")
                    continue

                etw = sections["etw"]
                ctrl = sections["ctrl"]
                rifl_doc = sections.get("docente_riflessione","")
                impatto_doc = sections.get("docente_impatto","")

                row_text_common = {"docente": docente, "mese": mese}

                row_etw_text = {
                    **row_text_common, "classe_tipo": "etwinning",
                    "attivita": etw.get("attivita",""),
                    "obiettivi": etw.get("obiettivi",""),
                    "problemi": etw.get("problemi",""),
                    "riflessioni": etw.get("riflessioni",""),
                    "c1_personale": etw.get("c1",""),
                    "c2_cittadinanza": etw.get("c2",""),
                    "c3_imprenditoriale": etw.get("c3",""),
                    "c4_culturale": etw.get("c4",""),
                    "c5_digitale": etw.get("c5",""),
                    "c6_multilinguistica": etw.get("c6",""),
                    "rifl_docente": rifl_doc,
                    "impatto_docente": impatto_doc
                }
                row_ctrl_text = {
                    **row_text_common, "classe_tipo": "controllo",
                    "attivita": ctrl.get("attivita",""),
                    "obiettivi": ctrl.get("obiettivi",""),
                    "problemi": ctrl.get("problemi",""),
                    "riflessioni": ctrl.get("riflessioni",""),
                    "c1_personale": ctrl.get("c1",""),
                    "c2_cittadinanza": ctrl.get("c2",""),
                    "c3_imprenditoriale": ctrl.get("c3",""),
                    "c4_culturale": ctrl.get("c4",""),
                    "c5_digitale": ctrl.get("c5",""),
                    "c6_multilinguistica": ctrl.get("c6",""),
                    "rifl_docente": rifl_doc,
                    "impatto_docente": impatto_doc
                }

                def has_any_text(d):
                    keys = ["attivita","obiettivi","problemi","riflessioni",
                            "c1_personale","c2_cittadinanza","c3_imprenditoriale",
                            "c4_culturale","c5_digitale","c6_multilinguistica",
                            "rifl_docente","impatto_docente"]
                    return any(str(d.get(k,"")).strip() for k in keys)

                added = False
                if has_any_text(row_etw_text):
                    rows_text.append(row_etw_text)
                    added = True
                if has_any_text(row_ctrl_text):
                    rows_text.append(row_ctrl_text)
                    added = True
                if not added:
                    log_fn(f"[{docente} / {mese}] Nessun campo con testo → escluso.")
                    continue

                
                etw_total_txt = " ".join([
                    row_etw_text["attivita"], row_etw_text["obiettivi"], row_etw_text["problemi"],
                    row_etw_text["riflessioni"], row_etw_text["c1_personale"], row_etw_text["c2_cittadinanza"],
                    row_etw_text["c3_imprenditoriale"], row_etw_text["c4_culturale"],
                    row_etw_text["c5_digitale"], row_etw_text["c6_multilinguistica"]
                ])
                ctrl_total_txt = " ".join([
                    row_ctrl_text["attivita"], row_ctrl_text["obiettivi"], row_ctrl_text["problemi"],
                    row_ctrl_text["riflessioni"], row_ctrl_text["c1_personale"], row_ctrl_text["c2_cittadinanza"],
                    row_ctrl_text["c3_imprenditoriale"], row_ctrl_text["c4_culturale"],
                    row_ctrl_text["c5_digitale"], row_ctrl_text["c6_multilinguistica"]
                ])

                etw_feats = compute_features_for_row(
                    etw_total_txt, etw.get("riflessioni",""), etw.get("problemi",""),
                    comp_lexicon_weighted, val_pos_w, val_neg_w, eventi_w,
                    extra_stopwords=extra_stopwords
                )
                ctrl_feats = compute_features_for_row(
                    ctrl_total_txt, ctrl.get("riflessioni",""), ctrl.get("problemi",""),
                    comp_lexicon_weighted, val_pos_w, val_neg_w, eventi_w,
                    extra_stopwords=extra_stopwords
                )

                rows_feats.append({**row_text_common, "classe_tipo":"etwinning", **etw_feats})
                rows_feats.append({**row_text_common, "classe_tipo":"controllo", **ctrl_feats})

                n_files_used += 1
                log_fn(f"[OK] {docente} / {mese}")

            except Exception as ex:
                log_fn(f"[ERRORE] {filepath}: {ex}")
                log_fn(traceback.format_exc())

    if not rows_text:
        raise RuntimeError("Nessun testo utile trovato. Verifica struttura cartelle e .docx.")

    
    df_text = pd.DataFrame(rows_text)
    df_text.sort_values(by=["docente","mese","classe_tipo"],
                        key=lambda s: s.map(ORDINE_MESE) if s.name=="mese" else s, inplace=True)
    df_text.to_csv(export_dir/"diari_parsed.csv", index=False, encoding="utf-8-sig")

    df_feat = pd.DataFrame(rows_feats)
    df_feat.sort_values(by=["docente","mese","classe_tipo"],
                        key=lambda s: s.map(ORDINE_MESE) if s.name=="mese" else s, inplace=True)
    df_feat.to_csv(export_dir/"features_diari.csv", index=False, encoding="utf-8-sig")

    
    diffs = []
    for (docente, mese), grp in df_feat.groupby(["docente","mese"]):
        if {"etwinning","controllo"}.issubset(set(grp["classe_tipo"])):
            etw = grp[grp["classe_tipo"]=="etwinning"].iloc[0]
            ctrl = grp[grp["classe_tipo"]=="controllo"].iloc[0]
            diff_row = {"docente": docente, "mese": mese}
            for col in df_feat.columns:
                if col in ["docente","mese","classe_tipo"]:
                    continue
                try:
                    diff_row[col] = float(etw[col]) - float(ctrl[col])
                except Exception:
                    pass
            diffs.append(diff_row)
    if diffs:
        df_diff = pd.DataFrame(diffs)
        df_diff.sort_values(by=["docente","mese"],
                            key=lambda s: s.map(ORDINE_MESE) if s.name=="mese" else s, inplace=True)
        df_diff.to_csv(export_dir/"diff_within_teacher.csv", index=False, encoding="utf-8-sig")

    
    did_rows = []
    for docente, grp in df_feat.groupby("docente"):
        def pick(mese, tipo):
            r = grp[(grp["mese"]==mese) & (grp["classe_tipo"]==tipo)]
            return r.iloc[0] if len(r)>0 else None
        e_j = pick("gennaio","etwinning")
        e_m = pick("maggio","etwinning")
        c_j = pick("gennaio","controllo")
        c_m = pick("maggio","controllo")
        if e_j is not None and e_m is not None and c_j is not None and c_m is not None:
            row = {"docente": docente}
            for col in df_feat.columns:
                if col in ["docente","mese","classe_tipo"]:
                    continue
                try:
                    e_delta = float(e_m[col]) - float(e_j[col])
                    c_delta = float(c_m[col]) - float(c_j[col])
                    row[col] = e_delta - c_delta
                except Exception:
                    pass
            did_rows.append(row)
    if did_rows:
        df_did = pd.DataFrame(did_rows)
        df_did.to_csv(export_dir/"did_summary.csv", index=False, encoding="utf-8-sig")

    
    have_topics = False
    if do_topics and _SCIKIT_OK:
        corpus = []
        meta = []
        for _, r in df_text.iterrows():
            parts = [
                r.get("attivita",""), r.get("obiettivi",""), r.get("problemi",""),
                r.get("riflessioni",""), r.get("c1_personale",""), r.get("c2_cittadinanza",""),
                r.get("c3_imprenditoriale",""), r.get("c4_culturale",""),
                r.get("c5_digitale",""), r.get("c6_multilinguistica","")
            ]
            corpus.append(normalize_ws(" ".join([p for p in parts if p])))
            meta.append((r["docente"], r["mese"], r["classe_tipo"]))
        topics, W = topic_modeling(corpus, extra_stopwords=extra_stopwords, n_topics=n_topics)
        if topics:
            pd.DataFrame({"topic": [f"topic_{i}" for i in range(len(topics))],
                          "top_terms": [", ".join(t) for t in topics]}
                        ).to_csv(export_dir/"topics.csv", index=False, encoding="utf-8-sig")
            have_topics = True
        if W is not None:
            df_dt = pd.DataFrame(W, columns=[f"topic_{i}" for i in range(W.shape[1])])
            df_dt.insert(0, "classe_tipo", [m[2] for m in meta])
            df_dt.insert(0, "mese", [m[1] for m in meta])
            df_dt.insert(0, "docente", [m[0] for m in meta])
            df_dt.to_csv(export_dir/"doc_topics.csv", index=False, encoding="utf-8-sig")

    
    charts_dir = export_dir / "charts"
    charts_dir.mkdir(exist_ok=True)
    plot_slope_valenza_profondita(df_feat, charts_dir)
    plot_radar_competenze(df_feat, charts_dir)
    plot_event_trends(df_feat, charts_dir)

    
    if do_cooc and _NETWORKX_OK:
        network_dir = charts_dir / "network"
        network_dir.mkdir(parents=True, exist_ok=True)
        build_cooc_network(df_text, comp_lexicon_weighted, network_dir)

    
    kwic_dir = export_dir / "kwic"
    kwic_dir.mkdir(parents=True, exist_ok=True)
    generate_kwic_exports(df_text, comp_lexicon_weighted, kwic_dir, window_chars=kwic_window, max_examples=2000)
    select_exemplary_quotes(df_text, comp_lexicon_weighted, val_pos_w, val_neg_w, kwic_dir, per_competenza=12, extra_stopwords=extra_stopwords)

    
    codebook_out = {
        "timestamp": datetime.now().isoformat(timespec="seconds"),
        "valenza_pos_stems_weights": val_pos_w,
        "valenza_neg_stems_weights": val_neg_w,
        "eventi_stems_weights": eventi_w,
        "competenze_stems_weights": comp_lexicon_weighted
    }
    with open(export_dir/"codebook_usato.json", "w", encoding="utf-8") as f:
        json.dump(codebook_out, f, ensure_ascii=False, indent=2)

    
    cfg = {
        "root_dir": str(root_dir),
        "export_dir": str(export_dir),
        "timestamp": datetime.now().isoformat(timespec="seconds"),
        "n_files_total": n_files_total,
        "n_files_used": n_files_used,
        "dependencies": {
            "spacy_loaded": _NLP is not None,
            "scikit_learn": _SCIKIT_OK,
            "networkx": _NETWORKX_OK
        },
        "params": {
            "n_topics": n_topics,
            "do_topics": do_topics,
            "do_cooc": do_cooc,
            "kwic_window_chars": kwic_window
        }
    }
    with open(export_dir/"run_config.json", "w", encoding="utf-8") as f:
        json.dump(cfg, f, ensure_ascii=False, indent=2)

    return {
        "n_files_total": n_files_total,
        "n_files_used": n_files_used,
        "have_topics": have_topics
    }





class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Analisi Qualitativa - Diari di Bordo (eTwinning)")
        self.geometry("980x680")

        self.root_dir = tk.StringVar(value="")
        self.export_dir = tk.StringVar(value="")
        self.zip_res = tk.StringVar(value="")
        self.stopwords_path = tk.StringVar(value="")
        self.do_topics = tk.BooleanVar(value=True)
        self.do_cooc = tk.BooleanVar(value=True)
        self.n_topics = tk.IntVar(value=8)
        self.kwic_window = tk.IntVar(value=40)

        self.status_var = tk.StringVar(value="Seleziona la cartella RADICE e la cartella di ESPORTAZIONE, poi avvia.")
        self.create_widgets()

    def create_widgets(self):
        frm = ttk.Frame(self, padding=12)
        frm.pack(fill="both", expand=True)

        
        row = ttk.Frame(frm); row.pack(fill="x", pady=4)
        ttk.Label(row, text="Cartella RADICE (contiene le cartelle dei docenti):").pack(anchor="w")
        e = ttk.Entry(row, textvariable=self.root_dir); e.pack(side="left", fill="x", expand=True, padx=(0,8))
        ttk.Button(row, text="Sfoglia…", command=self.browse_root).pack(side="left")

        
        row = ttk.Frame(frm); row.pack(fill="x", pady=4)
        ttk.Label(row, text="Cartella di ESPORTAZIONE (CSV, grafici, KWIC, ecc.):").pack(anchor="w")
        e = ttk.Entry(row, textvariable=self.export_dir); e.pack(side="left", fill="x", expand=True, padx=(0,8))
        ttk.Button(row, text="Sfoglia…", command=self.browse_export).pack(side="left")

        
        row = ttk.Frame(frm); row.pack(fill="x", pady=4)
        ttk.Label(row, text="(Opzionale) ZIP risorse (es. 2.zip):").pack(anchor="w")
        e = ttk.Entry(row, textvariable=self.zip_res); e.pack(side="left", fill="x", expand=True, padx=(0,8))
        ttk.Button(row, text="Sfoglia…", command=self.browse_zip).pack(side="left")

        
        row = ttk.Frame(frm); row.pack(fill="x", pady=4)
        ttk.Label(row, text="(Opzionale) File STOPWORDS (.txt):").pack(anchor="w")
        e = ttk.Entry(row, textvariable=self.stopwords_path); e.pack(side="left", fill="x", expand=True, padx=(0,8))
        ttk.Button(row, text="Sfoglia…", command=self.browse_stop).pack(side="left")

        
        row = ttk.Frame(frm); row.pack(fill="x", pady=6)
        ttk.Checkbutton(row, text="Topic Modeling (LDA)", variable=self.do_topics).pack(side="left", padx=(0,16))
        ttk.Label(row, text="n_topics:").pack(side="left")
        ttk.Spinbox(row, from_=3, to=15, textvariable=self.n_topics, width=5).pack(side="left", padx=(0,16))
        ttk.Checkbutton(row, text="Rete co-occorrenze (networkx)", variable=self.do_cooc).pack(side="left", padx=(0,16))
        ttk.Label(row, text="KWIC window (caratteri):").pack(side="left")
        ttk.Spinbox(row, from_=20, to=120, textvariable=self.kwic_window, width=5).pack(side="left")

        
        row = ttk.Frame(frm); row.pack(fill="x", pady=8)
        ttk.Button(row, text="Esegui ANALISI", command=self.run_all).pack(side="left", padx=(0,8))
        ttk.Button(row, text="Apri cartella EXPORT", command=self.open_export).pack(side="left")

        
        ttk.Label(frm, textvariable=self.status_var, foreground="#333").pack(anchor="w", pady=(6,2))

        
        self.txt = tk.Text(frm, height=24, wrap="word")
        self.txt.pack(fill="both", expand=True)
        self.txt.configure(state="disabled")

        
        ttk.Label(frm, text="Struttura attesa: <RADICE>/<Docente>/<mese>/File.docx — mesi: gennaio…maggio").pack(anchor="w", pady=(6,0))

    def browse_root(self):
        d = filedialog.askdirectory(title="Seleziona la cartella RADICE")
        if d:
            self.root_dir.set(d)

    def browse_export(self):
        d = filedialog.askdirectory(title="Seleziona la cartella di ESPORTAZIONE")
        if d:
            self.export_dir.set(d)

    def browse_zip(self):
        f = filedialog.askopenfilename(title="Seleziona ZIP risorse", filetypes=[("Zip files","*.zip"),("All files","*.*")])
        if f:
            self.zip_res.set(f)

    def browse_stop(self):
        f = filedialog.askopenfilename(title="Seleziona file STOPWORDS", filetypes=[("Text files","*.txt"),("All files","*.*")])
        if f:
            self.stopwords_path.set(f)

    def log(self, msg):
        self.txt.configure(state="normal")
        self.txt.insert("end", msg + "\n")
        self.txt.see("end")
        self.txt.configure(state="disabled")
        self.update_idletasks()

    def run_all(self):
        root = self.root_dir.get().strip()
        exp = self.export_dir.get().strip()
        if not root:
            messagebox.showwarning("Seleziona RADICE", "Seleziona la cartella RADICE.")
            return
        if not exp:
            messagebox.showwarning("Seleziona EXPORT", "Seleziona la cartella di ESPORTAZIONE.")
            return
        root_dir = Path(root)
        export_dir = Path(exp)
        zip_res = Path(self.zip_res.get().strip()) if self.zip_res.get().strip() else None
        stop_f = Path(self.stopwords_path.get().strip()) if self.stopwords_path.get().strip() else None

        self.txt.configure(state="normal"); self.txt.delete("1.0","end"); self.txt.configure(state="disabled")
        self.status_var.set("Analisi in corso…")
        self.log(f"RADICE: {root_dir}")
        self.log(f"EXPORT: {export_dir}")
        if zip_res:
            self.log(f"ZIP risorse: {zip_res}")
        if stop_f:
            self.log(f"STOPWORDS: {stop_f}")
        self.log(f"Topic modeling: {'ON' if self.do_topics.get() else 'OFF'} (n_topics={self.n_topics.get()})")
        self.log(f"Rete co-occorrenze: {'ON' if self.do_cooc.get() else 'OFF'}")
        self.log(f"KWIC window: {self.kwic_window.get()} caratteri")

        try:
            res = process_root(
                root_dir, export_dir,
                resources_zip=zip_res, stopwords_file=stop_f,
                n_topics=self.n_topics.get(), do_topics=self.do_topics.get(),
                do_cooc=self.do_cooc.get(), kwic_window=self.kwic_window.get(),
                log_fn=self.log
            )
            self.status_var.set("Analisi completata.")
            self.log("")
            self.log(f"File elaborati: {res['n_files_used']} / trovati: {res['n_files_total']}")
            self.log("Output creati in: " + str(export_dir))
            if res["have_topics"]:
                self.log("Creati: topics.csv e doc_topics.csv")
            messagebox.showinfo("Completato", f"Analisi completata.\nOutput in:\n{export_dir}")
        except Exception as ex:
            self.status_var.set("Errore durante l'analisi.")
            self.log(f"[ERRORE] {ex}")
            self.log(traceback.format_exc())
            messagebox.showerror("Errore", str(ex))

    def open_export(self):
        exp = self.export_dir.get().strip()
        if not exp:
            messagebox.showwarning("Nessuna cartella", "Seleziona prima la cartella di ESPORTAZIONE.")
            return
        out_dir = Path(exp)
        if not out_dir.exists():
            messagebox.showwarning("Manca export", "La cartella di esportazione non esiste.")
            return
        try:
            import subprocess, platform
            if platform.system() == "Windows":
                os.startfile(out_dir)  
            elif platform.system() == "Darwin":
                subprocess.Popen(["open", out_dir])
            else:
                subprocess.Popen(["xdg-open", out_dir])
        except Exception:
            messagebox.showinfo("Percorso export", str(out_dir))

def main():
    app = App()
    app.mainloop()

if __name__ == "__main__":
    main()
