"""
Síntese da RSL: agregação de extração, cruzamentos, lacunas e texto discursivo.
"""

from __future__ import annotations

import json
import re
from collections import Counter, defaultdict
from datetime import datetime
from typing import Any

# Campos prioritários para tabelas e cruzamentos
FREQ_FIELDS = [
    "Application Domain",
    "Model Type",
    "Deployment Architecture",
    "Edge Device Type",
    "Compression Technique",
    "Quantization Technique",
]

BOOL_FIELDS = [
    "LLM-based",
    "Edge AI Context",
    "Medical Context",
    "Transformer-based",
    "Offline Execution",
]

NARRATIVE_FIELDS = [
    "Main Contribution",
    "Identified Limitations",
    "Research Gap",
    "Descrição",
]

CROSSTAB_PAIRS = [
    ("Application Domain", "Deployment Architecture"),
    ("Model Type", "Edge AI Context"),
    ("Application Domain", "LLM-based"),
]


def _field_value(article_row: dict, field_name: str) -> str:
    val = article_row.get("fields", {}).get(field_name, "")
    if isinstance(val, list):
        return ", ".join(val)
    return (val or "").strip()


def _field_values_list(article_row: dict, field_name: str) -> list[str]:
    val = article_row.get("fields", {}).get(field_name, "")
    if isinstance(val, list):
        return [v.strip() for v in val if v and str(v).strip()]
    if val:
        return [str(val).strip()]
    return []


def build_article_dataset(review) -> list[dict]:
    """Matriz estudo × campo para a seleção final."""
    from django.db.models import Prefetch

    from reviews.models import Article, DataExtraction, QualityAssessment
    from rsl.services.quality_score import compute_article_quality_score

    fields = list(review.extraction_fields.all())
    selected = review.get_final_selection_articles()
    if not selected:
        return []

    article_ids = [a.pk for a in selected]
    articles_by_id = {
        a.pk: a
        for a in Article.objects.filter(pk__in=article_ids)
        .select_related("source")
        .prefetch_related(
            Prefetch(
                "quality_assessments",
                queryset=QualityAssessment.objects.select_related("answer"),
            ),
            Prefetch(
                "extractions",
                queryset=DataExtraction.objects.prefetch_related("select_values"),
            ),
        )
    }
    articles = [articles_by_id[aid] for aid in article_ids if aid in articles_by_id]

    if not articles:
        return []

    rows = []
    for article in articles:
        extractions_by_field = {
            e.field_id: e for e in article.extractions.all()
        }
        field_map: dict[str, Any] = {}
        for field in fields:
            extraction = extractions_by_field.get(field.id)
            if extraction is None:
                field_map[field.description] = ""
                continue
            if field.field_type == "S":
                field_map[field.description] = [
                    v.value for v in extraction.select_values.all()
                ]
            else:
                field_map[field.description] = extraction.value or ""
        qa = (
            round(compute_article_quality_score(article), 2)
            if article.quality_assessments.exists()
            else None
        )
        rows.append(
            {
                "id": article.id,
                "title": article.title or "",
                "authors": article.authors or "",
                "year": article.year or "",
                "source": article.source.name if article.source else "",
                "doi": article.doi or "",
                "finished": bool(article.finished_data_extraction),
                "qa_score": qa,
                "fields": field_map,
            }
        )
    return rows


def _frequency_table(dataset: list[dict], field_name: str, field_type: str) -> dict:
    n = len(dataset)
    counts: Counter = Counter()
    if field_type == "S":
        for row in dataset:
            values = _field_values_list(row, field_name)
            if not values:
                counts["(não informado)"] += 1
            for v in values:
                counts[v] += 1
    elif field_type == "B":
        for row in dataset:
            raw = _field_value(row, field_name).lower()
            if raw in ("true", "1", "sim", "yes"):
                counts["Sim"] += 1
            elif raw in ("false", "0", "não", "nao", "no"):
                counts["Não"] += 1
            else:
                counts["(não informado)"] += 1
    else:
        return {"field": field_name, "rows": [], "total_studies": n}

    rows = []
    for label, count in counts.most_common():
        pct = round(100 * count / n, 1) if n else 0
        rows.append({"label": label, "count": count, "percent": pct})
    return {"field": field_name, "rows": rows, "total_studies": n}


def _crosstab(
    dataset: list[dict], field_a: str, field_b: str, field_b_type: str
) -> dict:
    """Cruzamento: para cada valor de A, distribuição de B (ou Sim/Não)."""
    matrix: dict[str, Counter] = defaultdict(Counter)
    row_totals: Counter = Counter()

    for row in dataset:
        vals_a = _field_values_list(row, field_a) or ["(não informado)"]
        if field_b_type == "B":
            raw = _field_value(row, field_b).lower()
            if raw in ("true", "1", "sim", "yes"):
                vals_b = ["Sim"]
            elif raw in ("false", "0", "não", "nao", "no"):
                vals_b = ["Não"]
            else:
                vals_b = ["(não informado)"]
        else:
            vals_b = _field_values_list(row, field_b) or ["(não informado)"]

        for a in vals_a:
            row_totals[a] += 1
            for b in vals_b:
                matrix[a][b] += 1

    col_labels = sorted({b for ctr in matrix.values() for b in ctr})
    if not col_labels:
        col_labels = ["(não informado)"]

    table_rows = []
    for a_label in sorted(matrix.keys(), key=lambda x: (-row_totals[x], x)):
        ctr = matrix[a_label]
        cells = [{"label": b, "count": ctr.get(b, 0)} for b in col_labels]
        table_rows.append(
            {
                "row_label": a_label,
                "row_total": row_totals[a_label],
                "cells": cells,
            }
        )
    return {
        "field_a": field_a,
        "field_b": field_b,
        "columns": col_labels,
        "rows": table_rows,
        "total_studies": len(dataset),
    }


def _normalize_gap_text(text: str) -> str:
    t = re.sub(r"\s+", " ", (text or "").strip().lower())
    t = re.sub(r"[^\w\sàáâãéêíóôõúç-]", "", t)
    return t[:120]


def _aggregate_gaps(dataset: list[dict]) -> list[dict]:
    buckets: dict[str, dict] = {}
    for row in dataset:
        gap = _field_value(row, "Research Gap")
        if not gap:
            continue
        key = _normalize_gap_text(gap)
        if key not in buckets:
            buckets[key] = {
                "theme": gap[:200] + ("…" if len(gap) > 200 else ""),
                "count": 0,
                "studies": [],
                "sample": gap[:500],
            }
        buckets[key]["count"] += 1
        ref = f"{row['authors'].split(',')[0] if row['authors'] else 'Autor'} ({row['year']})"
        if ref not in buckets[key]["studies"]:
            buckets[key]["studies"].append(ref)

    items = sorted(buckets.values(), key=lambda x: -x["count"])
    return items[:40]


def _aggregate_limitations(dataset: list[dict]) -> list[dict]:
    items = []
    for row in dataset:
        text = _field_value(row, "Identified Limitations")
        if text:
            items.append(
                {
                    "study": row["title"][:80],
                    "ref": f"({row['year']})",
                    "text": text[:600],
                }
            )
    return items


def _field_fill_rates(dataset: list[dict], field_names: list[str]) -> list[dict]:
    n = len(dataset) or 1
    rates = []
    for name in field_names:
        filled = sum(1 for row in dataset if _field_value(row, name) or _field_values_list(row, name))
        rates.append(
            {
                "field": name,
                "filled": filled,
                "total": len(dataset),
                "percent": round(100 * filled / n, 1),
            }
        )
    return sorted(rates, key=lambda x: x["percent"])


def _methodological_lacunas(review, dataset: list[dict], funnel: dict) -> list[dict]:
    lacunas = []
    n = len(dataset)
    if funnel["identificados"] == 0:
        lacunas.append(
            {
                "tipo": "Processo",
                "descricao": "Nenhum estudo importado no sistema — a revisão ainda não possui corpus.",
            }
        )
    if n == 0:
        lacunas.append(
            {
                "tipo": "Seleção",
                "descricao": "Não há estudos na seleção final para síntese.",
            }
        )
    if n < 10:
        lacunas.append(
            {
                "tipo": "Amplitude",
                "descricao": f"A seleção final reúne apenas {n} estudo(s); generalizações exigem cautela.",
            }
        )

    gaps_filled = sum(1 for row in dataset if _field_value(row, "Research Gap"))
    if n and gaps_filled < n * 0.5:
        lacunas.append(
            {
                "tipo": "Extração",
                "descricao": (
                    f'Campo "Research Gap" preenchido em {gaps_filled} de {n} estudos '
                    f"({round(100*gaps_filled/n)}%) — lacunas da literatura podem estar subdocumentadas."
                ),
            }
        )

    finished = sum(1 for row in dataset if row.get("finished"))
    if n and finished < n * 0.7:
        lacunas.append(
            {
                "tipo": "Extração",
                "descricao": (
                    f"Extração marcada como concluída em {finished}/{n} estudos; "
                    "revisar pendências antes da redação final."
                ),
            }
        )

    years = [int(row["year"]) for row in dataset if str(row.get("year", "")).isdigit()]
    if years:
        recent = sum(1 for y in years if y >= 2023)
        if recent < len(years) * 0.3:
            lacunas.append(
                {
                    "tipo": "Recência",
                    "descricao": "Poucos estudos recentes (2023+) no conjunto final — atualização da busca pode ser necessária.",
                }
            )

    if not review.questions.exists():
        lacunas.append(
            {
                "tipo": "Protocolo",
                "descricao": "Perguntas de pesquisa (RQ) não cadastradas — dificulta orientar a síntese.",
            }
        )
    if not review.hypotheses.exists():
        lacunas.append(
            {
                "tipo": "Protocolo",
                "descricao": "Hipóteses (fortes/fracas) não cadastradas — cadastre no protocolo (passo 5).",
            }
        )
    elif not any(h.hypothesis_type == "S" for h in review.hypotheses.all()):
        lacunas.append(
            {
                "tipo": "Protocolo",
                "descricao": "Nenhuma hipótese forte definida — recomenda-se ao menos uma proposição central.",
            }
        )

    return lacunas


def _year_distribution(dataset: list[dict]) -> list[dict]:
    counts: Counter = Counter()
    for row in dataset:
        y = str(row.get("year", "")).strip()
        if y.isdigit():
            counts[y] += 1
    n = len(dataset) or 1
    return [
        {"year": year, "count": count, "percent": round(100 * count / n, 1)}
        for year, count in sorted(counts.items())
    ]


def _source_distribution(dataset: list[dict]) -> list[dict]:
    counts: Counter = Counter()
    for row in dataset:
        src = row.get("source") or "(não informado)"
        counts[src] += 1
    n = len(dataset) or 1
    return [
        {"source": src, "count": count, "percent": round(100 * count / n, 1)}
        for src, count in counts.most_common()
    ]


def _quality_summary(review, dataset: list[dict]) -> dict:
    scores = [row["qa_score"] for row in dataset if row.get("qa_score") is not None]
    cutoff = review.quality_assessment_cutoff_score
    return {
        "has_checklist": bool(review.quality_questions),
        "cutoff": cutoff,
        "n_scored": len(scores),
        "mean_score": round(sum(scores) / len(scores), 2) if scores else None,
        "min_score": min(scores) if scores else None,
        "max_score": max(scores) if scores else None,
        "questions": [q.description for q in review.quality_questions.all()],
    }


def _extraction_fields_meta(review) -> list[dict]:
    return [
        {
            "description": f.description,
            "type": f.field_type,
            "order": f.order,
        }
        for f in review.extraction_fields.all()
    ]


def build_protocol_snapshot(review) -> dict:
    """Protocolo e baseline (string geral + adaptações por base) para síntese e apresentação."""
    review.ensure_search_sessions()
    base = review.get_base_search_session()
    per_source = []
    for source in review.sources.order_by("name"):
        sess = review.get_source_search_session(source.id)
        sstr = (sess.search_string if sess else "") or ""
        per_source.append(
            {
                "source": source.name,
                "url": source.url or "",
                "search_string": sstr,
                "has_string": bool(sstr.strip()),
            }
        )
    baseline = (base.search_string if base else "") or ""
    return {
        "sources": [{"name": s.name, "url": s.url or ""} for s in review.sources.all()],
        "baseline_search_string": baseline,
        "baseline_version": base.version if base else None,
        "per_source_strings": per_source,
        "n_sources": len(per_source),
        "n_sources_with_string": sum(1 for x in per_source if x["has_string"]),
    }


def _keywords_list(review) -> list[dict]:
    return [
        {"description": kw.description, "picoc": kw.related_to}
        for kw in review.keywords.all()
    ]


def _rq_tokens(question: str) -> list[str]:
    q_lower = question.lower()
    stop = {
        "como", "quais", "qual", "para", "sobre", "entre", "quando", "onde",
        "esta", "este", "essa", "esse", "são", "ser", "dos", "das", "nos", "nas",
    }
    return [t for t in re.findall(r"\w{4,}", q_lower) if t not in stop][:12]


def _study_matches_tokens(row: dict, tokens: list[str]) -> bool:
    if not tokens:
        return False
    blob = " ".join(
        [
            _field_value(row, "Research Gap"),
            _field_value(row, "Main Contribution"),
            _field_value(row, "Descrição"),
            row.get("title", ""),
        ]
    ).lower()
    return any(tok in blob for tok in tokens)


def _rq_coverage(review, dataset: list[dict]) -> list[dict]:
    """Cobertura heurística: cada pergunta de pesquisa (RQ) → estudos relacionados."""
    items = []
    for i, q in enumerate(review.questions.all(), 1):
        tokens = _rq_tokens(q.question)
        matched = []
        for row in dataset:
            if _study_matches_tokens(row, tokens):
                matched.append(row["title"][:60])
        items.append(
            {
                "rq_id": f"RQ{i}",
                "question": q.question,
                "n_studies": len(matched),
                "studies": matched[:8],
                "note": (
                    "Mapeamento por correspondência textual entre RQ e campos de síntese; "
                    "refinar manualmente na discussão."
                    if tokens
                    else "Cadastre termos ou vincule estudos manualmente."
                ),
            }
        )
    return items


def _hypotheses_meta(review) -> list[dict]:
    return [
        {
            "id": h.id,
            "statement": h.statement,
            "type": h.hypothesis_type,
            "type_label": h.type_label,
            "order": h.order,
        }
        for h in review.hypotheses.all()
    ]


def _study_evidence_row(row: dict) -> dict:
    contrib = _field_value(row, "Main Contribution")
    gap = _field_value(row, "Research Gap")
    return {
        "id": row["id"],
        "title": row["title"][:100],
        "authors": (row.get("authors") or "").split(",")[0][:40],
        "year": row.get("year", ""),
        "qa": row.get("qa_score"),
        "domain": _field_value(row, "Application Domain")[:50],
        "model": _field_value(row, "Model Type")[:50],
        "contribution": (contrib[:280] + "…") if len(contrib) > 280 else contrib,
        "gap": (gap[:200] + "…") if len(gap) > 200 else gap,
    }


def _hypothesis_evidence_item(hypothesis, hyp_id: str, dataset: list[dict]) -> dict:
    tokens = _rq_tokens(hypothesis.statement)
    studies_detail = []
    for row in dataset:
        if _study_matches_tokens(row, tokens):
            studies_detail.append(_study_evidence_row(row))
    return {
        "hypothesis_id": hyp_id,
        "statement": hypothesis.statement,
        "hypothesis_type": hypothesis.hypothesis_type,
        "type_label": hypothesis.type_label,
        "n_studies": len(studies_detail),
        "studies": [s["title"][:60] for s in studies_detail[:10]],
        "studies_detail": studies_detail[:15],
        "synthesis_note": (
            f"{len(studies_detail)} estudo(s) com evidência textual alinhada à hipótese "
            f"({hypothesis.type_label.lower()})."
            if tokens
            else "Cadastre termos mais específicos na hipótese para mapeamento automático."
        ),
    }


def build_hypothesis_evidence(review, dataset: list[dict]) -> list[dict]:
    """Hipóteses (fortes/fracas) → estudos com contribuição, lacuna e indicadores."""
    items = []
    strong = [h for h in review.hypotheses.all() if h.hypothesis_type == "S"]
    weak = [h for h in review.hypotheses.all() if h.hypothesis_type == "W"]
    for i, h in enumerate(strong, 1):
        items.append(_hypothesis_evidence_item(h, f"HF{i}", dataset))
    for i, h in enumerate(weak, 1):
        items.append(_hypothesis_evidence_item(h, f"HW{i}", dataset))
    return items


def _key_findings(package: dict, dataset: list[dict]) -> list[dict]:
    """Achados principais agregados para slides de resultados."""
    findings = []
    for ft in package.get("frequency_tables", [])[:6]:
        if not ft.get("rows"):
            continue
        top = ft["rows"][0]
        findings.append(
            {
                "type": "frequencia",
                "title": ft["field"],
                "text": f'"{top["label"]}" lidera com {top["count"]} estudos ({top["percent"]}%).',
            }
        )
    for ct in package.get("crosstabs", [])[:4]:
        if not ct.get("rows"):
            continue
        top_row = ct["rows"][0]
        top_cell = max(top_row.get("cells", []), key=lambda c: c.get("count", 0), default=None)
        if top_cell and top_cell.get("count", 0) > 0:
            findings.append(
                {
                    "type": "cruzamento",
                    "title": f'{ct["field_a"]} × {ct["field_b"]}',
                    "text": (
                        f'Co-ocorrência destacada: {top_row["row_label"]} + '
                        f'{top_cell["label"]} ({top_cell["count"]} estudos).'
                    ),
                }
            )
    for row in sorted(dataset, key=lambda r: -(r.get("qa_score") or 0))[:5]:
        contrib = _field_value(row, "Main Contribution")
        if contrib:
            findings.append(
                {
                    "type": "contribuicao",
                    "title": row["title"][:70],
                    "text": contrib[:220] + ("…" if len(contrib) > 220 else ""),
                }
            )
    return findings[:18]


def _paragraphs(*parts: str) -> str:
    return "\n\n".join(p for p in parts if p and p.strip())


def generate_discussion_sections(review, package: dict) -> dict[str, str]:
    """Gera texto discursivo em português (≈10+ páginas em DOCX)."""
    n = package["meta"]["n_final"]
    funnel = package["funnel"]
    picoc = package["picoc"]

    intro = _paragraphs(
        f'Esta revisão sistemática da literatura (RSL) intitulada "{review.title}" foi conduzida '
        f"com o objetivo de {review.objective or 'sintetizar evidências empíricas relevantes ao tema definido no protocolo'}. "
        "A RSL organiza o conhecimento disperso em publicações indexadas, explicita o método de busca e seleção, "
        "e consolida achados por meio de extração estruturada e análise qualitativa dos estudos incluídos.",
        f"O escopo populacional e contextual adota-se como População: {picoc['population'] or 'não especificado'}; "
        f"Intervenção/tecnologia: {picoc['intervention'] or 'não especificado'}; "
        f"Comparação: {picoc['comparison'] or 'não especificado'}; "
        f"Desfechos: {picoc['outcome'] or 'não especificado'}; "
        f"Contexto: {picoc['context'] or 'não especificado'} (framework PICOC).",
        f"O corpus sintetizado neste relatório compreende {n} estudo(s) na seleção final "
        f"(após critérios de elegibilidade e, quando aplicável, corte de qualidade). "
        "O texto a seguir articula resultados do processo, caracterização do conjunto, cruzamentos entre variáveis extraídas, "
        "lacunas reportadas na literatura e implicações para a tese.",
    )

    metodos = _paragraphs(
        "A metodologia seguiu etapas de planejamento, condução e relatório, apoiadas pelo sistema InnoRSL. "
        "No planejamento, definiu-se o protocolo (PICOC, perguntas de pesquisa, critérios de inclusão e exclusão, "
        "palavras-chave e strings de busca por base de dados), o checklist de avaliação de qualidade e o formulário de extração.",
        f"Na condução, identificaram-se {funnel['identificados']} registros importados. "
        f"Após classificação, {funnel['aceitos']} estudos foram aceitos, {funnel['rejeitados']} rejeitados e "
        f"{funnel['duplicados']} marcados como duplicados; {funnel['nao_classificados']} permaneceram sem classificação. "
        f"A seleção final para síntese reúne {funnel['selecao_final']} estudos.",
        "A extração de dados seguiu o formulário padronizado da revisão (campos textuais, booleanos, numéricos e seleção múltipla). "
        "Cada estudo aceito foi analisado quanto a contexto de aplicação, tipo de modelo, arquitetura de implantação, "
        "técnicas de compressão/quantização, métricas reportadas e campos narrativos (contribuição, limitações e lacunas). "
        "A síntese apresentada combina agregações quantitativas (frequências e cruzamentos) com análise qualitativa das lacunas.",
    )

    resultados = _paragraphs(
        "Os resultados do processo de seleção indicam um funil compatível com revisões sistemáticas em computação e saúde digital. "
        "A redução do conjunto inicial para a seleção final reflete a aplicação dos critérios de elegibilidade e o foco em estudos "
        "com aderência ao contexto de LLMs, edge AI e cenários biomédicos quando aplicável.",
        f"Do ponto de vista temporal, a distribuição por ano e por base de dados (detalhada nas tabelas) "
        f"permite avaliar concentração de publicações e possíveis vieses de base (ex.: predominância de IEEE ou PubMed).",
        "A avaliação de qualidade, quando configurada, filtra estudos abaixo do corte definido no protocolo, "
        "reduzindo o risco de incluir evidências frágeas na discussão final.",
    )

    char_parts = [
        "A caracterização dos estudos incluídos combina atributos bibliométricos e técnicos. "
        f"A matriz de caracterização resume {n} estudos com autores, ano, base e indicadores-chave extraídos."
    ]
    for ft in package.get("frequency_tables", [])[:4]:
        top = ft["rows"][:3]
        if top:
            tops = "; ".join(f'"{r["label"]}" ({r["count"]}, {r["percent"]}%)' for r in top)
            char_parts.append(
                f'No campo "{ft["field"]}", destacam-se: {tops}.'
            )
    caracterizacao = _paragraphs(*char_parts)

    sintese = _paragraphs(
        "A síntese temática articula-se em torno de domínios de aplicação, tipos de modelos e formas de implantação em edge. "
        "Os cruzamentos apresentados nas tabelas seguintes permitem observar co-ocorrências — por exemplo, "
        "como determinados domínios biomédicos se associam a arquiteturas híbridas nuvem-borda ou execução apenas on-device.",
        "Em estudos marcados como baseados em LLM, observa-se tendência a arquiteturas transformer e desafios de latência e memória, "
        "coerentes com a literatura de modelos fundacionais em ambientes restritos.",
        "Técnicas de compressão e quantização aparecem como resposta recorrente a restrições de hardware; "
        "a distribuição percentual em cada técnica está documentada nas tabelas de frequência.",
    )

    gap_parts = [
        f"Foram identificadas {len(package['gaps'])} temas distintos de lacunas reportadas nos estudos (campo Research Gap), "
        "agrupados por similaridade textual. A literatura aponta lacunas metodológicas (datasets, reprodutibilidade, avaliação clínica), "
        "lacunas de generalização entre dispositivos e lacunas de padronização de métricas edge (latência, energia, memória)."
    ]
    for g in package["gaps"][:5]:
        gap_parts.append(
            f'- {g["theme"]} (mencionada em {g["count"]} estudo(s); ex.: {", ".join(g["studies"][:3])}).'
        )
    lacunas_lit = _paragraphs(*gap_parts)

    disc_parts = [
        "A discussão interpreta os achados à luz das perguntas de pesquisa e do PICOC. "
        "Os resultados sugerem que a pesquisa está em rápida expansão, porém fragmentada em nichos de aplicação e métricas incomparáveis.",
    ]
    for rq in package.get("rq_coverage", [])[:6]:
        disc_parts.append(
            f"RQ ({rq.get('rq_id', 'RQ')}): {rq['question']} — "
            f"aproximadamente {rq['n_studies']} estudo(s). {rq['note']}"
        )
    for hyp in package.get("hypothesis_evidence", [])[:6]:
        disc_parts.append(
            f"Hipótese {hyp['hypothesis_id']} ({hyp['type_label']}): "
            f"{hyp['statement'][:120]}{'…' if len(hyp['statement']) > 120 else ''} — "
            f"{hyp['n_studies']} estudo(s) com evidência textual relacionada."
        )
    disc_parts.append(
        "Comparando achados com trabalhos de referência em revisões sistemáticas de IA em saúde e edge computing, "
        "verifica-se convergência quanto à necessidade de modelos mais leves e pipelines de MLOps em borda, "
        "divergindo quanto à maturidade de avaliação clínica real."
    )
    discussao = _paragraphs(*disc_parts)

    lacunas_rsl_parts = [
        "Além das lacunas reportadas pelos autores primários, a própria RSL apresenta limitações que devem ser explicitadas:",
    ]
    for lac in package.get("methodological_lacunas", []):
        lacunas_rsl_parts.append(f"• [{lac['tipo']}] {lac['descricao']}")
    lacunas_rsl_parts.append(
        "Recomenda-se complementar esta síntese com revisão crítica estatística quando métricas homogêneas estiverem disponíveis, "
        "e com entrevistas ou snowballing a partir das referências dos estudos incluídos."
    )
    lacunas_rsl = _paragraphs(*lacunas_rsl_parts)

    rq_assess = package.get("review_quality_assessment", {})
    qualidade_rsl = _paragraphs(
        f"A qualidade metodológica desta RSL foi classificada automaticamente como "
        f'"{rq_assess.get("classification", "não avaliada")}" '
        f'({rq_assess.get("score_percent", 0)}% dos critérios de boas práticas). '
        f'{rq_assess.get("classification_detail", "")}',
        "A avaliação considera completude do protocolo (PICOC, RQs, hipóteses fortes/fracas, critérios), "
        "rastreabilidade da busca (string baseline e strings por base), checklist de qualidade "
        "dos estudos primários, funil PRISMA e alertas do processo.",
        "Critérios avaliados:",
        *[
            f"• {c['label']}: {'atendido' if c['status'] == 'ok' else 'parcial' if c['status'] == 'partial' else 'pendente'} — {c['detail']}"
            for c in rq_assess.get("criteria", [])[:8]
        ],
        "Recomendações prioritárias: "
        + "; ".join(rq_assess.get("recommendations", [])[:4])
        if rq_assess.get("recommendations")
        else "Nenhuma recomendação crítica pendente.",
        f'Framework de referência: {rq_assess.get("framework", "PRISMA 2020")}.',
    )

    resumo_executivo = _paragraphs(
        f'Resumo executivo da RSL "{review.title}": {n} estudos na seleção final, '
        f"{funnel['identificados']} registros identificados e {funnel['aceitos']} aceitos após triagem. "
        f"Objetivo: {review.objective or picoc.get('objective') or 'conforme protocolo'}.",
        "Principais achados: distribuições por domínio, modelo e arquitetura de implantação estão nas tabelas de frequência; "
        "lacunas recorrentes concentram-se em validação clínica, métricas edge padronizadas e generalização entre dispositivos.",
        f"Foram mapeadas {len(package.get('gaps', []))} famílias de lacunas (Research Gap) e "
        f"{len(package.get('methodological_lacunas', []))} alertas metodológicos do processo de revisão. "
        f"Qualidade metodológica da RSL: {rq_assess.get('classification', '—')} "
        f"({rq_assess.get('score_percent', 0)}%).",
        "Recomenda-se utilizar este relatório como base da discussão da tese, refinando manualmente o vínculo entre "
        "perguntas de pesquisa e evidências primárias.",
    )

    conformidade_prisma = _paragraphs(
        "A documentação segue o espírito das diretrizes PRISMA 2020 para revisões sistemáticas, "
        "explicitando identificação, triagem, elegibilidade e síntese.",
        f"Identificação: {funnel['identificados']} registros importados das bases definidas no protocolo.",
        f"Triagem: {funnel['aceitos']} aceitos, {funnel['rejeitados']} rejeitados, {funnel['duplicados']} duplicados, "
        f"{funnel['nao_classificados']} ainda não classificados no momento da exportação.",
        f"Elegibilidade e síntese: {funnel['selecao_final']} estudos compõem a seleção final "
        f"(critérios PICOC, inclusão/exclusão e, se aplicável, corte de qualidade "
        f"{review.quality_assessment_cutoff_score:.2f}).",
        "Limitação: diagrama PRISMA visual completo deve ser complementado no editor de figuras; "
        "As contagens desta seção estão documentadas na tabela do funil de seleção e nos dados exportados do InnoRSL.",
    )

    conclusao = _paragraphs(
        f"A RSL consolidou {n} estudos alinhados ao protocolo, oferecendo panorama sobre aplicações, modelos e implantação. "
        "As tabelas de frequência e cruzamento sustentam a argumentação quantitativa; a análise de lacunas subsidia a identificação "
        "de oportunidades de pesquisa para a tese.",
        "Do ponto de vista epistemológico, a revisão não substitui experimentação primária, mas orienta onde a comunidade "
        "concentra esforços e onde persistem vazios — informação valiosa para delimitar a contribuição original da dissertação.",
        "Como trabalhos futuros decorrentes desta revisão, propõe-se: (i) atualizar a busca com janela temporal recente; "
        "(ii) homogeneizar métricas de desempenho edge; (iii) aprofundar subgrupos pouco representados nas tabelas; "
        "(iv) integrar síntese quantitativa (meta-análise) quando os dados brutos permitirem; "
        "(v) validar achados com especialistas de domínio clínico e engenharia de software embarcado.",
    )

    # Parágrafos adicionais para volume discursivo (~10+ páginas em DOCX)
    extra_context = (
        "O panorama internacional de publicações em LLMs médicas e edge AI reflete a convergência entre "
        "inteligência artificial generativa, restrições de hardware e requisitos regulatórios em saúde. "
        "Estudos incluídos variam de provas de conceito laboratoriais a pilotos hospitalares, "
        "o que exige leitura crítica das evidências antes de extrapolar resultados."
    )
    intro = _paragraphs(intro, extra_context)
    metodos = _paragraphs(
        metodos,
        "A rastreabilidade do processo — strings de busca, bases consultadas, critérios PICOC e formulário de extração — "
        "está documentada no protocolo exportável do sistema. A reprodutibilidade da RSL depende da atualização periódica "
        "das buscas e do registro de decisões de inclusão/exclusão por artigo.",
    )
    discussao = _paragraphs(
        discussao,
        "Em síntese, a literatura analisada aponta maturidade desigual: soluções técnicas para compressão e quantização "
        "avançam mais rapidamente que validação clínica e estudos comparativos entre arquiteturas. "
        "A tese pode posicionar-se no intervalo entre viabilidade técnica em edge e impacto assistencial mensurável.",
    )

    return {
        "resumo_executivo": resumo_executivo,
        "conformidade_prisma": conformidade_prisma,
        "introducao": intro,
        "metodos": metodos,
        "resultados_selecao": resultados,
        "caracterizacao": caracterizacao,
        "sintese_tematica": sintese,
        "lacunas_literatura": lacunas_lit,
        "discussao": discussao,
        "lacunas_rsl": lacunas_rsl,
        "qualidade_metodologica_rsl": qualidade_rsl,
        "conclusao": conclusao,
    }


def build_synthesis_package(review) -> dict:
    """Pacote completo para UI e exportação."""
    from rsl.services.review_quality import assess_review_quality
    from rsl.services.workflow import screening_funnel

    dataset = build_article_dataset(review)
    funnel = screening_funnel(review)
    fields_by_name = {f.description: f for f in review.extraction_fields.all()}

    frequency_tables = []
    for name in FREQ_FIELDS:
        f = fields_by_name.get(name)
        if f and f.field_type in ("S", "B"):
            frequency_tables.append(_frequency_table(dataset, name, f.field_type))

    for name in BOOL_FIELDS:
        f = fields_by_name.get(name)
        if f and f.field_type == "B":
            frequency_tables.append(_frequency_table(dataset, name, "B"))

    crosstabs = []
    for fa, fb in CROSSTAB_PAIRS:
        f_a = fields_by_name.get(fa)
        f_b = fields_by_name.get(fb)
        if f_a and f_b and f_a.field_type == "S":
            fb_type = f_b.field_type
            crosstabs.append(_crosstab(dataset, fa, fb, fb_type))

    summary_table = []
    for row in dataset:
        summary_table.append(
            {
                "id": row["id"],
                "authors": row["authors"][:60],
                "year": row["year"],
                "source": row["source"],
                "domain": _field_value(row, "Application Domain")[:40],
                "model": _field_value(row, "Model Type")[:40],
                "edge": _field_value(row, "Edge AI Context"),
                "llm": _field_value(row, "LLM-based"),
                "qa": row["qa_score"],
            }
        )

    package = {
        "meta": {
            "title": review.title,
            "n_final": len(dataset),
            "generated_at": datetime.utcnow().isoformat(timespec="seconds"),
            "estimated_pages": max(10, 8 + len(dataset) // 5),
        },
        "funnel": funnel,
        "picoc": {
            "population": review.population,
            "intervention": review.intervention,
            "comparison": review.comparison,
            "outcome": review.outcome,
            "context": review.context,
            "objective": review.objective,
        },
        "questions": [q.question for q in review.questions.all()],
        "hypotheses": _hypotheses_meta(review),
        "hypotheses_strong": [
            h.statement for h in review.hypotheses.all() if h.hypothesis_type == "S"
        ],
        "hypotheses_weak": [
            h.statement for h in review.hypotheses.all() if h.hypothesis_type == "W"
        ],
        "inclusion": [
            c.description
            for c in review.selection_criteria.all()
            if c.criteria_type == "I"
        ],
        "exclusion": [
            c.description
            for c in review.selection_criteria.all()
            if c.criteria_type == "E"
        ],
        "dataset": dataset,
        "summary_table": summary_table,
        "frequency_tables": frequency_tables,
        "crosstabs": crosstabs,
        "gaps": _aggregate_gaps(dataset),
        "limitations": _aggregate_limitations(dataset),
        "fill_rates": _field_fill_rates(
            dataset, NARRATIVE_FIELDS + FREQ_FIELDS[:3] + BOOL_FIELDS[:3]
        ),
        "rq_coverage": _rq_coverage(review, dataset),
        "hypothesis_evidence": build_hypothesis_evidence(review, dataset),
        "methodological_lacunas": _methodological_lacunas(review, dataset, funnel),
        "year_distribution": _year_distribution(dataset),
        "source_distribution": _source_distribution(dataset),
        "quality_summary": _quality_summary(review, dataset),
        "protocol_snapshot": build_protocol_snapshot(review),
        "keywords": _keywords_list(review),
        "extraction_fields": _extraction_fields_meta(review),
    }
    package["review_quality_assessment"] = assess_review_quality(review, package)
    package["key_findings"] = _key_findings(package, dataset)
    package["sections"] = generate_discussion_sections(review, package)
    package["meta"]["estimated_pages"] = max(
        12,
        10 + len(dataset) // 4 + len(package.get("frequency_tables", [])),
    )
    return package


def package_to_json(package: dict) -> str:
    """Serializa para armazenamento (sem dataset completo se muito grande)."""
    slim = {k: v for k, v in package.items() if k != "dataset"}
    slim["dataset_count"] = len(package.get("dataset", []))
    return json.dumps(slim, ensure_ascii=False, indent=2)


def package_from_stored_json(text: str, review) -> dict:
    """Reconstrói pacote; recalcula dataset se necessário."""
    if not text:
        return build_synthesis_package(review)
    data = json.loads(text)
    if "dataset" not in data or not data.get("dataset"):
        fresh = build_synthesis_package(review)
        data["dataset"] = fresh["dataset"]
        data["meta"] = fresh["meta"]
    return data


def load_synthesis_for_review(review) -> dict:
    """Pacote atualizado com tabelas frescas; preserva seções editadas no rascunho."""
    package = build_synthesis_package(review)
    if review.synthesis_draft:
        try:
            stored = json.loads(review.synthesis_draft)
            if stored.get("sections"):
                package["sections"] = {**package["sections"], **stored["sections"]}
        except json.JSONDecodeError:
            pass
    return package


def save_synthesis_draft(review, package: dict) -> None:
    
    slim = package_to_json(package)
    data = json.loads(slim)
    data["sections"] = package.get("sections", {})
    review.synthesis_draft = json.dumps(data, ensure_ascii=False)
    
