"""Congela (pre-registra) la hipotesis y el plan de analisis de un paper.

Anti-HARKing (Hypothesizing After the Results are Known): el pre-registro fija
la hipotesis y el plan de analisis ANTES de mirar los datos. Una vez congelado
(``status: frozen``) es INMUTABLE: cualquier intento posterior de re-congelar con
un contenido distinto se RECHAZA en vez de sobrescribir, de modo que no se puede
"ajustar" la hipotesis a los resultados despues de verlos.

Escribe/actualiza ``<paper_dir>/preregistration.md`` con un frontmatter
(``paper_slug``, ``frozen_at``, ``content_hash``, ``status``) y un cuerpo
markdown DETERMINISTA derivado de ``(hypotheses, analysis_plan)``.

Estilo dict-no-throw: NUNCA lanza; cualquier error previsible se captura y se
devuelve como ``{"status": "error", "note": ...}``.
"""

import hashlib
import os
from datetime import datetime, timezone


def _build_body(hypotheses: dict, analysis_plan: dict) -> str:
    """Construye el cuerpo markdown del pre-registro de forma DETERMINISTA.

    Mismo ``(hypotheses, analysis_plan)`` -> mismo cuerpo byte a byte. Las claves
    se ordenan alfabeticamente para no depender del orden de insercion del dict.
    """
    lines = ["## Hypotheses", ""]
    for k in sorted(hypotheses.keys()):
        lines.append(f"- **{k}**: {hypotheses[k]}")
    lines.append("")
    lines.append("## Analysis plan")
    lines.append("")
    for k in sorted(analysis_plan.keys()):
        lines.append(f"- **{k}**: {analysis_plan[k]}")
    return "\n".join(lines)


def _normalize(body: str) -> str:
    """Normaliza el cuerpo para el hash: strip por linea + colapsa blancos.

    Cambios irrelevantes de whitespace (espacios al final, dobles lineas en
    blanco) no alteran el hash; cambios de contenido SI. Esto hace el hash
    robusto sin perder la capacidad de detectar ediciones reales.
    """
    out = []
    prev_blank = False
    for raw in body.splitlines():
        line = raw.strip()
        if line == "":
            if prev_blank:
                continue
            prev_blank = True
        else:
            prev_blank = False
        out.append(line)
    return "\n".join(out).strip()


def _content_hash(body: str) -> str:
    """sha256 hex del cuerpo NORMALIZADO (nunca del frontmatter)."""
    return hashlib.sha256(_normalize(body).encode("utf-8")).hexdigest()


def _parse_frontmatter(text: str) -> dict:
    """Parsea el frontmatter ``--- ... ---`` simple (key: value) de un .md."""
    if not text.startswith("---"):
        return {}
    parts = text.split("---", 2)
    if len(parts) < 3:
        return {}
    fm = {}
    for line in parts[1].splitlines():
        line = line.strip()
        if not line or ":" not in line:
            continue
        key, _, value = line.partition(":")
        fm[key.strip()] = value.strip()
    return fm


def _render_file(slug: str, frozen_at: str, content_hash: str, body: str) -> str:
    """Compone el archivo completo: frontmatter frozen + cuerpo."""
    return (
        "---\n"
        f"paper_slug: {slug}\n"
        f"frozen_at: {frozen_at}\n"
        f"content_hash: {content_hash}\n"
        "status: frozen\n"
        "---\n"
        "\n"
        f"{body}\n"
    )


def preregister_hypothesis(paper_dir: str, hypotheses: dict, analysis_plan: dict) -> dict:
    """Congela la hipotesis y el plan de analisis de un paper (anti-HARKing).

    Escribe ``<paper_dir>/preregistration.md`` con frontmatter ``status: frozen``
    y un cuerpo markdown determinista. Una vez congelado es inmutable.

    Args:
        paper_dir: ruta del directorio del paper (p.ej. ``"papers/0001-mi-paper"``).
            El ``paper_slug`` es el basename del directorio. Debe existir.
        hypotheses: dict de hipotesis, p.ej.
            ``{"h0": "no hay diferencia ...", "h1": "grupo A > grupo B ..."}``.
        analysis_plan: dict con el plan, p.ej.
            ``{"test": "welch_t_test", "effect_size_metric": "cohens_d",
            "decision_rule": "...", "planned_n": 100, "multiple_correction": "holm"}``.

    Returns:
        dict dict-no-throw (NUNCA lanza). Claves segun el caso:
          - frozen:    {"status": "frozen", "path", "content_hash", "frozen_at"}
          - unchanged: {"status": "unchanged", "path", "content_hash", "frozen_at"}
          - error:     {"status": "error", "path", "note", ...}
    """
    expected_path = os.path.join(paper_dir, "preregistration.md")
    try:
        # 1) El directorio del paper debe existir; no se crea aqui.
        if not isinstance(paper_dir, str) or not os.path.isdir(paper_dir):
            return {
                "status": "error",
                "path": expected_path,
                "note": f"paper_dir no existe: {paper_dir}",
            }

        if not isinstance(hypotheses, dict) or not isinstance(analysis_plan, dict):
            return {
                "status": "error",
                "path": expected_path,
                "note": "hypotheses y analysis_plan deben ser dict",
            }

        slug = os.path.basename(os.path.normpath(paper_dir))

        # 2) + 3) Cuerpo determinista y su hash (solo del cuerpo, no del frontmatter).
        body = _build_body(hypotheses, analysis_plan)
        new_hash = _content_hash(body)

        # 5) Logica de escritura.
        if os.path.exists(expected_path):
            existing = ""
            try:
                with open(expected_path, "r", encoding="utf-8") as fh:
                    existing = fh.read()
            except OSError as exc:
                return {
                    "status": "error",
                    "path": expected_path,
                    "note": f"no se pudo leer el pre-registro existente: {exc}",
                }
            fm = _parse_frontmatter(existing)
            old_status = fm.get("status", "")
            old_hash = fm.get("content_hash", "")
            old_frozen_at = fm.get("frozen_at", "")

            if old_status == "frozen":
                if old_hash == new_hash:
                    # Idempotente: mismo contenido ya congelado. No se reescribe.
                    return {
                        "status": "unchanged",
                        "path": expected_path,
                        "content_hash": new_hash,
                        "frozen_at": old_frozen_at,
                    }
                # Inmutabilidad: ya congelado con OTRO hash -> se rechaza (anti-HARKing).
                return {
                    "status": "error",
                    "path": expected_path,
                    "content_hash": new_hash,
                    "note": (
                        "pre-registro inmutable: ya esta congelado (frozen) con un "
                        "hash distinto; un pre-registro no se puede editar tras "
                        "congelarse"
                    ),
                }
            # status != "frozen" (p.ej. draft) -> se congela ahora.

        # Archivo nuevo o draft existente: congelar con timestamp actual.
        frozen_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        file_text = _render_file(slug, frozen_at, new_hash, body)
        try:
            with open(expected_path, "w", encoding="utf-8") as fh:
                fh.write(file_text)
        except OSError as exc:
            return {
                "status": "error",
                "path": expected_path,
                "note": f"no se pudo escribir el pre-registro: {exc}",
            }
        return {
            "status": "frozen",
            "path": expected_path,
            "content_hash": new_hash,
            "frozen_at": frozen_at,
        }
    except Exception as exc:  # noqa: BLE001 - dict-no-throw: nunca propagar.
        return {
            "status": "error",
            "path": expected_path,
            "note": f"error inesperado: {exc}",
        }
