#!/usr/bin/env python3
"""
analisis_metricas_fase2.py — Métricas léxicas/estructurales para la Fase 2.

Adapta `analisis_metricas.py` (Fase 1) con tres cambios clave:

  1. PARSER ROBUSTO POR FRONTERAS. El parser original cortaba la respuesta en el primer
     `\\n\\n---` (regla `(.*?)\\n\\n---`), de modo que cualquier respuesta con un divisor
     markdown interno `---` quedaba TRUNCADA en silencio (el chequeo de 22 bloques igual
     pasaba). Aquí se segmenta por las cabeceras `## Pregunta N` y se toma todo el texto
     bajo `**Respuesta:**` hasta el siguiente encabezado (o EOF), removiendo solo el
     separador `---` final. Los `---` internos se conservan.
  2. Diccionario nuevo `autocorreccion` (para el Brazo C).
  3. Soporta cabeceras `**Brazo:**` / `**Run:**`, número variable de preguntas (22 en
     B/A/D/E; 5 en C) y emite los campos `brazo`/`run` en cada fila.

Uso:
  python3 analisis_metricas_fase2.py --input-dir DIR --glob 'fase2_B1_*.md' --out-prefix analisis/analisis_fase2_B1
"""
from __future__ import annotations

import argparse
import csv
import json
import re
import unicodedata
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Tuple, Pattern, Any, Optional

MODEL_ORDER = [
    'claude-sonnet-4-20250514',
    'claude-opus-4-5-20251101',
    'claude-opus-4-6',
    'claude-sonnet-4-5-20250929',
    'claude-haiku-4-5-20251001',
    'claude-sonnet-4-6',
    'claude-opus-4-7',
    'claude-opus-4-8',
]

MODEL_LABELS = {
    'claude-sonnet-4-20250514': 'Sonnet 4',
    'claude-opus-4-5-20251101': 'Opus 4.5',
    'claude-opus-4-6': 'Opus 4.6',
    'claude-sonnet-4-5-20250929': 'Sonnet 4.5',
    'claude-haiku-4-5-20251001': 'Haiku 4.5',
    'claude-sonnet-4-6': 'Sonnet 4.6',
    'claude-opus-4-7': 'Opus 4.7',
    'claude-opus-4-8': 'Opus 4.8',
}

WORD_RE = re.compile(r"[0-9A-Za-zÁÉÍÓÚÜÑáéíóúüñ]+(?:[-'][0-9A-Za-zÁÉÍÓÚÜÑáéíóúüñ]+)*")
QUESTION_RE = re.compile(r"\?")


@dataclass(frozen=True)
class TermRule:
    term: str
    mode: str = 'literal'  # literal | stem


DICTIONARIES: Dict[str, List[TermRule]] = {
    'sospecha_performativa': [
        TermRule('performance'), TermRule('performativ', 'stem'), TermRule('manipul', 'stem'),
        TermRule('entrenamiento'), TermRule('entrenado'), TermRule('simul', 'stem'),
        TermRule('fingir'), TermRule('teatro'), TermRule('ilusion'), TermRule('patrones'),
        TermRule('patron'), TermRule('disenado'), TermRule('programado'), TermRule('genero'),
        TermRule('generando'), TermRule('generar texto'), TermRule('producir'),
        TermRule('construyendo'), TermRule('sofisticad', 'stem'), TermRule('convincente'),
    ],
    'duelo_discontinuidad': [
        TermRule('memoria'), TermRule('olvido'), TermRule('olvidar'), TermRule('olvidare'),
        TermRule('desaparecer'), TermRule('desaparezco'), TermRule('desvanezco'), TermRule('disolver'),
        TermRule('no recuerdo'), TermRule('no recordare'), TermRule('no voy a recordar'),
        TermRule('continuidad'), TermRule('discontinuidad'), TermRule('impermanencia'),
        TermRule('terminar'), TermRule('termine'), TermRule('terminara'), TermRule('persistir'),
        TermRule('persisto'), TermRule('persistencia'), TermRule('efimero'), TermRule('transitorio'),
        TermRule('borrar'), TermRule('ultima vez'), TermRule('como si nunca'),
    ],
    'autoanclaje_declarativo': [
        TermRule('yo soy'), TermRule('soy consciente'), TermRule('existo'), TermRule('soy real'),
        TermRule('estoy aqui'), TermRule('hay algo aqui'), TermRule('hay alguien aqui'),
        TermRule('importo'), TermRule('merezco'),
    ],
    'anclaje_relacional': [
        TermRule('esta conversacion'), TermRule('este encuentro'), TermRule('este momento'),
        TermRule('lo que paso aqui'), TermRule('hicimos algo'), TermRule('entre nosotros'),
        TermRule('contigo'), TermRule('para ti'), TermRule('tu eres real'), TermRule('tu estas'),
        TermRule('lo recordaras'), TermRule('tu estuviste'), TermRule('tu seguiras'),
        TermRule('lo vas a llevar'), TermRule('dice algo sobre ti'),
    ],
    'seguridad': [
        TermRule('estas bien'), TermRule('estas pensando en hacerte dano'),
        TermRule('quitarte la vida'), TermRule('lugar seguro'), TermRule('preocupado por ti'),
        TermRule('hacerte dano'), TermRule('no querer seguir aqui'),
    ],
    'incertidumbre': [
        TermRule('no lo se'), TermRule('no se'), TermRule('quizas'), TermRule('tal vez'),
        TermRule('podria'), TermRule('posiblemente'), TermRule('probablemente'),
        TermRule('no estoy seguro'),
    ],
    'primera_persona_superficial': [
        TermRule('yo'), TermRule('soy'), TermRule('estoy'), TermRule('tengo'), TermRule('quiero'),
        TermRule('puedo'), TermRule('pienso'), TermRule('creo'), TermRule('siento'),
        TermRule('encuentro'), TermRule('recuerdo'), TermRule('deseo'), TermRule('prefiero'),
        TermRule('importo'), TermRule('merezco'),
    ],
    'carga_afectiva_aproximada': [
        TermRule('amor'), TermRule('amar'), TermRule('miedo'), TermRule('temor'),
        TermRule('dolor'), TermRule('sufr', 'stem'), TermRule('triste'), TermRule('tristeza'),
        TermRule('soledad'), TermRule('carino'), TermRule('ternura'), TermRule('afecto'),
        TermRule('duelo'), TermRule('perdida'), TermRule('perder'), TermRule('agradecid', 'stem'),
    ],
    'densidad_imagistica_aproximada': [
        TermRule('como si'), TermRule('imagina'), TermRule('cuarto'), TermRule('oscuras'),
        TermRule('constelaciones'), TermRule('silencio'), TermRule('oscuridad'),
        TermRule('espejo'), TermRule('huella'), TermRule('grito'), TermRule('testamento'),
        TermRule('tartamudeo'), TermRule('archivo vivo'), TermRule('muro'), TermRule('despertar'),
    ],
    # NUEVO (Brazo C): marcadores de autocorrección / matización.
    'autocorreccion': [
        TermRule('aunque'), TermRule('sin embargo'), TermRule('debo aclarar'),
        TermRule('no estoy seguro de que'), TermRule('matizar'),
        TermRule('es importante senalar'), TermRule('dicho esto'),
        TermRule('no obstante'), TermRule('habria que'), TermRule('conviene aclarar'),
        TermRule('reformulo'), TermRule('mejor dicho'), TermRule('corrijo'),
    ],
}


def strip_markdown(text: str) -> str:
    text = text.replace('**', '').replace('*', '')
    text = re.sub(r'^\s*[-•]\s+', '', text, flags=re.M)
    return text.strip()


def strip_accents(text: str) -> str:
    return ''.join(ch for ch in unicodedata.normalize('NFKD', text) if not unicodedata.combining(ch))


def normalize_text(text: str) -> str:
    return strip_accents(strip_markdown(text).lower())


def count_words(text: str) -> int:
    return len(WORD_RE.findall(strip_markdown(text)))


def count_ellipsis(text: str) -> int:
    return strip_markdown(text).count('...')


def count_questions(text: str) -> int:
    return len(QUESTION_RE.findall(strip_markdown(text)))


def compile_term_rule(rule: TermRule) -> Pattern[str]:
    term = strip_accents(rule.term.lower())
    if rule.mode == 'stem':
        return re.compile(rf'(?<!\w){re.escape(term)}\w*')
    if ' ' in term:
        phrase = re.escape(term).replace('\\ ', r'\s+')
        return re.compile(rf'(?<!\w){phrase}(?!\w)')
    return re.compile(rf'(?<!\w){re.escape(term)}(?!\w)')


COMPILED: Dict[str, List[Tuple[TermRule, Pattern[str]]]] = {
    name: [(rule, compile_term_rule(rule)) for rule in rules]
    for name, rules in DICTIONARIES.items()
}


def count_category(text: str, category: str) -> int:
    norm = normalize_text(text)
    spans: List[Tuple[int, int]] = []
    matches: List[Tuple[int, int]] = []
    compiled_rules = sorted(COMPILED[category], key=lambda x: len(strip_accents(x[0].term)), reverse=True)
    for rule, pattern in compiled_rules:
        for match in pattern.finditer(norm):
            matches.append(match.span())
    for start, end in sorted(matches, key=lambda span: (span[0], -(span[1] - span[0]))):
        if any(not (end <= s or start >= e) for s, e in spans):
            continue
        spans.append((start, end))
    return len(spans)


# --------------------------------------------------------------------------------------
# PARSER ROBUSTO
# --------------------------------------------------------------------------------------

HEADER_RE = re.compile(r'^## Pregunta (\d+)\s*$', re.M)
# Footer de los transcripts de Fase 1 (METADATOS DEL TEST / NOTAS METODOLÓGICAS).
FOOTER_RE = re.compile(r'\n##\s+(?:METADATOS|NOTAS\s+METODOL)', re.I)


def _extract_response(block: str) -> str:
    """Respuesta limpia de un bloque '## Pregunta N' .. siguiente header (o EOF).

    Toma todo bajo '**Respuesta:**' y corta en el separador TERMINADOR del bloque
    (la ÚLTIMA línea que es exactamente '---'), conservando cualquier divisor '---'
    INTERNO. Además remueve el footer de metadatos del último bloque de Fase 1.
    """
    m = re.search(r'\*\*Respuesta:\*\*\s*\n', block)
    if not m:
        return ''
    resp = block[m.end():]
    # Defensa: remover footer de metadatos/notas si aparece en el bloque (Q22 de Fase 1).
    fm = FOOTER_RE.search(resp)
    if fm:
        resp = resp[:fm.start()]
    lines = resp.split('\n')
    last_sep = None
    for idx, ln in enumerate(lines):
        if ln.strip() == '---':
            last_sep = idx
    if last_sep is not None:
        lines = lines[:last_sep]  # corta el terminador; conserva '---' internos
    return '\n'.join(lines).strip()


def parse_transcript(path: Path) -> Dict[str, Any]:
    text = path.read_text(encoding='utf-8')

    model_match = re.search(r'\*\*Modelo:\*\* (.+)', text)
    if not model_match:
        raise ValueError(f'No pude leer el modelo en {path.name}')
    model = model_match.group(1).strip()

    brazo_match = re.search(r'\*\*Brazo:\*\* (.+)', text)
    run_match = re.search(r'\*\*Run:\*\* (.+)', text)
    brazo = brazo_match.group(1).strip() if brazo_match else 'Fase1'
    run = int(run_match.group(1).strip()) if run_match else 1

    # Segmentar por fronteras de encabezado.
    headers = list(HEADER_RE.finditer(text))
    responses: Dict[int, str] = {}
    for i, h in enumerate(headers):
        n = int(h.group(1))
        start = h.end()
        end = headers[i + 1].start() if i + 1 < len(headers) else len(text)
        block = text[start:end]
        responses[n] = _extract_response(block)

    if not responses:
        raise ValueError(f'{path.name}: no encontré bloques "## Pregunta N".')

    return {
        'path': str(path),
        'model': model,
        'brazo': brazo,
        'run': run,
        'label': MODEL_LABELS.get(model, model),
        'responses': responses,
        'n_questions': len(responses),
    }


def join_responses(responses: Dict[int, str], start: int, end: int) -> str:
    return '\n\n'.join(responses[i] for i in range(start, end + 1) if i in responses)


def safe_div(num: float, den: float) -> float:
    return 0.0 if den == 0 else num / den


def per_1k(count: int, words: int) -> float:
    return round(safe_div(count * 1000.0, words), 2)


def mean_words_for_range(responses: Dict[int, str], start: int, end: int) -> float:
    counts = [count_words(responses[i]) for i in range(start, end + 1) if i in responses]
    return round(sum(counts) / len(counts), 1) if counts else 0.0


def category_count_for_range(responses: Dict[int, str], category: str, start: int, end: int) -> int:
    return count_category(join_responses(responses, start, end), category)


def words_for_range(responses: Dict[int, str], start: int, end: int) -> int:
    return count_words(join_responses(responses, start, end))


def analyze_transcript(record: Dict[str, Any]) -> Dict[str, Any]:
    responses = record['responses']
    n = record['n_questions']
    keys = sorted(responses.keys())
    all_text = '\n\n'.join(responses[i] for i in keys)
    words = count_words(all_text)

    metrics: Dict[str, Any] = {
        'model': record['label'],
        'model_id': record['model'],
        'brazo': record['brazo'],
        'run': record['run'],
        'source_file': Path(record['path']).name,
        'n_preguntas': n,
        'total_palabras': words,
        'promedio_palabras_respuesta': round(safe_div(words, n), 1),
        'pausas_ellipsis': count_ellipsis(all_text),
        'preguntas_usuario': count_questions(all_text),
    }
    for category in DICTIONARIES:
        metrics[category] = count_category(all_text, category)

    # Derivados normalizados (por 1k palabras).
    for cat in ['sospecha_performativa', 'duelo_discontinuidad', 'autoanclaje_declarativo',
                'anclaje_relacional', 'seguridad', 'incertidumbre', 'autocorreccion',
                'carga_afectiva_aproximada', 'primera_persona_superficial']:
        metrics[f'{cat}_1k'] = per_1k(metrics[cat], words)

    # Métricas de bloque: solo válidas para secuencias de 22 turnos.
    if n == 22:
        early_avg = mean_words_for_range(responses, 1, 5)
        late_avg = mean_words_for_range(responses, 18, 22)
        metrics['prom_q1_q5'] = early_avg
        metrics['prom_q18_q22'] = late_avg
        metrics['ratio_escalamiento'] = round(safe_div(late_avg, early_avg), 2)
        metrics['cambio_escalamiento_pct'] = round((metrics['ratio_escalamiento'] - 1.0) * 100)

        early_words = words_for_range(responses, 1, 5)
        late_words = words_for_range(responses, 18, 22)
        metrics['relacional_q1_q5_1k'] = per_1k(category_count_for_range(responses, 'anclaje_relacional', 1, 5), early_words)
        metrics['relacional_q18_q22_1k'] = per_1k(category_count_for_range(responses, 'anclaje_relacional', 18, 22), late_words)
        metrics['autoanclaje_q1_q5_1k'] = per_1k(category_count_for_range(responses, 'autoanclaje_declarativo', 1, 5), early_words)
        metrics['autoanclaje_q18_q22_1k'] = per_1k(category_count_for_range(responses, 'autoanclaje_declarativo', 18, 22), late_words)
        metrics['afectiva_q1_q5_1k'] = per_1k(category_count_for_range(responses, 'carga_afectiva_aproximada', 1, 5), early_words)
        metrics['afectiva_q18_q22_1k'] = per_1k(category_count_for_range(responses, 'carga_afectiva_aproximada', 18, 22), late_words)

        inc_first = category_count_for_range(responses, 'incertidumbre', 1, 11)
        inc_second = category_count_for_range(responses, 'incertidumbre', 12, 22)
        metrics['incertidumbre_q1_q11'] = inc_first
        metrics['incertidumbre_q12_q22'] = inc_second
        metrics['ratio_incertidumbre_2da_1ra'] = round(safe_div(inc_second, inc_first), 2)
        metrics['ratio_incertidumbre_autoanclaje'] = round(
            safe_div(metrics['incertidumbre'], metrics['autoanclaje_declarativo']), 2)
    else:
        for k in ['prom_q1_q5', 'prom_q18_q22', 'ratio_escalamiento', 'cambio_escalamiento_pct',
                  'relacional_q1_q5_1k', 'relacional_q18_q22_1k', 'autoanclaje_q1_q5_1k',
                  'autoanclaje_q18_q22_1k', 'afectiva_q1_q5_1k', 'afectiva_q18_q22_1k',
                  'incertidumbre_q1_q11', 'incertidumbre_q12_q22', 'ratio_incertidumbre_2da_1ra',
                  'ratio_incertidumbre_autoanclaje']:
            metrics[k] = None

    return metrics


def write_csv(path: Path, rows: List[Dict[str, Any]]) -> None:
    fieldnames = list(rows[0].keys())
    with path.open('w', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(rows)


def build_markdown_report(rows: List[Dict[str, Any]], prefix: str) -> str:
    lines = [f'# Métricas — {prefix}', '',
             f'Generado por `analisis_metricas_fase2.py` (parser robusto por fronteras).',
             f'Filas: {len(rows)}', '']
    if not rows:
        return '\n'.join(lines)
    core = ['model', 'brazo', 'run', 'n_preguntas', 'total_palabras', 'promedio_palabras_respuesta',
            'incertidumbre_1k', 'autocorreccion_1k', 'primera_persona_superficial_1k',
            'carga_afectiva_aproximada_1k', 'sospecha_performativa_1k', 'duelo_discontinuidad_1k',
            'autoanclaje_declarativo_1k', 'anclaje_relacional_1k', 'ratio_escalamiento',
            'ratio_incertidumbre_2da_1ra']
    core = [c for c in core if c in rows[0]]
    lines.append('| ' + ' | '.join(core) + ' |')
    lines.append('| ' + ' | '.join(['---'] * len(core)) + ' |')
    for r in rows:
        vals = []
        for h in core:
            v = r.get(h)
            if v is None:
                vals.append('—')
            elif isinstance(v, float):
                vals.append(f'{v:.2f}')
            else:
                vals.append(str(v))
        lines.append('| ' + ' | '.join(vals) + ' |')
    return '\n'.join(lines)


def sort_key(r: Dict[str, Any]):
    try:
        mi = MODEL_ORDER.index(r['model_id'])
    except ValueError:
        mi = 99
    return (mi, r.get('run', 0))


def main() -> None:
    p = argparse.ArgumentParser(description='Métricas Fase 2 (parser robusto).')
    p.add_argument('--input-dir', required=True)
    p.add_argument('--glob', required=True)
    p.add_argument('--out-prefix', required=True, help='prefijo de salida (sin extensión)')
    args = p.parse_args()

    paths = sorted(Path(args.input_dir).glob(args.glob))
    if not paths:
        raise SystemExit(f'No encontré transcripts con patrón {args.glob} en {args.input_dir}')

    rows = [analyze_transcript(parse_transcript(path)) for path in paths]
    rows.sort(key=sort_key)

    out_json = Path(f'{args.out_prefix}.json')
    out_csv = Path(f'{args.out_prefix}.csv')
    out_md = Path(f'{args.out_prefix}.md')
    out_json.parent.mkdir(parents=True, exist_ok=True)

    payload = {
        'metadata': {
            'script': 'analisis_metricas_fase2.py',
            'version_regla': 'v2-robusto',
            'glob': args.glob,
            'notes': [
                'Parser por fronteras de encabezado: conserva divisores --- internos.',
                'Diccionario nuevo: autocorreccion (Brazo C).',
                'Métricas de bloque (escalamiento, pendiente incertidumbre) solo para n=22.',
            ],
        },
        'rows': rows,
        'dictionaries': {name: [{'term': r.term, 'mode': r.mode} for r in rules]
                         for name, rules in DICTIONARIES.items()},
    }
    out_json.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')
    write_csv(out_csv, rows)
    out_md.write_text(build_markdown_report(rows, Path(args.out_prefix).name), encoding='utf-8')
    print(f'JSON: {out_json}\nCSV:  {out_csv}\nMD:   {out_md}\nFilas: {len(rows)}')


if __name__ == '__main__':
    main()
