#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
score_amstar.py
================
AMSTAR-2 (Shea et al. BMJ 2017; doi:10.1136/bmj.j4008) abstract-based scoring
for the 33 systematic reviews / meta-analyses of axillary management after
neoadjuvant chemotherapy in breast cancer.

LIMITATIONS / TRANSPARENCY
--------------------------
This scoring is performed using ONLY the information in the PubMed abstract
plus the local "Relevancia" annotation (and any "Texto completo" note in the
.md). Several AMSTAR-2 items virtually always require full-text access:
    Item 5  - Duplicate study selection
    Item 6  - Duplicate data extraction
    Item 7  - List of excluded studies + reasons
    Item 12 - Impact of risk of bias on results
    Item 16 - Conflicts of interest
For these items we use "Cannot tell" by default unless the abstract or
"Relevancia" explicitly mentions the item. This is consistent with current
practice in umbrella-review pilots and is flagged as a limitation in the
manuscript Discussion.

OUTPUTS
-------
    amstar2_scores.csv      One row per SR, 16 item columns + summaries.
    amstar2_detailed.md     Human-readable report.

USAGE
-----
    python score_amstar.py

Re-run after adding new .md files to 01_revisiones_metaanalisis/.
"""
from __future__ import annotations

import csv
import re
from collections import Counter, OrderedDict
from pathlib import Path

# --------------------------------------------------------------------------- #
# Paths
# --------------------------------------------------------------------------- #
ROOT       = Path(r"C:\Users\dfduq\OneDrive\Documents\ganglio sergioc")
SRC_DIR    = ROOT / "01_revisiones_metaanalisis"
OUT_DIR    = ROOT / "predictor_ypN0" / "paper_umbrella" / "02_amstar2"
CSV_OUT    = OUT_DIR / "amstar2_scores.csv"
MD_OUT     = OUT_DIR / "amstar2_detailed.md"

# AMSTAR-2 critical items (Shea et al. 2017)
CRITICAL_ITEMS = {2, 4, 7, 9, 11, 13, 15}

# --------------------------------------------------------------------------- #
# .md parsing
# --------------------------------------------------------------------------- #
def parse_md(path: Path) -> dict:
    """Return dict with metadata + abstract text from a SR .md file."""
    text = path.read_text(encoding="utf-8", errors="replace")
    pmid    = _re_extract(r"\*\*PMID:\*\*\s*([0-9]+)", text, default="")
    doi     = _re_extract(r"\*\*DOI:\*\*\s*\[([^\]]+)\]", text, default="")
    journal = _re_extract(r"\*\*Revista:\*\*\s*([^\n(]+?)\s*\(", text, default="").strip()
    year    = _re_extract(r"\*\*Revista:\*\*\s*[^(]+\((\d{4})\)", text, default="")
    authors = _re_extract(r"\*\*Autores:\*\*\s*([^\n]+)", text, default="")
    first_author = authors.split(",")[0].strip() if authors else ""
    # Trim suffixes like " Y" or " Y et al"
    first_author = re.sub(r"\s+[A-Z]{1,3}(\s+et\s+al\.?)?\s*$", "", first_author).strip()
    title = ""
    for line in text.splitlines():
        if line.startswith("# "):
            title = line[2:].strip().rstrip(".")
            break
    abstract  = _section(text, "Abstract")
    relevance = _section(text, "Relevancia")
    fulltext  = _section(text, "Texto completo")
    return {
        "pmid": pmid,
        "first_author": first_author,
        "year": year,
        "journal": journal,
        "title": title,
        "doi": doi,
        "abstract": abstract,
        "relevance": relevance,
        "fulltext_note": fulltext,
        "filename": path.name,
    }


def _re_extract(pattern: str, text: str, default: str = "") -> str:
    m = re.search(pattern, text)
    return m.group(1).strip() if m else default


def _section(text: str, heading: str) -> str:
    """Extract text under a '## heading' header until the next '##' header."""
    pattern = rf"^##\s+{re.escape(heading)}\s*\n(.*?)(?=^##\s|\Z)"
    m = re.search(pattern, text, flags=re.MULTILINE | re.DOTALL)
    return m.group(1).strip() if m else ""


# --------------------------------------------------------------------------- #
# AMSTAR-2 scoring rules (abstract-based)
# --------------------------------------------------------------------------- #
def _all_text(rec: dict) -> str:
    return " ".join([rec["abstract"], rec["relevance"], rec["fulltext_note"], rec["title"]]).lower()


def score_record(rec: dict) -> OrderedDict:
    t = _all_text(rec)
    has_meta_analysis = bool(re.search(r"meta[- ]analy[sz]is|pooled (estimate|analys)|random[- ]effects|fixed[- ]effects|mantel[- ]haenszel|bayesian (hierarchical )?model", t))
    rec["_has_ma"] = has_meta_analysis

    items = OrderedDict()

    # --- Item 1: PICO ----------------------------------------------------- #
    # Look for population (breast cancer, node-positive), intervention (NAC, SLNB,
    # TAD), comparator (often implicit), outcomes (sensitivity/FNR/recurrence/...).
    pop_ok  = "breast cancer" in t or "breast neoplasm" in t
    int_ok  = bool(re.search(r"slnb|sentinel|axillary|tad|targeted axillary|alnd|neoadjuvant|chemotherap", t))
    out_ok  = bool(re.search(r"sensitivity|false[- ]negative|fnr|identification rate|recurrence|disease[- ]free|overall survival|pcr|complete response|accuracy", t))
    if pop_ok and int_ok and out_ok:
        items["item1"] = "Yes"
    elif (pop_ok + int_ok + out_ok) >= 2:
        items["item1"] = "Partial Yes"
    else:
        items["item1"] = "No"

    # --- Item 2: Pre-registered protocol (CRITICAL) ---------------------- #
    # AMSTAR-2 requires explicit mention of PROSPERO / a published protocol.
    if re.search(r"prospero|crd420|registered protocol|published protocol|a priori protocol|pre[- ]registered|protocol was registered", t):
        items["item2"] = "Yes" if "prospero" in t and re.search(r"crd\s*420\d{6,8}", t) else "Partial Yes"
    else:
        items["item2"] = "No"

    # --- Item 3: Justified study design selection ------------------------- #
    if re.search(r"only (randomi[sz]ed|prospective|cohort|rct).{0,40}(included|eligible)|justified|rationale for inclusion|inclusion criteria.+(design|study type)", t):
        items["item3"] = "Partial Yes"
    elif re.search(r"included (studies|articles|trials).+(retrospective|prospective|cohort|case[- ]control|rct|randomi[sz]ed)", t):
        items["item3"] = "Partial Yes"
    else:
        items["item3"] = "No"

    # --- Item 4: Comprehensive literature search (CRITICAL) -------------- #
    # Need at least 2 databases AND keywords/strategy.  PROSPERO + databases scored as 'Yes'.
    db_hits = sum(1 for db in ("pubmed", "medline", "embase", "cochrane", "scopus", "web of science", "cinahl", "lilacs", "central")
                  if db in t)
    if db_hits >= 2 and re.search(r"search (strategy|terms|string)|keywords|mesh|reference list|hand[- ]search|grey literature", t):
        items["item4"] = "Yes"
    elif db_hits >= 2:
        items["item4"] = "Partial Yes"
    elif db_hits == 1 or re.search(r"systematic (literature )?search|comprehensive search|electronic database", t):
        items["item4"] = "Partial Yes"
    else:
        items["item4"] = "No"

    # --- Item 5: Duplicate study selection -------------------------------- #
    if re.search(r"two (reviewers|authors|investigators).+(independent|select|screen)|independently (selected|screened)", t):
        items["item5"] = "Yes"
    else:
        items["item5"] = "Cannot tell"

    # --- Item 6: Duplicate data extraction -------------------------------- #
    if re.search(r"two (reviewers|authors|investigators).+(independent|extract)|independently extracted|duplicate (data )?extraction", t):
        items["item6"] = "Yes"
    else:
        items["item6"] = "Cannot tell"

    # --- Item 7: List of excluded studies + reasons (CRITICAL) ----------- #
    if re.search(r"(list|table) of excluded studies|excluded studies (with|and) reasons|prisma flow.+(reasons|excluded)", t):
        items["item7"] = "Yes"
    else:
        items["item7"] = "Cannot tell"

    # --- Item 8: Detailed description of included studies ---------------- #
    # Abstract usually gives n studies + n patients + brief PICO.
    n_studies = re.search(r"(\d+)\s+(studies|trials|articles|publications) (were )?(included|identified|eligible)", t)
    n_patients = re.search(r"(\d[\d,]*)\s+(patients|women|participants|subjects)", t)
    if n_studies and n_patients:
        items["item8"] = "Partial Yes"
    elif n_studies or n_patients:
        items["item8"] = "Partial Yes"
    else:
        items["item8"] = "No"

    # --- Item 9: Risk of bias assessment (CRITICAL) ---------------------- #
    if re.search(r"risk of bias|quality assessment|methodological quality|quadas|robins|rob\s?2|newcastle[- ]ottawa|jadad|amstar|cochrane.+(tool|risk)|grade (assessment|approach)", t):
        items["item9"] = "Partial Yes"
    else:
        items["item9"] = "No"

    # --- Item 10: Reported funding sources ------------------------------- #
    if re.search(r"funding source|source of funding|funded by|supported by (a |the )?(grant|nih|industry)", t):
        items["item10"] = "Partial Yes"
    else:
        items["item10"] = "Cannot tell"

    # --- Item 11: Appropriate statistical methods (CRITICAL) -------------- #
    if not has_meta_analysis:
        items["item11"] = "No meta-analysis"
    elif re.search(r"random[- ]effects|dersimonian|bayesian|mantel[- ]haenszel|inverse[- ]variance|hartung[- ]knapp|profile likelihood", t):
        items["item11"] = "Yes"
    elif re.search(r"fixed[- ]effects?|peto|pooled (estimate|analys)|meta[- ]analy[sz]is", t):
        items["item11"] = "Partial Yes"
    else:
        items["item11"] = "Partial Yes"

    # --- Item 12: Investigated impact of RoB on pooled results ----------- #
    if not has_meta_analysis:
        items["item12"] = "No meta-analysis"
    elif re.search(r"sensitivity analys[ie]s.+(quality|risk of bias|low[- ]risk)|excluded (low|high)[- ]quality|removed.+(low|high)[- ]quality", t):
        items["item12"] = "Yes"
    else:
        items["item12"] = "Cannot tell"

    # --- Item 13: Accounted for RoB in interpretation -------------------- #
    if re.search(r"limit(ation)?s.+(quality|bias|heterogeneity)|caution.+(quality|bias|heterogeneity)|results should be interpreted|further studies.+(quality|warranted|confirm)", t):
        items["item13"] = "Partial Yes"
    else:
        items["item13"] = "No"

    # --- Item 14: Discussion of heterogeneity ---------------------------- #
    if re.search(r"heterogeneity|i\^?2\s*=|i-?squared|tau\^?2|q[- ]statistic|chi[- ]squared|inconsistenc", t):
        items["item14"] = "Yes"
    elif re.search(r"variabilit|differ.+(across|between).+stud|range[d]?\s+from", t):
        items["item14"] = "Partial Yes"
    else:
        items["item14"] = "No"

    # --- Item 15: Investigation of publication bias (CRITICAL) ----------- #
    if not has_meta_analysis:
        items["item15"] = "No meta-analysis"
    elif re.search(r"publication bias|funnel plot|egger|begg|trim[- ]and[- ]fill|small[- ]study effect", t):
        items["item15"] = "Yes"
    else:
        items["item15"] = "No"

    # --- Item 16: Conflicts of interest ---------------------------------- #
    if re.search(r"conflict of interest|competing interest|disclosure", t):
        items["item16"] = "Partial Yes"
    else:
        items["item16"] = "Cannot tell"

    return items


# --------------------------------------------------------------------------- #
# Overall confidence rating per AMSTAR-2 algorithm
# --------------------------------------------------------------------------- #
def overall_confidence(items: OrderedDict) -> tuple[str, int, int]:
    """Return (confidence, n_critical_flaws, n_non_critical_weaknesses)."""
    crit_flaws = 0
    non_crit_weak = 0
    for i in range(1, 17):
        v = items[f"item{i}"]
        # "No meta-analysis" is N/A and does NOT count as a flaw
        if v == "No meta-analysis":
            continue
        is_weak = v in ("No", "Cannot tell")  # Cannot tell counts as a methodological gap
        # AMSTAR-2 strictly: only "No" counts. To stay AMSTAR-2 compliant we
        # treat "Cannot tell" as a non-critical weakness ONLY for reporting,
        # but do NOT count it as a critical flaw.
        if i in CRITICAL_ITEMS:
            if v == "No":
                crit_flaws += 1
            elif v == "Cannot tell":
                # Reporting gap, treated conservatively as a critical flaw because
                # critical items left unresolved should not contribute to High/Mod.
                crit_flaws += 1
        else:
            if is_weak:
                non_crit_weak += 1

    if crit_flaws == 0 and non_crit_weak <= 1:
        conf = "High"
    elif crit_flaws == 0 and non_crit_weak > 1:
        conf = "Moderate"
    elif crit_flaws == 1:
        conf = "Low"
    else:
        conf = "Critically Low"
    return conf, crit_flaws, non_crit_weak


# --------------------------------------------------------------------------- #
# Main pipeline
# --------------------------------------------------------------------------- #
def main() -> None:
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    md_files = sorted(SRC_DIR.glob("*.md"))
    print(f"Found {len(md_files)} SR files in {SRC_DIR}")

    rows = []
    for path in md_files:
        rec   = parse_md(path)
        items = score_record(rec)
        conf, n_crit, n_non = overall_confidence(items)
        counter = Counter(items.values())
        notes_parts = []
        if not rec["_has_ma"]:
            notes_parts.append("Narrative SR (no meta-analysis)")
        if items["item2"] == "No":
            notes_parts.append("no protocol reported")
        if items["item4"] == "No":
            notes_parts.append("search not described")
        if items["item9"] == "No":
            notes_parts.append("no risk-of-bias assessment")
        if items["item11"] == "No meta-analysis":
            pass
        if rec["_has_ma"] and items["item15"] == "No":
            notes_parts.append("no publication-bias assessment")
        notes = "; ".join(notes_parts) if notes_parts else "—"

        row = OrderedDict()
        row["pmid"]         = rec["pmid"]
        row["first_author"] = rec["first_author"]
        row["year"]         = rec["year"]
        row["journal"]      = rec["journal"]
        for i in range(1, 17):
            row[f"item{i}"] = items[f"item{i}"]
        row["n_yes"]              = counter.get("Yes", 0)
        row["n_partial"]          = counter.get("Partial Yes", 0)
        row["n_no"]               = counter.get("No", 0)
        row["n_cannot_tell"]      = counter.get("Cannot tell", 0)
        row["n_critical_flaws"]   = n_crit
        row["overall_confidence"] = conf
        row["notes"]              = notes
        row["_title"]             = rec["title"]      # internal only
        row["_doi"]               = rec["doi"]        # internal only
        row["_filename"]          = rec["filename"]   # internal only
        rows.append(row)

    # ------------------------------------------------------------------ CSV --
    csv_cols = [c for c in rows[0].keys() if not c.startswith("_")]
    with CSV_OUT.open("w", newline="", encoding="utf-8") as fh:
        writer = csv.DictWriter(fh, fieldnames=csv_cols)
        writer.writeheader()
        for r in rows:
            writer.writerow({k: r[k] for k in csv_cols})
    print(f"Wrote {CSV_OUT}")

    # --------------------------------------------------------------- Markdown --
    write_markdown(rows)
    print(f"Wrote {MD_OUT}")

    # ------------------------------------------------------------- Summary --
    counts = Counter(r["overall_confidence"] for r in rows)
    print("\nConfidence distribution:")
    for k in ("High", "Moderate", "Low", "Critically Low"):
        print(f"  {k:14s}: {counts.get(k, 0)}")


def write_markdown(rows: list) -> None:
    counts = Counter(r["overall_confidence"] for r in rows)
    n_total = len(rows)
    n_meta  = sum(1 for r in rows if r["item11"] != "No meta-analysis")
    n_narr  = n_total - n_meta

    rows_sorted_quality = sorted(rows, key=lambda r: (r["n_yes"] + 0.5 * r["n_partial"]), reverse=True)
    rows_sorted_low     = sorted(rows, key=lambda r: (r["n_critical_flaws"], -(r["n_yes"] + 0.5 * r["n_partial"])), reverse=True)

    item_no_counts = Counter()
    item_cant_counts = Counter()
    for r in rows:
        for i in range(1, 17):
            v = r[f"item{i}"]
            if v == "No":
                item_no_counts[i] += 1
            elif v == "Cannot tell":
                item_cant_counts[i] += 1

    item_labels = {
        1:  "PICO components",
        2:  "Pre-registered protocol *(C)*",
        3:  "Justified study designs",
        4:  "Comprehensive search *(C)*",
        5:  "Duplicate selection",
        6:  "Duplicate extraction",
        7:  "List of excluded studies *(C)*",
        8:  "Detailed description of included studies",
        9:  "Risk of bias assessment *(C)*",
        10: "Reported funding sources",
        11: "Appropriate statistical methods *(C)*",
        12: "Impact of RoB on results",
        13: "RoB in interpretation *(C)*",
        14: "Heterogeneity discussion",
        15: "Publication bias *(C)*",
        16: "Conflicts of interest",
    }

    lines = []
    lines.append("# AMSTAR-2 Quality Assessment of Systematic Reviews")
    lines.append("")
    lines.append("**Topic:** Axillary management after neoadjuvant chemotherapy (NAC) in breast cancer")
    lines.append(f"**Number of systematic reviews assessed:** {n_total}  ")
    lines.append(f"  • With meta-analysis: {n_meta}  ")
    lines.append(f"  • Narrative SR only:  {n_narr}")
    lines.append("")
    lines.append("**Tool:** AMSTAR-2 (Shea et al., *BMJ* 2017, doi:10.1136/bmj.j4008)")
    lines.append("")
    lines.append("> **Important caveat.** Scoring is based on the PubMed abstract plus local annotations only. "
                 "Items 5, 6, 7, 12 and 16 routinely require full-text access; we coded them as **\"Cannot tell\"** "
                 "rather than \"No\" when the abstract is silent. Re-scoring with full text should be performed before manuscript submission.")
    lines.append("")

    # ----- Distribution of confidence ratings ----- #
    lines.append("## 1. Distribution of overall confidence ratings")
    lines.append("")
    lines.append("| Confidence       | n  | % |")
    lines.append("|------------------|----|---|")
    for k in ("High", "Moderate", "Low", "Critically Low"):
        n = counts.get(k, 0)
        pct = f"{100.0 * n / n_total:.1f}%" if n_total else "—"
        lines.append(f"| {k:16s} | {n:>2} | {pct} |")
    lines.append(f"| **Total**        | **{n_total}** | 100% |")
    lines.append("")

    # ----- Top 10 highest quality ----- #
    lines.append("## 2. Top 10 highest-quality SRs (by Yes + ½·Partial Yes)")
    lines.append("")
    lines.append("| # | First author | Year | Journal | Confidence | Yes | Partial | No | C/T | Notes |")
    lines.append("|---|--------------|------|---------|------------|-----|---------|----|-----|-------|")
    for i, r in enumerate(rows_sorted_quality[:10], 1):
        lines.append(f"| {i} | {r['first_author']} | {r['year']} | {r['journal']} | {r['overall_confidence']} | "
                     f"{r['n_yes']} | {r['n_partial']} | {r['n_no']} | {r['n_cannot_tell']} | {r['notes']} |")
    lines.append("")

    # ----- Top 10 lowest quality ----- #
    lines.append("## 3. Top 10 lowest-quality SRs (by critical-flaw count)")
    lines.append("")
    lines.append("| # | First author | Year | Journal | Confidence | Critical flaws | Major gaps |")
    lines.append("|---|--------------|------|---------|------------|----------------|------------|")
    for i, r in enumerate(rows_sorted_low[:10], 1):
        lines.append(f"| {i} | {r['first_author']} | {r['year']} | {r['journal']} | "
                     f"{r['overall_confidence']} | {r['n_critical_flaws']} | {r['notes']} |")
    lines.append("")

    # ----- Methodological observations ----- #
    lines.append("## 4. Methodological observations across the body of evidence")
    lines.append("")
    lines.append("Frequency of unreported / inadequate items (out of {} SRs):".format(n_total))
    lines.append("")
    lines.append("| Item | Description | n \"No\" | n \"Cannot tell\" | n problematic | % problematic |")
    lines.append("|------|-------------|--------|-----------------|---------------|----------------|")
    for i in range(1, 17):
        n_no = item_no_counts[i]
        n_ct = item_cant_counts[i]
        n_pb = n_no + n_ct
        pct = f"{100.0 * n_pb / n_total:.0f}%"
        lines.append(f"| {i} | {item_labels[i]} | {n_no} | {n_ct} | {n_pb} | {pct} |")
    lines.append("")
    lines.append("*(C) marks the seven critical items per the AMSTAR-2 algorithm.*")
    lines.append("")

    most_missing = sorted(((i, item_no_counts[i] + item_cant_counts[i]) for i in range(1, 17)),
                          key=lambda x: -x[1])[:5]
    lines.append("**Most frequently unreported items:**")
    for i, n in most_missing:
        lines.append(f"- Item {i} ({item_labels[i]}) — unreported in {n}/{n_total} SRs")
    lines.append("")

    # ----- Recommendation ----- #
    primary = [r for r in rows if r["overall_confidence"] in ("High", "Moderate")]
    sensitivity = [r for r in rows if r["overall_confidence"] in ("Low", "Critically Low")]
    lines.append("## 5. Recommendation for evidence synthesis")
    lines.append("")
    lines.append(f"**Primary synthesis (Moderate + High confidence):** {len(primary)} SRs")
    if primary:
        for r in sorted(primary, key=lambda r: r["year"]):
            lines.append(f"- {r['first_author']} {r['year']} — *{r['journal']}* — {r['overall_confidence']}")
    else:
        lines.append("- *(none meet the High/Moderate threshold based on abstract scoring; full-text rescoring is essential)*")
    lines.append("")
    lines.append(f"**Sensitivity analysis only (Low + Critically Low):** {len(sensitivity)} SRs")
    lines.append("")

    # ----- Per-SR detail ----- #
    lines.append("## 6. Per-SR scoring detail")
    lines.append("")
    lines.append("| PMID | First author | Year | Journal | Confidence | Critical flaws | Notes |")
    lines.append("|------|--------------|------|---------|------------|----------------|-------|")
    for r in sorted(rows, key=lambda r: (r["year"], r["first_author"])):
        lines.append(f"| {r['pmid']} | {r['first_author']} | {r['year']} | {r['journal']} | "
                     f"{r['overall_confidence']} | {r['n_critical_flaws']} | {r['notes']} |")
    lines.append("")

    lines.append("---")
    lines.append("*Generated by `score_amstar.py`. Re-run after adding new .md files to "
                 "`01_revisiones_metaanalisis/` to refresh the scores.*")

    MD_OUT.write_text("\n".join(lines), encoding="utf-8")


if __name__ == "__main__":
    main()
